Updated on 2026-08-14
This commit is contained in:
commit
e388c14eb9
184 changed files with 4338 additions and 1298 deletions
|
|
@ -139,10 +139,10 @@ abstract class BaseTestCase : TestCase(
|
|||
return ApplicationInjectionExecutionRule(
|
||||
toggleStates = mapOf(
|
||||
"NEW_TOKEN_RECEIVE_ENABLED" to true,
|
||||
"WALLET_BALANCE_FETCHER_ENABLED" to true,
|
||||
"SWAP_REDESIGN_ENABLED" to true,
|
||||
"SWAP_REDESIGN_ENABLED" to false,
|
||||
"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_SHORTENED_ADDRESS = "0x5aa711F440Eb6d43...89d03464628ace84"
|
||||
const val BITCOIN_ADDRESS = "bc1qtg9aa6jcpqtvun0pe0uct7sxm8nq2nsxfmfxm3"
|
||||
const val BITCOIN_RECIPIENT_ADDRESS = "bc1qt90qc0na7z05nh63kyd78tujfc8vqv6sl7e4a9"
|
||||
const val CARDANO_ADDRESS =
|
||||
"addr1q8f9499e58k4hhfd9vhawprxt3xd94x7rmlyp33ee4xkatakcl2zgkrg0p6ceqkndtkw4cumfe9enhdph8yhuswn785srksm9p"
|
||||
const val SOLANA_RECIPIENT_ADDRESS = "5fcy9woa8Di1QHcce65CsV3XKrxdB2pD4HJx5xx82ipM"
|
||||
|
|
@ -30,6 +31,7 @@ object TestConstants {
|
|||
const val XRP_ACTIVATED_RECIPIENT_ADDRESS = "rN7n7otQDd6FczFgLdSqtcsAUxDkw6fzRH"
|
||||
const val DOGECOIN_RECIPIENT_ADDRESS = "DJQR3bdhBKcFGMHX2BkMCkrMFApNWNzr6V"
|
||||
const val DOGECOIN_ADDRESS = "DJ2TaZ5vvp3mBLugUpKjVM3pRBLi4uYaqz"
|
||||
const val TERRA_RECIPIENT_ADDRESS = "terra148dmp5ccazcwdmrcpvqz5rprnn886kemqen3tj"
|
||||
|
||||
const val WAIT_UNTIL_TIMEOUT = 20_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") {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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 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.wallet.R
|
||||
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 androidx.compose.ui.test.hasText as withText
|
||||
|
||||
class SelectNetworkFeePageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<SelectNetworkFeePageObject>(semanticsProvider = semanticsProvider) {
|
||||
class SwapSelectNetworkFeeBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<SwapSelectNetworkFeeBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val title: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.TITLE)
|
||||
|
|
@ -21,22 +21,22 @@ class SelectNetworkFeePageObject(semanticsProvider: SemanticsNodeInteractionsPro
|
|||
}
|
||||
|
||||
val marketSelectorItem: KNode = child {
|
||||
hasTestTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM)
|
||||
hasTestTag(SwapSelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM)
|
||||
hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_market)))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val fastSelectorItem: KNode = child {
|
||||
hasTestTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM)
|
||||
hasTestTag(SwapSelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM)
|
||||
hasAnyChild(withText(getResourceString(R.string.common_fee_selector_option_fast)))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val readMoreTextBlock: KNode = child {
|
||||
hasTestTag(SelectNetworkFeeBottomSheetTestTags.READ_MORE_TEXT)
|
||||
hasTestTag(SwapSelectNetworkFeeBottomSheetTestTags.READ_MORE_TEXT)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onSelectNetworkFeeBottomSheet(function: SelectNetworkFeePageObject.() -> Unit) =
|
||||
internal fun BaseTestCase.onSwapSelectNetworkFeeBottomSheet(function: SwapSelectNetworkFeeBottomSheetPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -215,19 +215,19 @@ class SwapTokenTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
step("Assert 'Select fee' bottom sheet title is displayed") {
|
||||
onSelectNetworkFeeBottomSheet { title.assertIsDisplayed() }
|
||||
onSwapSelectNetworkFeeBottomSheet { title.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Market' item is displayed") {
|
||||
onSelectNetworkFeeBottomSheet { marketSelectorItem.assertIsDisplayed() }
|
||||
onSwapSelectNetworkFeeBottomSheet { marketSelectorItem.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Fast' item is displayed") {
|
||||
onSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
|
||||
onSwapSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Read more' text block is displayed") {
|
||||
onSelectNetworkFeeBottomSheet { readMoreTextBlock.assertIsDisplayed() }
|
||||
onSwapSelectNetworkFeeBottomSheet { readMoreTextBlock.assertIsDisplayed() }
|
||||
}
|
||||
step("Click on 'Fast' item") {
|
||||
onSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
|
||||
onSwapSelectNetworkFeeBottomSheet { fastSelectorItem.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Network fee' block is displayed") {
|
||||
onSwapTokenScreen {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
fun provideFetchStakingOptionsUseCase(
|
||||
stakeKitRepository: StakeKitRepository,
|
||||
p2pRepository: P2PEthPoolRepository,
|
||||
p2pEthPoolRepository: P2PEthPoolRepository,
|
||||
stakingErrorResolver: StakingErrorResolver,
|
||||
): FetchStakingOptionsUseCase {
|
||||
return FetchStakingOptionsUseCase(
|
||||
stakeKitRepository = stakeKitRepository,
|
||||
p2pRepository = p2pRepository,
|
||||
p2pEthPoolRepository = p2pEthPoolRepository,
|
||||
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),
|
||||
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(
|
||||
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),
|
||||
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(
|
||||
|
|
@ -318,6 +340,20 @@ object WalletMockContent : MockContent {
|
|||
parentFingerprint = byteArrayOf(0, 0, 0, 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),
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
|
|||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
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.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
|
|
@ -54,6 +55,7 @@ internal class CardSettingsModel @Inject constructor(
|
|||
private val getUserWalletUseCase: GetUserWalletUseCase,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val onboardingRepository: OnboardingRepository,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<CardSettingsComponent.Params>()
|
||||
|
|
@ -218,15 +220,19 @@ internal class CardSettingsModel @Inject constructor(
|
|||
} else {
|
||||
val card = scanResponse.card
|
||||
|
||||
store.dispatchNavigationAction {
|
||||
push(
|
||||
route = AppRoute.ResetToFactory(
|
||||
userWalletId = userWalletId,
|
||||
cardId = card.cardId,
|
||||
isActiveBackupStatus = card.backupStatus?.isActive == true,
|
||||
backupCardsCount = scanResponse.getBackupCardsCount() ?: 0,
|
||||
),
|
||||
)
|
||||
modelScope.launch {
|
||||
val hasTangemPay = onboardingRepository.checkCustomerWallet(userWalletId).getOrNull() == true
|
||||
store.dispatchNavigationAction {
|
||||
push(
|
||||
route = AppRoute.ResetToFactory(
|
||||
userWalletId = userWalletId,
|
||||
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.SettingsScreensScaffold
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import com.tangem.tap.features.details.ui.resetcard.ResetCardScreenState.Dialog as ResetCardDialog
|
||||
|
||||
@Composable
|
||||
|
|
@ -114,22 +115,11 @@ private fun Description(text: TextReference) {
|
|||
@Composable
|
||||
private fun Conditions(state: ResetCardScreenState) {
|
||||
state.warningsToShow.forEach { warning ->
|
||||
when (warning) {
|
||||
ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> {
|
||||
ConditionCheckBox(
|
||||
checkedState = state.isAcceptCondition1Checked,
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
ConditionCheckBox(
|
||||
checkedState = warning.isChecked,
|
||||
onCheckedChange = { state.onToggleWarning(warning.type) },
|
||||
description = warning.description,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -239,14 +229,16 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) {
|
|||
ResetCardScreen(
|
||||
state = ResetCardScreenState(
|
||||
isResetButtonEnabled = true,
|
||||
isResetPasswordButtonShown = false,
|
||||
warningsToShow = listOf(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS),
|
||||
warningsToShow = persistentListOf(
|
||||
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),
|
||||
isAcceptCondition1Checked = false,
|
||||
isAcceptCondition2Checked = false,
|
||||
onAcceptCondition1ToggleClick = {},
|
||||
onAcceptCondition2ToggleClick = {},
|
||||
onResetButtonClick = {},
|
||||
onToggleWarning = {},
|
||||
dialog = null,
|
||||
),
|
||||
onBackClick = {},
|
||||
|
|
|
|||
|
|
@ -3,16 +3,13 @@ package com.tangem.tap.features.details.ui.resetcard
|
|||
import androidx.annotation.StringRes
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
internal data class ResetCardScreenState(
|
||||
val isResetButtonEnabled: Boolean,
|
||||
val descriptionText: TextReference,
|
||||
val warningsToShow: List<WarningsToReset>,
|
||||
val isResetPasswordButtonShown: Boolean,
|
||||
val isAcceptCondition1Checked: Boolean,
|
||||
val isAcceptCondition2Checked: Boolean,
|
||||
val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
|
||||
val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
|
||||
val warningsToShow: ImmutableList<WarningUM>,
|
||||
val onToggleWarning: (WarningType) -> Unit,
|
||||
val onResetButtonClick: () -> Unit,
|
||||
val dialog: Dialog?,
|
||||
) {
|
||||
|
|
@ -58,7 +55,13 @@ internal data class ResetCardScreenState(
|
|||
}
|
||||
}
|
||||
|
||||
internal enum class WarningsToReset {
|
||||
LOST_WALLET_ACCESS, LOST_PASSWORD_RESTORE
|
||||
internal data class WarningUM(
|
||||
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 isActiveBackupStatus: Boolean,
|
||||
val backupCardsCount: Int,
|
||||
val hasTangemPay: Boolean,
|
||||
)
|
||||
|
||||
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.model.Model
|
||||
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.ResetCardUseCase
|
||||
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.utils.coroutines.CoroutineDispatcherProvider
|
||||
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.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
|
@ -82,33 +86,22 @@ internal class ResetCardModel @Inject constructor(
|
|||
// TODO: move logic to separate domain entity
|
||||
private var resetBackupCardCount = 0
|
||||
|
||||
private var warningsMap = emptyMap<ResetCardScreenState.WarningType, ResetCardScreenState.WarningUM>()
|
||||
|
||||
val screenState: MutableStateFlow<ResetCardScreenState> = MutableStateFlow(
|
||||
value = getInitialState(),
|
||||
)
|
||||
|
||||
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(
|
||||
isResetButtonEnabled = false,
|
||||
descriptionText = getResetToFactoryDescription(
|
||||
isActiveBackupStatus = isActiveBackupPrimaryCard,
|
||||
typesResolver = currentCardTypesResolver,
|
||||
),
|
||||
warningsToShow = warningsToShow,
|
||||
isResetPasswordButtonShown = shouldShowResetPasswordButton,
|
||||
isAcceptCondition1Checked = false,
|
||||
isAcceptCondition2Checked = false,
|
||||
onAcceptCondition1ToggleClick = ::toggleFirstCondition,
|
||||
onAcceptCondition2ToggleClick = ::toggleSecondCondition,
|
||||
warningsToShow = buildInitialItems(),
|
||||
onResetButtonClick = { showDialog(ResetCardDialog.StartResetDialog) },
|
||||
onToggleWarning = ::toggleCondition,
|
||||
dialog = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -119,28 +112,46 @@ internal class ResetCardModel @Inject constructor(
|
|||
return isTangemWallet && isActiveBackupPrimaryCard
|
||||
}
|
||||
|
||||
private fun toggleFirstCondition(isAccepted: Boolean) {
|
||||
screenState.update { prevState ->
|
||||
val isResetButtonEnabled = if (prevState.isResetPasswordButtonShown) {
|
||||
isAccepted && prevState.isAcceptCondition2Checked
|
||||
} else {
|
||||
isAccepted
|
||||
}
|
||||
private fun buildInitialItems(): ImmutableList<ResetCardScreenState.WarningUM> {
|
||||
val shouldShowResetPasswordButton = shouldShowResetPasswordButton()
|
||||
val shouldShowResetTangemPayButton = params.hasTangemPay
|
||||
|
||||
prevState.copy(
|
||||
isAcceptCondition1Checked = isAccepted,
|
||||
isResetButtonEnabled = isResetButtonEnabled,
|
||||
)
|
||||
val lostWalletUM = ResetCardScreenState.WarningUM(
|
||||
isChecked = false,
|
||||
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 ->
|
||||
val isResetButtonEnabled = prevState.isAcceptCondition1Checked && isAccepted
|
||||
|
||||
warningsMap[type]?.let { current ->
|
||||
warningsMap = warningsMap + (type to current.copy(isChecked = !current.isChecked))
|
||||
}
|
||||
prevState.copy(
|
||||
isAcceptCondition2Checked = isAccepted,
|
||||
isResetButtonEnabled = isResetButtonEnabled,
|
||||
warningsToShow = warningsMap.values.toImmutableList(),
|
||||
isResetButtonEnabled = warningsMap.values.all { it.isChecked },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tap.network.auth
|
||||
|
||||
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
|
||||
|
||||
internal class DefaultP2PEthPoolAuthProvider(
|
||||
|
|
@ -12,6 +12,6 @@ internal class DefaultP2PEthPoolAuthProvider(
|
|||
val keys = environmentConfigStorage.getConfigSync().p2pApiKey
|
||||
?: 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
|
||||
}
|
||||
}
|
||||
|
|
@ -311,7 +311,7 @@ internal class ChildFactory @Inject constructor(
|
|||
params = StakingComponent.Params(
|
||||
userWalletId = route.userWalletId,
|
||||
cryptoCurrency = route.cryptoCurrency,
|
||||
yieldId = route.yieldId,
|
||||
integrationId = route.integrationId,
|
||||
),
|
||||
componentFactory = stakingComponentFactory,
|
||||
)
|
||||
|
|
@ -424,6 +424,7 @@ internal class ChildFactory @Inject constructor(
|
|||
cardId = route.cardId,
|
||||
isActiveBackupStatus = route.isActiveBackupStatus,
|
||||
backupCardsCount = route.backupCardsCount,
|
||||
hasTangemPay = route.hasTangemPay,
|
||||
),
|
||||
componentFactory = resetCardComponentFactory,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ dependencies {
|
|||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.staking)
|
||||
implementation(projects.domain.markets.models)
|
||||
implementation(projects.domain.onramp.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.bundle
|
||||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.feedback.models.WalletMetaInfo
|
||||
|
|
@ -112,6 +113,7 @@ sealed class AppRoute(val path: String) : Route {
|
|||
val cardId: String,
|
||||
val isActiveBackupStatus: Boolean,
|
||||
val backupCardsCount: Int,
|
||||
val hasTangemPay: Boolean,
|
||||
) : AppRoute(
|
||||
path = "/reset_to_factory" +
|
||||
"/${userWalletId.stringValue}" +
|
||||
|
|
@ -208,8 +210,8 @@ sealed class AppRoute(val path: String) : Route {
|
|||
data class Staking(
|
||||
val userWalletId: UserWalletId,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val yieldId: String,
|
||||
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/$yieldId")
|
||||
val integrationId: StakingIntegrationID,
|
||||
) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrency.id.value}/${integrationId.value}")
|
||||
|
||||
@Serializable
|
||||
data class PushNotification(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
|||
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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import com.tangem.domain.models.account.Account
|
|||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.quote.PriceChange
|
||||
import com.tangem.utils.converter.Converter
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import java.math.BigDecimal
|
||||
|
||||
class AccountCryptoPortfolioItemStateConverter(
|
||||
|
|
@ -41,14 +40,11 @@ class AccountCryptoPortfolioItemStateConverter(
|
|||
private fun Account.CryptoPortfolio.mapToContentState(
|
||||
fiatBalance: TotalFiatBalance.Loaded,
|
||||
): TokenItemState.Content {
|
||||
val subtitle2State = when (fiatBalance.amount.isZero()) {
|
||||
true -> null
|
||||
false -> priceChangeLce?.fold(
|
||||
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
|
||||
ifError = { null },
|
||||
ifContent = { priceChange -> priceChange.toSubtitle2State() },
|
||||
)
|
||||
}
|
||||
val subtitle2State = priceChangeLce?.fold(
|
||||
ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading },
|
||||
ifError = { null },
|
||||
ifContent = { priceChange -> priceChange.toSubtitle2State() },
|
||||
)
|
||||
return TokenItemState.Content(
|
||||
id = account.accountId.toItemId(),
|
||||
iconState = AccountIconItemStateConverter.convert(this),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.TextAutoSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.amountScreen.models.AmountState
|
||||
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.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.format.bigdecimal.crypto
|
||||
|
|
@ -66,11 +66,14 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis
|
|||
state = amountState.tokenIconState,
|
||||
iconSize = 40.dp,
|
||||
)
|
||||
ResizableText(
|
||||
Text(
|
||||
text = firstAmount,
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
autoSize = TextAutoSize.StepBased(
|
||||
maxFontSize = TangemTheme.typography.h2.fontSize,
|
||||
),
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.content.res.Configuration
|
|||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.TextAutoSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.amountScreen.models.AmountState
|
||||
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.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
|
|
@ -128,21 +128,23 @@ private fun AmountBlockV2(
|
|||
.padding(top = 8.dp)
|
||||
.weight(1f),
|
||||
) {
|
||||
ResizableText(
|
||||
Text(
|
||||
text = firstAmount,
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 1,
|
||||
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography.h2.fontSize),
|
||||
modifier = Modifier.testTag(BaseAmountBlockTestTags.PRIMARY_AMOUNT),
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
ResizableText(
|
||||
Text(
|
||||
text = secondAmount,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography.body2.fontSize),
|
||||
modifier = Modifier.testTag(BaseAmountBlockTestTags.SECONDARY_AMOUNT),
|
||||
)
|
||||
extraContent()
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ import com.tangem.domain.models.currency.yieldSupplyKey
|
|||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
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.lib.crypto.BlockchainUtils
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
|
|
@ -247,9 +248,9 @@ class TokenItemStateConverter(
|
|||
stakingApyMap = stakingApyMap,
|
||||
)
|
||||
val rewardTypeRes = when (stakingInfo.rewardType) {
|
||||
Yield.RewardType.APR -> R.string.staking_apr_earn_badge
|
||||
Yield.RewardType.UNKNOWN,
|
||||
Yield.RewardType.APY,
|
||||
RewardType.APR -> R.string.staking_apr_earn_badge
|
||||
RewardType.UNKNOWN,
|
||||
RewardType.APY,
|
||||
null,
|
||||
-> R.string.yield_module_earn_badge
|
||||
}
|
||||
|
|
@ -281,10 +282,11 @@ class TokenItemStateConverter(
|
|||
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
|
||||
|
||||
val rateInfo = when (val stakingOptions = stakingAvailability.option) {
|
||||
is StakingOption.P2P -> {
|
||||
// P2P or no balance: use preferred validators
|
||||
// TODO add p2p logic
|
||||
null
|
||||
is StakingOption.P2PEthPool -> {
|
||||
RewardInfo(
|
||||
rate = stakingOptions.apy,
|
||||
type = RewardType.APY,
|
||||
)
|
||||
}
|
||||
is StakingOption.StakeKit -> if (stakeKitBalance != null) {
|
||||
val validatorsByAddress = stakingOptions.yield.validators.associateBy { it.address }
|
||||
|
|
@ -455,7 +457,7 @@ class TokenItemStateConverter(
|
|||
private data class StakingLocalInfo(
|
||||
val rate: BigDecimal?,
|
||||
val isActive: Boolean,
|
||||
val rewardType: Yield.RewardType?,
|
||||
val rewardType: RewardType?,
|
||||
)
|
||||
|
||||
private data class EarnApyInfo(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.datasource.api.common.config
|
||||
|
||||
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.utils.ProviderSuspend
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ internal class P2PEthPool(
|
|||
private fun getInitialEnvironment(): ApiEnvironment {
|
||||
return when (BuildConfig.BUILD_TYPE) {
|
||||
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)
|
||||
enum class P2PEthPoolTxStatusDTO {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import com.squareup.moshi.Json
|
|||
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)
|
||||
data class P2PEthPoolErrorResponse(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.squareup.moshi.JsonClass
|
|||
/**
|
||||
* 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
|
||||
* {
|
||||
* "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.JsonClass
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* 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)
|
||||
enum class P2PEthPoolNetworkDTO {
|
||||
|
|
@ -33,15 +34,15 @@ data class P2PEthPoolVaultDTO(
|
|||
@Json(name = "displayName")
|
||||
val displayName: String,
|
||||
@Json(name = "apy")
|
||||
val apy: Double,
|
||||
val apy: BigDecimal,
|
||||
@Json(name = "baseApy")
|
||||
val baseApy: Double,
|
||||
val baseApy: BigDecimal,
|
||||
@Json(name = "capacity")
|
||||
val capacity: Double,
|
||||
val capacity: BigDecimal,
|
||||
@Json(name = "totalAssets")
|
||||
val totalAssets: Double,
|
||||
val totalAssets: BigDecimal,
|
||||
@Json(name = "feePercent")
|
||||
val feePercent: Double,
|
||||
val feePercent: BigDecimal,
|
||||
@Json(name = "isPrivate")
|
||||
val isPrivate: Boolean,
|
||||
@Json(name = "isGenesis")
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ internal object StakingStoreModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PBalancesPersistenceStore(
|
||||
fun provideP2PEthPoolBalancesPersistenceStore(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
|
|
@ -91,7 +91,7 @@ internal object StakingStoreModule {
|
|||
types = mapWithStringKeyTypes(valueTypes = setTypes<P2PEthPoolAccountResponse>()),
|
||||
defaultValue = emptyMap(),
|
||||
),
|
||||
produceFile = { context.dataStoreFile(fileName = "p2p_balances") },
|
||||
produceFile = { context.dataStoreFile(fileName = "p2p_eth_pool_balances") },
|
||||
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.Flow
|
|||
* (similar to StakingYieldsStore for StakeKit yields)
|
||||
*
|
||||
* 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 {
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ interface P2PEthPoolVaultsStore {
|
|||
suspend fun getSync(): List<P2PEthPoolVault>
|
||||
|
||||
/**
|
||||
* Store vaults from P2P API
|
||||
* Store vaults from P2PEthPool API
|
||||
*/
|
||||
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.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_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.P2PEthPoolAuthProvider
|
||||
import com.tangem.lib.auth.StakeKitAuthProvider
|
||||
|
|
@ -300,7 +300,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
}
|
||||
|
||||
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/"
|
||||
} else {
|
||||
ApiEnvironment.PROD to "https://api.p2p.org/"
|
||||
|
|
|
|||
|
|
@ -1083,6 +1083,7 @@
|
|||
<string name="reset_card_to_factory_button_title">Reset the card</string>
|
||||
<string name="reset_card_to_factory_condition_1">I understand that after performing this action, I will no longer have access to the current wallet</string>
|
||||
<string name="reset_card_to_factory_condition_2">I realize that I can\'t use this card to recover my access code on the other cards of the current wallet</string>
|
||||
<string name="reset_card_to_factory_condition_3">I understand that I will completely lose access to my Tangem Pay Card and all funds on it without the possibility of recovery</string>
|
||||
<string name="reset_card_with_backup_to_factory_message">Factory Reset will completely delete the wallet from the selected card or ring. You will not be able to restore the current wallet or use the card or ring to recover the access code.</string>
|
||||
<string name="reset_card_without_backup_to_factory_message">Factory Reset will completely delete the wallet from the selected card or ring and remove it from the app. You will not be able to restore the current wallet.</string>
|
||||
<string name="reset_cards_dialog_complete_description">All Tangem devices have been reset.</string>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -146,7 +145,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheet(
|
|||
sheetState = SheetState(
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
initialValue = Expanded,
|
||||
density = LocalDensity.current,
|
||||
positionalThreshold = { 0f },
|
||||
velocityThreshold = { 0f },
|
||||
),
|
||||
onBack = null,
|
||||
bsContent = {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
|
|
@ -141,7 +140,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheetW
|
|||
sheetState = SheetState(
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
initialValue = Expanded,
|
||||
density = LocalDensity.current,
|
||||
positionalThreshold = { 0f },
|
||||
velocityThreshold = { 0f },
|
||||
),
|
||||
onBack = null,
|
||||
containerColor = containerColor,
|
||||
|
|
|
|||
|
|
@ -134,7 +134,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
|
|||
sheetState = SheetState(
|
||||
skipPartiallyExpanded = skipPartiallyExpanded,
|
||||
initialValue = Expanded,
|
||||
density = LocalDensity.current,
|
||||
positionalThreshold = { 0f },
|
||||
velocityThreshold = { 0f },
|
||||
),
|
||||
onBack = null,
|
||||
containerColor = containerColor,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import androidx.compose.animation.fadeIn
|
|||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.TextAutoSize
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
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.sp
|
||||
import com.tangem.core.ui.components.ResizableText
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.BaseButtonTestTags
|
||||
import com.tangem.core.ui.utils.MultipleClickPreventer
|
||||
|
|
@ -75,7 +75,7 @@ fun TangemButton(
|
|||
)
|
||||
},
|
||||
text = {
|
||||
ResizableText(
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.heightIn(MinButtonContentSize, maxContentSize)
|
||||
|
|
@ -86,7 +86,10 @@ fun TangemButton(
|
|||
textAlign = TextAlign.Center,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
minFontSize = 12.sp,
|
||||
autoSize = TextAutoSize.StepBased(
|
||||
minFontSize = 12.sp,
|
||||
maxFontSize = textStyle.fontSize,
|
||||
),
|
||||
)
|
||||
},
|
||||
icon = { iconResId ->
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
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.res.TangemTheme
|
||||
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)
|
||||
|
|
@ -92,7 +94,7 @@ fun InputRowEnter(
|
|||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clip(CircleShape),
|
||||
text = description.resolveReference(),
|
||||
text = description,
|
||||
content = { contentModifier ->
|
||||
Icon(
|
||||
modifier = contentModifier.size(16.dp),
|
||||
|
|
@ -113,7 +115,8 @@ fun InputRowEnter(
|
|||
visualTransformation = visualTransformation,
|
||||
keyboardOptions = keyboardOptions,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8),
|
||||
.padding(top = TangemTheme.dimens.spacing8)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.NONCE_INPUT_TEXT_FIELD),
|
||||
)
|
||||
}
|
||||
iconRes?.let { iconRes ->
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.test.SendSelectNetworkFeeBottomSheetTestTags
|
||||
import com.tangem.core.ui.utils.rememberDecimalFormat
|
||||
|
||||
/**
|
||||
|
|
@ -112,6 +114,7 @@ fun InputRowEnterInfoAmount(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun InputRowEnterInfoAmountV2(
|
||||
title: TextReference,
|
||||
|
|
@ -139,20 +142,23 @@ fun InputRowEnterInfoAmountV2(
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
.padding(16.dp)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = titleColor,
|
||||
modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_TITLE),
|
||||
)
|
||||
if (description != null) {
|
||||
TangemTooltip(
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.clip(CircleShape),
|
||||
text = description.resolveReference(),
|
||||
.clip(CircleShape)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_TOOLTIP_ICON),
|
||||
text = description,
|
||||
content = { contentModifier ->
|
||||
Icon(
|
||||
modifier = contentModifier.size(16.dp),
|
||||
|
|
@ -183,7 +189,8 @@ fun InputRowEnterInfoAmountV2(
|
|||
backgroundColor = Color.Transparent,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8)
|
||||
.weight(1f),
|
||||
.weight(1f)
|
||||
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_INPUT_TEXT_FIELD),
|
||||
)
|
||||
info?.let { info ->
|
||||
Text(
|
||||
|
|
@ -192,7 +199,8 @@ fun InputRowEnterInfoAmountV2(
|
|||
color = infoColor,
|
||||
modifier = Modifier
|
||||
.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.res.TangemTheme
|
||||
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
|
||||
|
||||
@Composable
|
||||
|
|
@ -70,7 +70,7 @@ fun SelectorRowItem(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(paddingValues)
|
||||
.testTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM),
|
||||
.testTag(SwapSelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
|
|
|
|||
|
|
@ -239,19 +239,19 @@ private fun StandardBottomSheet(
|
|||
}
|
||||
val newTarget =
|
||||
when (val oldTarget = state.anchoredDraggableState.targetValue) {
|
||||
Hidden -> if (newAnchors.hasAnchorFor(Hidden)) Hidden else oldTarget
|
||||
Hidden -> if (newAnchors.hasPositionFor(Hidden)) Hidden else oldTarget
|
||||
PartiallyExpanded ->
|
||||
when {
|
||||
newAnchors.hasAnchorFor(PartiallyExpanded) -> PartiallyExpanded
|
||||
newAnchors.hasAnchorFor(Expanded) -> Expanded
|
||||
newAnchors.hasAnchorFor(Hidden) -> Hidden
|
||||
newAnchors.hasPositionFor(PartiallyExpanded) -> PartiallyExpanded
|
||||
newAnchors.hasPositionFor(Expanded) -> Expanded
|
||||
newAnchors.hasPositionFor(Hidden) -> Hidden
|
||||
else -> oldTarget
|
||||
}
|
||||
Expanded ->
|
||||
when {
|
||||
newAnchors.hasAnchorFor(Expanded) -> Expanded
|
||||
newAnchors.hasAnchorFor(PartiallyExpanded) -> PartiallyExpanded
|
||||
newAnchors.hasAnchorFor(Hidden) -> Hidden
|
||||
newAnchors.hasPositionFor(Expanded) -> Expanded
|
||||
newAnchors.hasPositionFor(PartiallyExpanded) -> PartiallyExpanded
|
||||
newAnchors.hasPositionFor(Hidden) -> Hidden
|
||||
else -> oldTarget
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,11 +102,11 @@ class TangemSheetState(
|
|||
|
||||
/** Whether the sheet has an expanded state defined. */
|
||||
val hasExpandedState: Boolean
|
||||
get() = anchoredDraggableState.anchors.hasAnchorFor(Expanded)
|
||||
get() = anchoredDraggableState.anchors.hasPositionFor(Expanded)
|
||||
|
||||
/** Whether the modal bottom sheet has a partially expanded state defined. */
|
||||
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
|
||||
|
|
@ -274,8 +274,8 @@ internal fun consumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
|
|||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
val toFling = available.toFloat()
|
||||
val currentOffset = sheetState.requireOffset()
|
||||
val minAnchor = sheetState.anchoredDraggableState.anchors.minAnchor()
|
||||
return if (toFling < 0 && currentOffset > minAnchor) {
|
||||
val minPosition = sheetState.anchoredDraggableState.anchors.minPosition()
|
||||
return if (toFling < 0 && currentOffset > minPosition) {
|
||||
onFling(toFling)
|
||||
// since we go to the anchor with tween settling, consume all for the best UX
|
||||
available
|
||||
|
|
|
|||
|
|
@ -1,29 +1,39 @@
|
|||
package com.tangem.core.ui.components.tooltip
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
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.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.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.TangemThemePreview
|
||||
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
|
||||
fun TangemTooltip(
|
||||
text: String,
|
||||
text: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
content: @Composable (Modifier) -> Unit,
|
||||
|
|
@ -33,30 +43,10 @@ fun TangemTooltip(
|
|||
enabled = enabled,
|
||||
tooltipContent = {
|
||||
Text(
|
||||
modifier = Modifier.background(TangemTheme.colors.icon.secondary),
|
||||
text = text,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary2,
|
||||
)
|
||||
},
|
||||
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,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.icon.secondary)
|
||||
.padding(horizontal = 6.dp, vertical = 8.dp),
|
||||
text = text.resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary2,
|
||||
)
|
||||
|
|
@ -75,16 +65,22 @@ private fun InternalTangemTooltip(
|
|||
) {
|
||||
val tooltipState = rememberTooltipState(isPersistent = true)
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val windowSize = LocalWindowSize.current.width
|
||||
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(spacingBetweenTooltipAndAnchor = 8.dp),
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
|
||||
positioning = TooltipAnchorPosition.Above,
|
||||
spacingBetweenTooltipAndAnchor = 8.dp,
|
||||
),
|
||||
state = tooltipState,
|
||||
modifier = modifier,
|
||||
tooltip = {
|
||||
PlainTooltip(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
caretSize = DpSize(width = 14.dp, height = 8.dp),
|
||||
modifier = Modifier.padding(end = 12.dp),
|
||||
shape = RoundedCornerShape(14.dp),
|
||||
caretShape = TooltipDefaults.caretShape(),
|
||||
maxWidth = windowSize - 24.dp,
|
||||
contentColor = TangemTheme.colors.text.primary2,
|
||||
containerColor = TangemTheme.colors.icon.secondary,
|
||||
content = { tooltipContent() },
|
||||
|
|
@ -103,24 +99,23 @@ private fun InternalTangemTooltip(
|
|||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
// region Preview
|
||||
@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() {
|
||||
TangemThemePreview {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(500.dp)
|
||||
.background(TangemTheme.colors.background.secondary),
|
||||
contentAlignment = Alignment.Center,
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.tertiary),
|
||||
) {
|
||||
TangemTooltip(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.size(64.dp),
|
||||
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed venenatis.",
|
||||
modifier = Modifier,
|
||||
text = stringReference("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed venenatis."),
|
||||
content = { contentModifier ->
|
||||
Icon(
|
||||
modifier = contentModifier.size(64.dp),
|
||||
modifier = contentModifier,
|
||||
painter = painterResource(R.drawable.ic_token_info_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.core.ui.res
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.compose.foundation.ComposeFoundationFlags
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
|
||||
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.valentinilk.shimmer.Shimmer
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun TangemTheme(
|
||||
activity: Activity,
|
||||
|
|
@ -36,6 +39,9 @@ fun TangemTheme(
|
|||
overrideSystemBarColors: Boolean = true,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
// TODO Research and implement in redesign [REDACTED_TASK_KEY]
|
||||
ComposeFoundationFlags.isPausableCompositionInPrefetchEnabled = false
|
||||
|
||||
val appThemeMode by uiDependencies.appThemeModeHolder.appThemeMode
|
||||
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"
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import com.tangem.domain.staking.model.StakingOption
|
|||
import com.tangem.domain.staking.model.ethpool.*
|
||||
import com.tangem.domain.staking.repositories.P2PEthPoolRepository
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import com.tangem.domain.staking.toggles.StakingFeatureToggles
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
|
|
@ -25,12 +26,13 @@ import kotlinx.coroutines.withContext
|
|||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* P2P staking repository implementation
|
||||
* P2PEthPool staking repository implementation
|
||||
*/
|
||||
internal class DefaultP2PEthPoolRepository(
|
||||
private val p2pApi: P2PEthPoolApi,
|
||||
private val p2pEthPoolApi: P2PEthPoolApi,
|
||||
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val stakingFeatureToggles: StakingFeatureToggles,
|
||||
) : P2PEthPoolRepository {
|
||||
|
||||
private val vaultConverter = P2PEthPoolVaultConverter
|
||||
|
|
@ -40,17 +42,21 @@ internal class DefaultP2PEthPoolRepository(
|
|||
private val errorConverter = P2PEthPoolErrorConverter
|
||||
|
||||
override suspend fun fetchVaults(network: P2PEthPoolNetwork) {
|
||||
val vaults = getVaults(network).getOrElse { error ->
|
||||
Timber.e("Error fetching P2P vaults: $error")
|
||||
val vaults = if (stakingFeatureToggles.isEthStakingEnabled) {
|
||||
getVaults(network).getOrElse { error ->
|
||||
Timber.e("Error fetching P2PEthPool vaults: $error")
|
||||
emptyList()
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
p2pEthPoolVaultsStore.store(vaults)
|
||||
|
||||
p2pEthPoolVaultsStore.store(vaults.filter { !it.isPrivate }) // TODO eth isSmoothingPool?
|
||||
}
|
||||
|
||||
override suspend fun getVaults(network: P2PEthPoolNetwork): Either<StakingError, List<P2PEthPoolVault>> = either {
|
||||
withContext(dispatchers.io) {
|
||||
val response = p2pApi.getVaults(network.value)
|
||||
when (response) {
|
||||
when (val response = p2pEthPoolApi.getVaults(network.value)) {
|
||||
is ApiResponse.Success -> {
|
||||
val data = response.data
|
||||
ensure(data.error == null) {
|
||||
|
|
@ -76,7 +82,7 @@ internal class DefaultP2PEthPoolRepository(
|
|||
vaultAddress = vaultAddress,
|
||||
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) {
|
||||
is ApiResponse.Success -> {
|
||||
val data = response.data
|
||||
|
|
@ -101,7 +107,7 @@ internal class DefaultP2PEthPoolRepository(
|
|||
stakerPublicKey = stakerPublicKey,
|
||||
stakeTransactionHash = stakeTransactionHash,
|
||||
)
|
||||
val response = p2pApi.createUnstakeTransaction(network.value, requestBody)
|
||||
val response = p2pEthPoolApi.createUnstakeTransaction(network.value, requestBody)
|
||||
when (response) {
|
||||
is ApiResponse.Success -> {
|
||||
val data = response.data
|
||||
|
|
@ -133,7 +139,7 @@ internal class DefaultP2PEthPoolRepository(
|
|||
): Either<StakingError, P2PEthPoolUnsignedTx> = either {
|
||||
withContext(dispatchers.io) {
|
||||
val requestBody = P2PEthPoolWithdrawRequest(stakerAddress = stakerAddress)
|
||||
val response = p2pApi.createWithdrawTransaction(network.value, requestBody)
|
||||
val response = p2pEthPoolApi.createWithdrawTransaction(network.value, requestBody)
|
||||
when (response) {
|
||||
is ApiResponse.Success -> {
|
||||
val data = response.data
|
||||
|
|
@ -154,7 +160,7 @@ internal class DefaultP2PEthPoolRepository(
|
|||
): Either<StakingError, P2PEthPoolBroadcastResult> = either {
|
||||
withContext(dispatchers.io) {
|
||||
val requestBody = P2PEthPoolBroadcastRequest(signedTransaction = signedTransaction)
|
||||
val response = p2pApi.broadcastTransaction(network.value, requestBody)
|
||||
val response = p2pEthPoolApi.broadcastTransaction(network.value, requestBody)
|
||||
when (response) {
|
||||
is ApiResponse.Success -> {
|
||||
val data = response.data
|
||||
|
|
@ -175,7 +181,7 @@ internal class DefaultP2PEthPoolRepository(
|
|||
vaultAddress: String,
|
||||
): Either<StakingError, P2PEthPoolAccount> = either {
|
||||
withContext(dispatchers.io) {
|
||||
val response = p2pApi.getAccountInfo(network.value, delegatorAddress, vaultAddress)
|
||||
val response = p2pEthPoolApi.getAccountInfo(network.value, delegatorAddress, vaultAddress)
|
||||
when (response) {
|
||||
is ApiResponse.Success -> {
|
||||
val data = response.data
|
||||
|
|
@ -197,7 +203,7 @@ internal class DefaultP2PEthPoolRepository(
|
|||
period: Int?,
|
||||
): Either<StakingError, List<P2PEthPoolReward>> = either {
|
||||
withContext(dispatchers.io) {
|
||||
val response = p2pApi.getRewards(
|
||||
val response = p2pEthPoolApi.getRewards(
|
||||
network = network.value,
|
||||
delegatorAddress = delegatorAddress,
|
||||
vaultAddress = vaultAddress,
|
||||
|
|
@ -217,6 +223,10 @@ internal class DefaultP2PEthPoolRepository(
|
|||
}
|
||||
}
|
||||
|
||||
override fun getVaultsFlow(): Flow<List<P2PEthPoolVault>> {
|
||||
return p2pEthPoolVaultsStore.get()
|
||||
}
|
||||
|
||||
override fun getStakingAvailability(): Flow<StakingAvailability> {
|
||||
return getVaultsFlow()
|
||||
.distinctUntilChanged()
|
||||
|
|
@ -224,7 +234,7 @@ internal class DefaultP2PEthPoolRepository(
|
|||
if (vaults.isEmpty()) {
|
||||
return@map StakingAvailability.TemporaryUnavailable
|
||||
} else {
|
||||
StakingAvailability.Available(StakingOption.P2P(vaults))
|
||||
StakingAvailability.Available(StakingOption.P2PEthPool(vaults))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -234,15 +244,11 @@ internal class DefaultP2PEthPoolRepository(
|
|||
return if (vaults.isEmpty()) {
|
||||
StakingAvailability.TemporaryUnavailable
|
||||
} else {
|
||||
StakingAvailability.Available(StakingOption.P2P(vaults))
|
||||
StakingAvailability.Available(StakingOption.P2PEthPool(vaults))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getVaultsSync(): List<P2PEthPoolVault> {
|
||||
return p2pEthPoolVaultsStore.getSync()
|
||||
}
|
||||
|
||||
private fun getVaultsFlow(): Flow<List<P2PEthPoolVault>> {
|
||||
return p2pEthPoolVaultsStore.get()
|
||||
}
|
||||
}
|
||||
|
|
@ -364,6 +364,7 @@ internal class DefaultStakeKitRepository(
|
|||
}
|
||||
|
||||
override fun getStakingAvailability(
|
||||
integrationId: StakingIntegrationID.StakeKit,
|
||||
rawCurrencyId: CryptoCurrency.RawID,
|
||||
symbol: String,
|
||||
): Flow<StakingAvailability> {
|
||||
|
|
@ -381,7 +382,7 @@ internal class DefaultStakeKitRepository(
|
|||
)
|
||||
|
||||
if (prefetchedYield != null) {
|
||||
StakingAvailability.Available(StakingOption.StakeKit(prefetchedYield))
|
||||
StakingAvailability.Available(StakingOption.StakeKit(integrationId, prefetchedYield))
|
||||
} else {
|
||||
StakingAvailability.TemporaryUnavailable
|
||||
}
|
||||
|
|
@ -389,6 +390,7 @@ internal class DefaultStakeKitRepository(
|
|||
}
|
||||
|
||||
override suspend fun getStakingAvailabilitySync(
|
||||
integrationId: StakingIntegrationID.StakeKit,
|
||||
rawCurrencyId: CryptoCurrency.RawID,
|
||||
symbol: String,
|
||||
): StakingAvailability {
|
||||
|
|
@ -404,7 +406,7 @@ internal class DefaultStakeKitRepository(
|
|||
)
|
||||
|
||||
return if (prefetchedYield != null) {
|
||||
StakingAvailability.Available(StakingOption.StakeKit(prefetchedYield))
|
||||
StakingAvailability.Available(StakingOption.StakeKit(integrationId, prefetchedYield))
|
||||
} else {
|
||||
StakingAvailability.TemporaryUnavailable
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,12 +29,12 @@ internal class DefaultStakingErrorResolver(
|
|||
is StakingError.DomainError -> {
|
||||
analyticsEventHandler.send(StakingAnalyticsEvent.DomainError(error))
|
||||
}
|
||||
// P2P errors
|
||||
// P2PEthPool errors
|
||||
is StakingError.InvalidAmount,
|
||||
is StakingError.DataError,
|
||||
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 availabilityFlow = when (stakingIntegration) {
|
||||
is StakingIntegrationID.P2P -> p2pEthPoolRepository.getStakingAvailability()
|
||||
StakingIntegrationID.P2PEthPool -> p2pEthPoolRepository.getStakingAvailability()
|
||||
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailability(
|
||||
stakingIntegration,
|
||||
rawCurrencyId,
|
||||
cryptoCurrency.symbol,
|
||||
)
|
||||
|
|
@ -94,8 +95,9 @@ internal class DefaultStakingRepository(
|
|||
?: return StakingAvailability.Unavailable
|
||||
|
||||
return when (stakingIntegration) {
|
||||
is StakingIntegrationID.P2P -> p2pEthPoolRepository.getStakingAvailabilitySync()
|
||||
StakingIntegrationID.P2PEthPool -> p2pEthPoolRepository.getStakingAvailabilitySync()
|
||||
is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailabilitySync(
|
||||
stakingIntegration,
|
||||
rawCurrencyId,
|
||||
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.ValidatorDTO.ValidatorStatusDTO
|
||||
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.Yield
|
||||
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")
|
||||
|
||||
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 commission = validatorDTO.commission
|
||||
// gross = net / (1 - commission)
|
||||
|
|
@ -162,17 +164,17 @@ internal object YieldConverter : Converter<YieldDTO, Yield> {
|
|||
} else {
|
||||
netApy
|
||||
}
|
||||
grossAprOrApy?.let { Yield.RewardInfo(rate = it, type = rewardType) }
|
||||
grossAprOrApy?.let { RewardInfo(rate = it, type = rewardType) }
|
||||
} 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) {
|
||||
YieldDTO.RewardTypeDTO.APY -> Yield.RewardType.APY
|
||||
YieldDTO.RewardTypeDTO.APR -> Yield.RewardType.APR
|
||||
else -> Yield.RewardType.UNKNOWN
|
||||
YieldDTO.RewardTypeDTO.APY -> RewardType.APY
|
||||
YieldDTO.RewardTypeDTO.APR -> RewardType.APR
|
||||
else -> RewardType.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import com.tangem.utils.converter.Converter
|
|||
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> {
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.tangem.utils.converter.Converter
|
|||
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> {
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.tangem.domain.staking.model.stakekit.StakingError
|
|||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from P2P Error Response to Domain StakingError
|
||||
* Converter from P2PEthPool Error Response to Domain StakingError
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
internal object P2PEthPoolErrorConverter : Converter<P2PEthPoolErrorResponse, StakingError> {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolReward
|
|||
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> {
|
||||
|
||||
|
|
|
|||
|
|
@ -8,17 +8,18 @@ import com.tangem.domain.models.StatusSource
|
|||
import com.tangem.domain.models.staking.*
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import kotlinx.datetime.Instant
|
||||
import java.math.BigDecimal
|
||||
|
||||
/** Converts P2P ETH Pool API response to [StakingBalance.Data.P2P] */
|
||||
internal object P2PStakingBalanceConverter {
|
||||
/** Converts P2PEthPool API response to [StakingBalance] */
|
||||
internal object P2PEthPoolStakingBalanceConverter {
|
||||
|
||||
fun convert(response: P2PEthPoolAccountResponse, source: StatusSource): StakingBalance.Data.P2P {
|
||||
fun convert(response: P2PEthPoolAccountResponse, source: StatusSource): StakingBalance {
|
||||
val stakingId = StakingID(
|
||||
integrationId = StakingIntegrationID.P2P.EthereumPooled.value,
|
||||
integrationId = StakingIntegrationID.P2PEthPool.value,
|
||||
address = response.delegatorAddress,
|
||||
)
|
||||
|
||||
val account = P2PStakingAccount(
|
||||
val account = P2PEthPoolStakingAccount(
|
||||
delegatorAddress = response.delegatorAddress,
|
||||
vaultAddress = response.vaultAddress,
|
||||
stake = convertStake(response.stake),
|
||||
|
|
@ -27,29 +28,40 @@ internal object P2PStakingBalanceConverter {
|
|||
exitQueue = convertExitQueue(response.exitQueue),
|
||||
)
|
||||
|
||||
return StakingBalance.Data.P2P(
|
||||
stakingId = stakingId,
|
||||
source = source,
|
||||
account = account,
|
||||
)
|
||||
val hasActivePosition = account.stake.assets > BigDecimal.ZERO ||
|
||||
account.exitQueue.total > BigDecimal.ZERO ||
|
||||
account.availableToWithdraw > BigDecimal.ZERO
|
||||
|
||||
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 {
|
||||
return P2PStake(
|
||||
private fun convertStake(dto: P2PEthPoolStakeDTO): P2PEthPoolStake {
|
||||
return P2PEthPoolStake(
|
||||
assets = dto.assets,
|
||||
totalEarnedAssets = dto.totalEarnedAssets,
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PExitQueue {
|
||||
return P2PExitQueue(
|
||||
private fun convertExitQueue(dto: P2PEthPoolExitQueueDTO): P2PEthPoolExitQueue {
|
||||
return P2PEthPoolExitQueue(
|
||||
total = dto.total.toBigDecimal(),
|
||||
requests = dto.requests.map(::convertExitRequest),
|
||||
)
|
||||
}
|
||||
|
||||
private fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PExitRequest {
|
||||
return P2PExitRequest(
|
||||
private fun convertExitRequest(dto: P2PEthPoolExitRequestDTO): P2PEthPoolExitRequest {
|
||||
return P2PEthPoolExitRequest(
|
||||
ticket = dto.ticket,
|
||||
totalAssets = dto.totalAssets.toBigDecimal(),
|
||||
timestamp = Instant.fromEpochSeconds(dto.timestamp),
|
||||
|
|
@ -6,7 +6,7 @@ import com.tangem.utils.converter.Converter
|
|||
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> {
|
||||
|
||||
|
|
|
|||
|
|
@ -3,21 +3,26 @@ package com.tangem.data.staking.converters.ethpool
|
|||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolVaultDTO
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
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> {
|
||||
|
||||
private val HUNDRED = BigDecimal(100)
|
||||
private const val DIVIDE_SCALE = 8
|
||||
|
||||
override fun convert(value: P2PEthPoolVaultDTO): P2PEthPoolVault {
|
||||
return P2PEthPoolVault(
|
||||
vaultAddress = value.vaultAddress,
|
||||
displayName = value.displayName,
|
||||
apy = value.apy.toBigDecimal(),
|
||||
baseApy = value.baseApy.toBigDecimal(),
|
||||
capacity = value.capacity.toBigDecimal(),
|
||||
totalAssets = value.totalAssets.toBigDecimal(),
|
||||
feePercent = value.feePercent.toBigDecimal(),
|
||||
apy = value.apy.divide(HUNDRED, DIVIDE_SCALE, RoundingMode.HALF_UP),
|
||||
baseApy = value.baseApy.divide(HUNDRED, DIVIDE_SCALE, RoundingMode.HALF_UP),
|
||||
capacity = value.capacity,
|
||||
totalAssets = value.totalAssets,
|
||||
feePercent = value.feePercent,
|
||||
isPrivate = value.isPrivate,
|
||||
isGenesis = value.isGenesis,
|
||||
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
|
||||
|
||||
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.P2PBalancesStore
|
||||
import com.tangem.data.staking.store.P2PEthPoolBalancesStore
|
||||
import com.tangem.data.staking.store.StakingBalancesStore
|
||||
import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse
|
||||
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
|
|
@ -38,11 +38,11 @@ internal object StakingBalanceSupplierModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PBalancesStore(
|
||||
fun provideP2PEthPoolBalancesStore(
|
||||
persistenceStore: DataStore<Map<String, Set<P2PEthPoolAccountResponse>>>,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): P2PBalancesStore {
|
||||
return DefaultP2PBalancesStore(
|
||||
): P2PEthPoolBalancesStore {
|
||||
return DefaultP2PEthPoolBalancesStore(
|
||||
runtimeStore = RuntimeSharedStore(),
|
||||
persistenceStore = persistenceStore,
|
||||
dispatchers = dispatchers,
|
||||
|
|
|
|||
|
|
@ -75,14 +75,16 @@ internal object StakingDataModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideP2PEthPoolRepository(
|
||||
p2pApi: P2PEthPoolApi,
|
||||
p2pEthPoolApi: P2PEthPoolApi,
|
||||
p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
stakingFeatureToggles: StakingFeatureToggles,
|
||||
): P2PEthPoolRepository {
|
||||
return DefaultP2PEthPoolRepository(
|
||||
p2pApi = p2pApi,
|
||||
p2pEthPoolApi = p2pEthPoolApi,
|
||||
p2pEthPoolVaultsStore = p2pEthPoolVaultsStore,
|
||||
dispatchers = dispatchers,
|
||||
stakingFeatureToggles = stakingFeatureToggles,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import arrow.core.left
|
|||
import arrow.core.right
|
||||
import arrow.core.toOption
|
||||
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.utils.YieldBalanceRequestBodyFactory
|
||||
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.isMultiCurrency
|
||||
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.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runSuspendCatching
|
||||
|
|
@ -38,16 +39,16 @@ import javax.inject.Inject
|
|||
/**
|
||||
* Default implementation of [MultiStakingBalanceFetcher]
|
||||
*
|
||||
* Supports both StakeKit and P2P staking providers.
|
||||
* Supports both StakeKit and P2PEthPool staking providers.
|
||||
*
|
||||
* @property userWalletsStore user wallets store
|
||||
* @property stakingYieldsStore staking yields store
|
||||
* @property stakingBalancesStore staking balances store (StakeKit)
|
||||
* @property p2pBalancesStore P2P balances store
|
||||
* @property stakeKitApi stake kit API
|
||||
* @property p2pApi P2P ETH Pool API
|
||||
* @property p2pVaultsStore P2P vaults store
|
||||
* @property dispatchers dispatchers
|
||||
* @property userWalletsStore user wallets store
|
||||
* @property stakingYieldsStore staking yields store
|
||||
* @property stakingBalancesStore staking balances store (StakeKit)
|
||||
* @property p2PEthPoolBalancesStore P2PEthPool balances store
|
||||
* @property stakeKitApi stake kit API
|
||||
* @property p2pEthPoolApi P2PEthPool API
|
||||
* @property p2pEthPoolVaultsStore P2PEthPool vaults store
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
|
@ -56,10 +57,10 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
private val userWalletsStore: UserWalletsStore,
|
||||
private val stakingYieldsStore: StakingYieldsStore,
|
||||
private val stakingBalancesStore: StakingBalancesStore,
|
||||
private val p2pBalancesStore: P2PBalancesStore,
|
||||
private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore,
|
||||
private val stakeKitApi: StakeKitApi,
|
||||
private val p2pApi: P2PEthPoolApi,
|
||||
private val p2pVaultsStore: P2PEthPoolVaultsStore,
|
||||
private val p2pEthPoolApi: P2PEthPoolApi,
|
||||
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MultiStakingBalanceFetcher {
|
||||
|
||||
|
|
@ -75,7 +76,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
return it.left()
|
||||
}
|
||||
|
||||
val (stakeKitIds, p2pIds) = stakingIds.partition { stakingId ->
|
||||
val (stakeKitIds, p2pEthPoolIds) = stakingIds.partition { stakingId ->
|
||||
val stakingIntegrationID = StakingIntegrationID.entries.find {
|
||||
it.value == stakingId.integrationId
|
||||
}
|
||||
|
|
@ -86,7 +87,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
"""
|
||||
Staking IDs to fetch:
|
||||
- StakeKit: ${stakeKitIds.joinToString()}
|
||||
- P2P: ${p2pIds.joinToString()}
|
||||
- P2PEthPool: ${p2pEthPoolIds.joinToString()}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
|
|
@ -96,8 +97,8 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
launch { fetchStakeKitBalances(params.userWalletId, stakeKitIds.toSet()) }
|
||||
}
|
||||
|
||||
if (p2pIds.isNotEmpty()) {
|
||||
launch { fetchP2PBalances(params.userWalletId, p2pIds.toSet()) }
|
||||
if (p2pEthPoolIds.isNotEmpty()) {
|
||||
launch { fetchP2PBalances(params.userWalletId, p2pEthPoolIds.toSet()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -110,8 +111,11 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
stakingIds = stakeKitIds.toSet(),
|
||||
)
|
||||
}
|
||||
if (p2pIds.isNotEmpty()) {
|
||||
p2pBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = p2pIds.toSet())
|
||||
if (p2pEthPoolIds.isNotEmpty()) {
|
||||
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>) {
|
||||
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()) {
|
||||
Timber.w("No P2P vaults available for $userWalletId")
|
||||
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
Timber.w("No P2PEthPool vaults available for $userWalletId, storing empty balances")
|
||||
p2PEthPoolBalancesStore.storeEmpty(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -143,59 +147,17 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
private suspend fun fetchFromP2P(
|
||||
userWalletId: UserWalletId,
|
||||
stakingIds: Set<StakingID>,
|
||||
vaults: List<com.tangem.domain.staking.model.ethpool.P2PEthPoolVault>,
|
||||
vaults: List<P2PEthPoolVault>,
|
||||
) {
|
||||
safeApiCall(
|
||||
call = {
|
||||
val addresses = stakingIds.map { it.address }.toSet()
|
||||
val responses = fetchP2PAccountResponses(vaults = vaults, addresses = addresses)
|
||||
|
||||
val responses = mutableSetOf<P2PEthPoolAccountResponse>()
|
||||
|
||||
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")
|
||||
Timber.i("Successfully fetched ${responses.size} P2PEthPool balances for $userWalletId")
|
||||
|
||||
if (responses.isNotEmpty()) {
|
||||
p2pBalancesStore.storeActual(userWalletId = userWalletId, values = responses)
|
||||
p2PEthPoolBalancesStore.storeActual(userWalletId = userWalletId, values = responses)
|
||||
|
||||
val missingStakingIds = stakingIds.filter { stakingId ->
|
||||
responses.none { response ->
|
||||
|
|
@ -205,23 +167,76 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor(
|
|||
|
||||
if (missingStakingIds.isNotEmpty()) {
|
||||
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 {
|
||||
Timber.i("No P2P responses received for $userWalletId")
|
||||
p2pBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
Timber.i("No P2PEthPool responses received for $userWalletId")
|
||||
p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds)
|
||||
}
|
||||
},
|
||||
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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
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) {
|
||||
val maybeUserWallet = userWalletsStore.getSyncOrNull(key = userWalletId).toOption()
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package com.tangem.data.staking.multi
|
|||
|
||||
import arrow.core.Option
|
||||
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.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.staking.multi.MultiStakingBalanceProducer
|
||||
|
|
@ -19,11 +19,11 @@ import kotlinx.coroutines.flow.onEmpty
|
|||
/**
|
||||
* 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 stakingBalancesStore StakeKit staking balances store
|
||||
* @property p2pBalancesStore P2P balances store
|
||||
* @property p2PEthPoolBalancesStore P2PEthPool balances store
|
||||
* @property dispatchers dispatchers
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.onEmpty
|
|||
internal class DefaultMultiStakingBalanceProducer @AssistedInject constructor(
|
||||
@Assisted val params: MultiStakingBalanceProducer.Params,
|
||||
private val stakingBalancesStore: StakingBalancesStore,
|
||||
private val p2pBalancesStore: P2PBalancesStore,
|
||||
private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : MultiStakingBalanceProducer {
|
||||
|
||||
|
|
@ -39,10 +39,10 @@ internal class DefaultMultiStakingBalanceProducer @AssistedInject constructor(
|
|||
|
||||
override fun produce(): Flow<Set<StakingBalance>> {
|
||||
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 ->
|
||||
stakeKitBalances + p2pBalances
|
||||
return combine(stakeKitFlow, p2pEthPoolFlow) { stakeKitBalances, p2pEthPoolBalances ->
|
||||
stakeKitBalances + p2pEthPoolBalances
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEmpty { emit(value = hashSetOf()) }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.data.staking.store
|
||||
|
||||
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.local.datastore.RuntimeSharedStore
|
||||
import com.tangem.domain.models.StatusSource
|
||||
|
|
@ -20,22 +20,22 @@ import kotlinx.coroutines.flow.map
|
|||
import kotlinx.coroutines.launch
|
||||
|
||||
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 persistenceStore persistence store
|
||||
* @param dispatchers coroutine dispatchers
|
||||
*/
|
||||
internal class DefaultP2PBalancesStore(
|
||||
internal class DefaultP2PEthPoolBalancesStore(
|
||||
private val runtimeStore: RuntimeSharedStore<WalletIdWithP2PStakingBalances>,
|
||||
private val persistenceStore: DataStore<WalletIdWithP2PResponses>,
|
||||
private val persistenceStore: DataStore<WalletIdWithP2PEthPoolResponses>,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
) : P2PBalancesStore {
|
||||
) : P2PEthPoolBalancesStore {
|
||||
|
||||
private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io)
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ internal class DefaultP2PBalancesStore(
|
|||
value = cachedData.map { (stringWalletId, responses) ->
|
||||
val key = UserWalletId(stringWalletId)
|
||||
val value = responses.map { response ->
|
||||
P2PStakingBalanceConverter.convert(
|
||||
P2PEthPoolStakingBalanceConverter.convert(
|
||||
response = response,
|
||||
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>) {
|
||||
updateInRuntime(
|
||||
userWalletId = userWalletId,
|
||||
|
|
@ -108,7 +117,7 @@ internal class DefaultP2PBalancesStore(
|
|||
|
||||
private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set<P2PEthPoolAccountResponse>) {
|
||||
val newBalances = values.map { response ->
|
||||
P2PStakingBalanceConverter.convert(
|
||||
P2PEthPoolStakingBalanceConverter.convert(
|
||||
response = response,
|
||||
source = StatusSource.ACTUAL,
|
||||
)
|
||||
|
|
@ -152,7 +161,7 @@ internal class DefaultP2PBalancesStore(
|
|||
current.toMutableMap().apply {
|
||||
this[userWalletId.stringValue] = this[userWalletId.stringValue].orEmpty()
|
||||
.filterNot { response ->
|
||||
StakingIntegrationID.P2P.EthereumPooled.value in integrationIds
|
||||
StakingIntegrationID.P2PEthPool.value in integrationIds
|
||||
}
|
||||
.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)
|
||||
}
|
||||
|
|
@ -7,9 +7,9 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
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>>
|
||||
|
||||
|
|
@ -23,6 +23,8 @@ interface P2PBalancesStore {
|
|||
|
||||
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 clear(userWalletId: UserWalletId, stakingIds: Set<StakingID>)
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
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.stakekit.models.response.model.YieldBalanceWrapperDTO
|
||||
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)!!
|
||||
}
|
||||
|
||||
internal fun P2PEthPoolAccountResponse.toDomain(source: StatusSource = StatusSource.CACHE): StakingBalance.Data.P2P {
|
||||
return P2PStakingBalanceConverter.convert(
|
||||
internal fun P2PEthPoolAccountResponse.toDomain(
|
||||
source: StatusSource = StatusSource.CACHE,
|
||||
): StakingBalance {
|
||||
return P2PEthPoolStakingBalanceConverter.convert(
|
||||
response = this,
|
||||
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.MockYieldDTOFactory
|
||||
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.utils.YieldBalanceRequestBodyFactory
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
|
|
@ -36,19 +36,19 @@ internal class DefaultMultiStakingBalanceFetcherTest {
|
|||
private val userWalletsStore: UserWalletsStore = mockk()
|
||||
private val stakingYieldsStore: StakingYieldsStore = mockk()
|
||||
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 p2pApi: P2PEthPoolApi = mockk()
|
||||
private val p2pVaultsStore: P2PEthPoolVaultsStore = mockk()
|
||||
private val p2pEthPoolApi: P2PEthPoolApi = mockk()
|
||||
private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore = mockk()
|
||||
|
||||
private val fetcher = DefaultMultiStakingBalanceFetcher(
|
||||
userWalletsStore = userWalletsStore,
|
||||
stakingYieldsStore = stakingYieldsStore,
|
||||
stakingBalancesStore = stakingBalancesStore,
|
||||
p2pBalancesStore = p2pBalancesStore,
|
||||
p2PEthPoolBalancesStore = p2PEthPoolBalancesStore,
|
||||
stakeKitApi = stakeKitApi,
|
||||
p2pApi = p2pApi,
|
||||
p2pVaultsStore = p2pVaultsStore,
|
||||
p2pEthPoolApi = p2pEthPoolApi,
|
||||
p2pEthPoolVaultsStore = p2pEthPoolVaultsStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package com.tangem.data.staking.multi
|
|||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory
|
||||
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.toDomain
|
||||
import com.tangem.domain.models.StatusSource
|
||||
|
|
@ -28,13 +28,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
|||
private val params = MultiStakingBalanceProducer.Params(userWalletId = UserWalletId("011"))
|
||||
|
||||
private val stakingBalancesStore = mockk<StakingBalancesStore>()
|
||||
private val p2pBalancesStore = mockk<P2PBalancesStore>()
|
||||
private val p2PEthPoolBalancesStore = mockk<P2PEthPoolBalancesStore>()
|
||||
private val dispatchers = TestingCoroutineDispatcherProvider()
|
||||
|
||||
private val producer = DefaultMultiStakingBalanceProducer(
|
||||
params = params,
|
||||
stakingBalancesStore = stakingBalancesStore,
|
||||
p2pBalancesStore = p2pBalancesStore,
|
||||
p2PEthPoolBalancesStore = p2PEthPoolBalancesStore,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
|
|
@ -48,13 +48,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
|||
val networksStatusesFlow = flowOf(balances)
|
||||
|
||||
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()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
||||
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
|
||||
|
||||
val values = getEmittedValues(flow = actual)
|
||||
|
||||
|
|
@ -67,13 +67,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
|||
val networksStatusesFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
|
||||
|
||||
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()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
||||
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
|
||||
|
||||
// first emit
|
||||
val balances = setOf(
|
||||
|
|
@ -108,13 +108,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
|||
val networksStatusesFlow = MutableSharedFlow<Set<StakingBalance>>(replay = 2)
|
||||
|
||||
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()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
||||
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
|
||||
|
||||
// first emit
|
||||
val wrappers = setOf(
|
||||
|
|
@ -157,13 +157,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
|||
.buffer(capacity = 5)
|
||||
|
||||
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()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
||||
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
|
||||
|
|
@ -181,13 +181,13 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
|||
@Test
|
||||
fun `test that flow is empty`() = runTest {
|
||||
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()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
||||
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
|
||||
|
||||
val values = getEmittedValues(flow = actual)
|
||||
|
||||
|
|
@ -198,53 +198,53 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
|||
@Test
|
||||
fun `test that StakeKit and P2P balances are combined`() = runTest {
|
||||
val stakeKitBalances = createStakeKitBalances()
|
||||
val p2pBalances = createP2PBalances()
|
||||
val p2pEthPoolBalances = createP2PEthPoolBalances()
|
||||
|
||||
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()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
||||
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
|
||||
|
||||
val values = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values.size).isEqualTo(1)
|
||||
Truth.assertThat(values.first()).isEqualTo(stakeKitBalances + p2pBalances)
|
||||
Truth.assertThat(values.first()).isEqualTo(stakeKitBalances + p2pEthPoolBalances)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test that P2P balances are updated independently from StakeKit`() = runTest {
|
||||
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 { p2pBalancesStore.get(params.userWalletId) } returns p2pFlow
|
||||
every { p2PEthPoolBalancesStore.get(params.userWalletId) } returns p2pEthPoolFlow
|
||||
|
||||
val actual = producer.produce()
|
||||
|
||||
// check after producer.produce()
|
||||
verify { stakingBalancesStore.get(params.userWalletId) }
|
||||
verify { p2pBalancesStore.get(params.userWalletId) }
|
||||
verify { p2PEthPoolBalancesStore.get(params.userWalletId) }
|
||||
|
||||
// first emit - empty P2P
|
||||
p2pFlow.emit(emptySet())
|
||||
// first emit - empty P2PEthPool
|
||||
p2pEthPoolFlow.emit(emptySet())
|
||||
|
||||
val values1 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values1.size).isEqualTo(1)
|
||||
Truth.assertThat(values1.first()).isEqualTo(stakeKitBalances)
|
||||
|
||||
// second emit - with P2P balance
|
||||
val p2pBalances = createP2PBalances()
|
||||
p2pFlow.emit(p2pBalances)
|
||||
// second emit - with P2PEthPool balance
|
||||
val p2pEthPoolBalances = createP2PEthPoolBalances()
|
||||
p2pEthPoolFlow.emit(p2pEthPoolBalances)
|
||||
|
||||
val values2 = getEmittedValues(flow = actual)
|
||||
|
||||
Truth.assertThat(values2.size).isEqualTo(2)
|
||||
Truth.assertThat(values2.last()).isEqualTo(stakeKitBalances + p2pBalances)
|
||||
Truth.assertThat(values2.last()).isEqualTo(stakeKitBalances + p2pEthPoolBalances)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
@ -255,7 +255,7 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
|||
address = "0x1",
|
||||
)
|
||||
val p2pEthereumId = StakingID(
|
||||
integrationId = StakingIntegrationID.P2P.EthereumPooled.value,
|
||||
integrationId = StakingIntegrationID.P2PEthPool.value,
|
||||
address = "0x5aa711F440Eb6d4361148bBD89d03464628ace84",
|
||||
)
|
||||
|
||||
|
|
@ -272,7 +272,7 @@ internal class DefaultMultiStakingBalanceProducerTest {
|
|||
)
|
||||
}
|
||||
|
||||
fun createP2PBalances(): Set<StakingBalance> {
|
||||
fun createP2PEthPoolBalances(): Set<StakingBalance> {
|
||||
return setOf(
|
||||
MockP2PEthPoolAccountResponseFactory.createWithBalance(stakingId = p2pEthereumId).toDomain(
|
||||
source = StatusSource.ACTUAL,
|
||||
|
|
|
|||
|
|
@ -4,31 +4,31 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
|
|||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/** P2P.org staking account */
|
||||
/** P2P.org eth pooled staking account */
|
||||
@Serializable
|
||||
data class P2PStakingAccount(
|
||||
data class P2PEthPoolStakingAccount(
|
||||
val delegatorAddress: String,
|
||||
val vaultAddress: String,
|
||||
val stake: P2PStake,
|
||||
val stake: P2PEthPoolStake,
|
||||
val availableToUnstake: SerializedBigDecimal,
|
||||
val availableToWithdraw: SerializedBigDecimal,
|
||||
val exitQueue: P2PExitQueue,
|
||||
val exitQueue: P2PEthPoolExitQueue,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class P2PStake(
|
||||
data class P2PEthPoolStake(
|
||||
val assets: SerializedBigDecimal,
|
||||
val totalEarnedAssets: SerializedBigDecimal,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class P2PExitQueue(
|
||||
data class P2PEthPoolExitQueue(
|
||||
val total: SerializedBigDecimal,
|
||||
val requests: List<P2PExitRequest>,
|
||||
val requests: List<P2PEthPoolExitRequest>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class P2PExitRequest(
|
||||
data class P2PEthPoolExitRequest(
|
||||
val ticket: String,
|
||||
val totalAssets: SerializedBigDecimal,
|
||||
val timestamp: Instant,
|
||||
|
|
@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
|
|||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Staking balance facade covering StakeKit and P2P balances
|
||||
* Staking balance facade covering StakeKit and P2PEthPool balances
|
||||
*/
|
||||
@Serializable
|
||||
sealed interface StakingBalance {
|
||||
|
|
@ -50,10 +50,10 @@ sealed interface StakingBalance {
|
|||
}
|
||||
|
||||
@Serializable
|
||||
data class P2P(
|
||||
data class P2PEthPool(
|
||||
override val stakingId: StakingID,
|
||||
override val source: StatusSource,
|
||||
val account: P2PStakingAccount,
|
||||
val account: P2PEthPoolStakingAccount,
|
||||
) : Data {
|
||||
|
||||
override val totalStaked: BigDecimal
|
||||
|
|
@ -93,7 +93,7 @@ sealed interface StakingBalance {
|
|||
fun copySealed(source: StatusSource): StakingBalance {
|
||||
return when (this) {
|
||||
is Data.StakeKit -> copy(source = source)
|
||||
is Data.P2P -> copy(source = source)
|
||||
is Data.P2PEthPool -> copy(source = source)
|
||||
is Empty -> copy(source = source)
|
||||
is Error -> this
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,4 +12,17 @@ data class YieldToken(
|
|||
val coinGeckoId: String?,
|
||||
val logoURI: String?,
|
||||
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.wallets.models)
|
||||
|
||||
implementation(projects.features.staking.api)
|
||||
|
||||
implementation(tangemDeps.blockchain) {
|
||||
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
|
||||
|
||||
/**
|
||||
* P2P.org account staking information
|
||||
* Account staking information
|
||||
* Contains detailed balance and exit queue information
|
||||
*/
|
||||
data class P2PEthPoolAccount(
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ data class P2PEthPoolAction(
|
|||
)
|
||||
|
||||
/**
|
||||
* Types of P2P staking actions
|
||||
* Types of P2PEthPool staking actions
|
||||
*/
|
||||
@Serializable
|
||||
enum class P2PEthPoolActionType {
|
||||
|
|
@ -33,7 +33,7 @@ enum class P2PEthPoolActionType {
|
|||
}
|
||||
|
||||
/**
|
||||
* Status of P2P staking action
|
||||
* Status of P2PEthPool staking action
|
||||
*/
|
||||
@Serializable
|
||||
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(
|
||||
val id: String?,
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
|
|||
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
|
||||
*/
|
||||
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(
|
||||
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 {
|
||||
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
|
||||
* @return true if supported, false otherwise
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.domain.models.serialization.SerializedBigDecimal
|
|||
import org.joda.time.DateTime
|
||||
|
||||
/**
|
||||
* P2P.org rewards history entry
|
||||
* Rewards history entry
|
||||
* Historical reward information for account
|
||||
*/
|
||||
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(
|
||||
val vaultAddress: String,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
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.
|
||||
*/
|
||||
object P2PStakingConfig {
|
||||
object P2PEthPoolStakingConfig {
|
||||
|
||||
const val USE_TESTNET: Boolean = true
|
||||
|
||||
|
|
@ -3,7 +3,7 @@ package com.tangem.domain.staking.model.ethpool
|
|||
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
|
||||
*/
|
||||
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.staking.YieldToken
|
||||
import com.tangem.domain.staking.model.common.RewardInfo
|
||||
import com.tangem.domain.staking.model.common.RewardType
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
|
@ -138,18 +140,6 @@ data class Yield(
|
|||
UNKNOWN,
|
||||
}
|
||||
}
|
||||
|
||||
enum class RewardType {
|
||||
APY, // compound rate
|
||||
APR, // simple rate
|
||||
UNKNOWN,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class RewardInfo(
|
||||
val rate: SerializedBigDecimal,
|
||||
val type: RewardType,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
|
|
|
|||
|
|
@ -12,11 +12,11 @@ import kotlinx.coroutines.launch
|
|||
|
||||
/**
|
||||
* 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(
|
||||
private val stakeKitRepository: StakeKitRepository,
|
||||
private val p2pRepository: P2PEthPoolRepository,
|
||||
private val p2pEthPoolRepository: P2PEthPoolRepository,
|
||||
private val stakingErrorResolver: StakingErrorResolver,
|
||||
) {
|
||||
suspend operator fun invoke(): Either<StakingError, Unit> {
|
||||
|
|
@ -25,7 +25,7 @@ class FetchStakingOptionsUseCase(
|
|||
block = {
|
||||
coroutineScope {
|
||||
launch { stakeKitRepository.fetchYields() }
|
||||
launch { p2pRepository.fetchVaults() }
|
||||
launch { p2pEthPoolRepository.fetchVaults() }
|
||||
}
|
||||
},
|
||||
catch = { stakingErrorResolver.resolve(it) },
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class GetStakingEntryInfoUseCase(
|
|||
symbol = symbol,
|
||||
)
|
||||
}
|
||||
is StakingOption.P2P -> {
|
||||
is StakingOption.P2PEthPool -> {
|
||||
StakingEntryInfo(
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
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 java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Strategy interface for staking integrations.
|
||||
* Abstracts over StakeKit and P2PEthPool staking providers.
|
||||
*/
|
||||
interface StakingIntegration {
|
||||
|
||||
// Basic
|
||||
|
||||
val integrationId: StakingIntegrationID
|
||||
|
||||
val token: YieldToken
|
||||
|
||||
val tokens: List<YieldToken>
|
||||
|
||||
// Targets (validators or vaults)
|
||||
|
||||
val targets: List<StakingTarget>
|
||||
|
||||
val preferredTargets: List<StakingTarget>
|
||||
|
||||
val areAllTargetsFull: Boolean
|
||||
|
||||
// Enter/Exit Args
|
||||
|
||||
val isPartialAmountDisabled: Boolean
|
||||
|
||||
val enterMinimumAmount: BigDecimal?
|
||||
|
||||
val exitMinimumAmount: BigDecimal?
|
||||
|
||||
val enterArgs: StakingActionArgs?
|
||||
|
||||
val exitArgs: StakingActionArgs?
|
||||
|
||||
// Metadata
|
||||
|
||||
val warmupPeriodDays: Int
|
||||
|
||||
val cooldownPeriodDays: Int?
|
||||
|
||||
val rewardSchedule: RewardSchedule
|
||||
|
||||
val rewardClaiming: RewardClaiming
|
||||
|
||||
// Basic
|
||||
|
||||
fun getCurrentToken(rawCurrencyId: CryptoCurrency.RawID?): YieldToken
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@ import com.tangem.blockchainsdk.utils.toBlockchain
|
|||
import com.tangem.blockchainsdk.utils.toMigratedCoinId
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||
|
||||
/**
|
||||
* Represents a staking integration identifier.
|
||||
|
|
@ -97,16 +97,14 @@ sealed interface StakingIntegrationID {
|
|||
}
|
||||
|
||||
/**
|
||||
* Represents P2P staking integrations
|
||||
* Represents P2PEthPool staking integration
|
||||
*/
|
||||
enum class P2P : StakingIntegrationID {
|
||||
EthereumPooled {
|
||||
override val value: String = "p2p-ethereum-pooled"
|
||||
override val blockchain: Blockchain
|
||||
get() = if (P2PStakingConfig.USE_TESTNET) Blockchain.EthereumTestnet else Blockchain.Ethereum
|
||||
override val networkId: String
|
||||
get() = P2PStakingConfig.activeNetwork.stakingNetworkId
|
||||
},
|
||||
object P2PEthPool : StakingIntegrationID {
|
||||
override val value: String = "p2p-ethereum-pooled"
|
||||
override val blockchain: Blockchain
|
||||
get() = if (P2PEthPoolStakingConfig.USE_TESTNET) Blockchain.EthereumTestnet else Blockchain.Ethereum
|
||||
override val networkId: String
|
||||
get() = P2PEthPoolStakingConfig.activeNetwork.stakingNetworkId
|
||||
}
|
||||
|
||||
// Polkadot {
|
||||
|
|
@ -138,7 +136,7 @@ sealed interface StakingIntegrationID {
|
|||
|
||||
/** List of all native staking integration IDs */
|
||||
val entries: List<StakingIntegrationID> by lazy {
|
||||
StakeKit.Coin.entries + StakeKit.EthereumToken.entries + P2P.entries
|
||||
StakeKit.Coin.entries + StakeKit.EthereumToken.entries + listOf(P2PEthPool)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -152,9 +150,12 @@ sealed interface StakingIntegrationID {
|
|||
val blockchain = Blockchain.fromId(id = currencyId.rawNetworkId)
|
||||
|
||||
return if (currencyId.contractAddress.isNullOrBlank()) {
|
||||
// Order is not important — either P2P or Stakekit.Coin can be in any order
|
||||
P2P.entries.firstOrNull { it.blockchain == blockchain }
|
||||
?: StakeKit.Coin.entries.firstOrNull { it.blockchain == blockchain }
|
||||
// Order is not important — either P2PEthPool or Stakekit.Coin can be in any order
|
||||
if (P2PEthPool.blockchain == blockchain) {
|
||||
P2PEthPool
|
||||
} else {
|
||||
StakeKit.Coin.entries.firstOrNull { it.blockchain == blockchain }
|
||||
}
|
||||
} else {
|
||||
StakeKit.EthereumToken.entries.firstOrNull { token ->
|
||||
token.blockchain == blockchain &&
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
package com.tangem.domain.staking.model
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.staking.NetworkType
|
||||
import com.tangem.domain.models.staking.YieldToken
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
|
||||
/**
|
||||
* Represents a staking option from any provider
|
||||
* Unified abstraction over StakeKit and P2P staking integrations
|
||||
* Unified abstraction over StakeKit and P2PEthPool staking integrations
|
||||
*/
|
||||
sealed interface StakingOption {
|
||||
|
||||
/** Unique identifier for the staking option */
|
||||
val integrationId: String
|
||||
val integrationId: StakingIntegrationID
|
||||
|
||||
/** Annual Percentage Yield */
|
||||
val apy: SerializedBigDecimal
|
||||
|
|
@ -28,34 +27,23 @@ sealed interface StakingOption {
|
|||
* StakeKit staking option
|
||||
* Wraps StakeKit Yield with all validator and metadata information
|
||||
*/
|
||||
data class StakeKit(val yield: Yield) : StakingOption {
|
||||
override val integrationId: String = yield.id
|
||||
data class StakeKit(
|
||||
override val integrationId: StakingIntegrationID.StakeKit,
|
||||
val yield: Yield,
|
||||
) : StakingOption {
|
||||
override val apy: SerializedBigDecimal = yield.apy
|
||||
override val token: YieldToken = yield.token
|
||||
override val isAvailable: Boolean = yield.isAvailable
|
||||
}
|
||||
|
||||
/**
|
||||
* P2P pooled staking option
|
||||
* Wraps P2P ETH Pool vault information
|
||||
* P2PEthPool staking option
|
||||
* Wraps P2PEthPool vault information
|
||||
*/
|
||||
data class P2P(val vaults: List<P2PEthPoolVault>) : StakingOption {
|
||||
override val integrationId: String = "p2p-ethereum-pooled"
|
||||
data class P2PEthPool(val vaults: List<P2PEthPoolVault>) : StakingOption {
|
||||
override val integrationId: StakingIntegrationID = StakingIntegrationID.P2PEthPool
|
||||
override val apy: SerializedBigDecimal = vaults.maxOf { it.apy }
|
||||
override val token: YieldToken = createEthToken()
|
||||
override val token: YieldToken = YieldToken.ETH
|
||||
override val isAvailable: Boolean = vaults.isNotEmpty()
|
||||
|
||||
private fun createEthToken(): YieldToken { // TODO
|
||||
return YieldToken(
|
||||
name = "Ethereum",
|
||||
network = NetworkType.ETHEREUM,
|
||||
symbol = "ETH",
|
||||
decimals = 18,
|
||||
address = null, // Native token
|
||||
coinGeckoId = "ethereum",
|
||||
logoURI = null,
|
||||
isPoints = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolNetwork
|
|||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolReward
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault
|
||||
import com.tangem.domain.staking.model.ethpool.P2PStakingConfig
|
||||
import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig
|
||||
import com.tangem.domain.staking.model.stakekit.StakingError
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
|
@ -17,24 +17,24 @@ interface P2PEthPoolRepository {
|
|||
/**
|
||||
* Fetch and store available staking vaults
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
*/
|
||||
suspend fun fetchVaults(network: P2PEthPoolNetwork = P2PStakingConfig.activeNetwork)
|
||||
suspend fun fetchVaults(network: P2PEthPoolNetwork = P2PEthPoolStakingConfig.activeNetwork)
|
||||
|
||||
/**
|
||||
* Get list of available staking vaults
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
* @return Either error or list of vaults with APY, capacity, fees
|
||||
*/
|
||||
suspend fun getVaults(
|
||||
network: P2PEthPoolNetwork = P2PStakingConfig.activeNetwork,
|
||||
network: P2PEthPoolNetwork = P2PEthPoolStakingConfig.activeNetwork,
|
||||
): Either<StakingError, List<P2PEthPoolVault>>
|
||||
|
||||
/**
|
||||
* Create unsigned transaction for depositing ETH into a vault
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
* @param delegatorAddress User's wallet address
|
||||
* @param vaultAddress Vault contract address
|
||||
* @param amount Amount of ETH to deposit
|
||||
|
|
@ -53,7 +53,7 @@ interface P2PEthPoolRepository {
|
|||
* Unstaking adds funds to exit queue. After ~1-4 days, use [createWithdrawTransaction]
|
||||
* to withdraw the funds.
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
* @param stakerPublicKey Staker's public key (note: API doc may have Bitcoin terminology)
|
||||
* @param stakeTransactionHash Original stake transaction hash
|
||||
* @return Either error or unsigned transaction
|
||||
|
|
@ -69,7 +69,7 @@ interface P2PEthPoolRepository {
|
|||
*
|
||||
* Only works when funds are available (after exit queue wait period).
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
* @param stakerAddress User's wallet address
|
||||
* @return Either error or unsigned transaction with withdrawal tickets
|
||||
*/
|
||||
|
|
@ -81,7 +81,7 @@ interface P2PEthPoolRepository {
|
|||
/**
|
||||
* Broadcast signed transaction to blockchain
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
* @param signedTransaction Signed transaction in hex format (with 0x prefix)
|
||||
* @return Either error or broadcast result with transaction hash
|
||||
*/
|
||||
|
|
@ -95,7 +95,7 @@ interface P2PEthPoolRepository {
|
|||
*
|
||||
* Returns current stake, rewards, exit queue status, and available amounts
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
* @param delegatorAddress User's wallet address
|
||||
* @param vaultAddress Vault contract address
|
||||
* @return Either error or account info
|
||||
|
|
@ -109,7 +109,7 @@ interface P2PEthPoolRepository {
|
|||
/**
|
||||
* Get rewards history for account and vault
|
||||
*
|
||||
* @param network P2P network (MAINNET or TESTNET)
|
||||
* @param network P2PEthPool network (MAINNET or TESTNET)
|
||||
* @param delegatorAddress User's wallet address
|
||||
* @param vaultAddress Vault contract address
|
||||
* @param period Optional period filter in days (30, 60, or 90)
|
||||
|
|
@ -123,17 +123,27 @@ interface P2PEthPoolRepository {
|
|||
): Either<StakingError, List<P2PEthPoolReward>>
|
||||
|
||||
/**
|
||||
* Check P2P staking availability by finding public vault
|
||||
* Get flow of cached vaults.
|
||||
*
|
||||
* @return Flow of StakingAvailability - Available with StakingOption.P2P if public vault found,
|
||||
* This returns vaults from the local cache/store.
|
||||
* Call [fetchVaults] first to populate the cache from the network.
|
||||
*
|
||||
* @return Flow of cached vaults list
|
||||
*/
|
||||
fun getVaultsFlow(): Flow<List<P2PEthPoolVault>>
|
||||
|
||||
/**
|
||||
* Check P2PEthPool staking availability by finding public vault
|
||||
*
|
||||
* @return Flow of StakingAvailability - Available with StakingOption.P2PEthPool if public vault found,
|
||||
* TemporaryUnavailable if not found or vaults empty
|
||||
*/
|
||||
fun getStakingAvailability(): Flow<StakingAvailability>
|
||||
|
||||
/**
|
||||
* Check P2P staking availability synchronously
|
||||
* Check P2PEthPool staking availability synchronously
|
||||
*
|
||||
* @return StakingAvailability - Available with StakingOption.P2P if public vault found,
|
||||
* @return StakingAvailability - Available with StakingOption.P2PEthPool if public vault found,
|
||||
* TemporaryUnavailable if not found or vaults empty
|
||||
*/
|
||||
suspend fun getStakingAvailabilitySync(): StakingAvailability
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.domain.models.network.Network
|
|||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.domain.staking.model.StakingEntryInfo
|
||||
import com.tangem.domain.staking.model.StakingIntegrationID
|
||||
import com.tangem.domain.models.staking.NetworkType
|
||||
import com.tangem.domain.staking.model.stakekit.Yield
|
||||
import com.tangem.domain.staking.model.stakekit.action.StakingAction
|
||||
|
|
@ -24,9 +25,17 @@ interface StakeKitRepository {
|
|||
|
||||
fun getEnabledYields(): Flow<List<Yield>>
|
||||
|
||||
fun getStakingAvailability(rawCurrencyId: CryptoCurrency.RawID, symbol: String): Flow<StakingAvailability>
|
||||
fun getStakingAvailability(
|
||||
integrationId: StakingIntegrationID.StakeKit,
|
||||
rawCurrencyId: CryptoCurrency.RawID,
|
||||
symbol: String,
|
||||
): Flow<StakingAvailability>
|
||||
|
||||
suspend fun getStakingAvailabilitySync(rawCurrencyId: CryptoCurrency.RawID, symbol: String): StakingAvailability
|
||||
suspend fun getStakingAvailabilitySync(
|
||||
integrationId: StakingIntegrationID.StakeKit,
|
||||
rawCurrencyId: CryptoCurrency.RawID,
|
||||
symbol: String,
|
||||
): StakingAvailability
|
||||
|
||||
suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import java.math.BigDecimal
|
|||
|
||||
/**
|
||||
* Provider-agnostic extension to get total balance including rewards.
|
||||
* Works for both StakeKit and P2P providers.
|
||||
* Works for both StakeKit and P2PEthPool providers.
|
||||
*
|
||||
* Returns sum of all staking-related balances including rewards
|
||||
* (staked + unstaking + withdrawable + rewards).
|
||||
|
|
@ -18,7 +18,7 @@ import java.math.BigDecimal
|
|||
fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String): BigDecimal {
|
||||
return when (this) {
|
||||
is StakingBalance.Data.StakeKit -> getTotalWithRewardsStakingBalanceStakeKit(blockchainId)
|
||||
is StakingBalance.Data.P2P -> {
|
||||
is StakingBalance.Data.P2PEthPool -> {
|
||||
val rewards = totalRewards
|
||||
if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId)) {
|
||||
totalStaked + unstakingAmount + withdrawableAmount + rewards
|
||||
|
|
@ -31,7 +31,7 @@ fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String):
|
|||
|
||||
/**
|
||||
* Provider-agnostic extension to get total staking balance excluding rewards.
|
||||
* Works for both StakeKit and P2P providers.
|
||||
* Works for both StakeKit and P2PEthPool providers.
|
||||
*
|
||||
* Returns sum of all staking-related balances (staked + unstaking + withdrawable)
|
||||
* excluding rewards.
|
||||
|
|
@ -39,7 +39,7 @@ fun StakingBalance.Data.getTotalWithRewardsStakingBalance(blockchainId: String):
|
|||
fun StakingBalance.Data.getTotalStakingBalance(blockchainId: String): BigDecimal {
|
||||
return when (this) {
|
||||
is StakingBalance.Data.StakeKit -> getTotalStakingBalanceStakeKit(blockchainId)
|
||||
is StakingBalance.Data.P2P -> totalStaked + unstakingAmount + withdrawableAmount
|
||||
is StakingBalance.Data.P2PEthPool -> totalStaked + unstakingAmount + withdrawableAmount
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
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