From 48a6276e1272aea339205903b3fc0066b4f80fb4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 27 Jun 2026 22:25:05 +0300 Subject: [PATCH 1/5] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index ef9bcda031..0f32ca5313 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.39-1575" +tangemBlockchainSdk = "releases-5.39-1589" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.39-623" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From ca91ddb27fe058df75a4fde6c434fb11b4ef89db Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Jun 2026 13:36:46 +0300 Subject: [PATCH 2/5] Updated on 2026-08-14 --- .../network/solana/WcSolanaSignAndSendTransactionUseCase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAndSendTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAndSendTransactionUseCase.kt index 897db70e35..362c98fe80 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAndSendTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignAndSendTransactionUseCase.kt @@ -57,7 +57,7 @@ internal class WcSolanaSignAndSendTransactionUseCase @AssistedInject constructor val formattedHash = getFormattedHash(hash) // uses for flow sendLargeSolanaTransaction if (context.session.wallet is UserWallet.Cold && isLargeHash(formattedHash)) { // workaround for large transactions that cannot be signed directly by card - TangemLogger.w("The transaction hash is too large to be signed directly: ${formattedHash.size} bytes") + TangemLogger.i("The transaction hash is too large to be signed directly: ${formattedHash.size} bytes") sendLargeSolanaTransactionUseCase(context.session.wallet as UserWallet.Cold, context.network, formattedHash) .fold( ifLeft = { error -> From f642bd1e28b59ee88c2b2bcedaa2ef65393a238a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Jun 2026 13:01:36 +0300 Subject: [PATCH 3/5] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 3 ++- core/res/src/main/res/values-fr/strings.xml | 1 + core/res/src/main/res/values-ja/strings.xml | 1 + core/res/src/main/res/values-zh-rCN/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 14 ++++++++++++++ 5 files changed, 19 insertions(+), 1 deletion(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index f4e0b41e2d..e37cc25476 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -566,7 +566,7 @@ Dynamische Adressen deaktivieren Dynamische Adressen deaktivieren Dynamische Adressen deaktiviert - Einige Adressen fehlen + Dynamische Adressen aktiviert Verwenden Sie für jede Transaktion eine neue Adresse, um die Rückverfolgbarkeit zu verringern und den Datenschutz in der Kette zu verbessern. Verbesserter Datenschutz Einfacher Geldempfang in UTXO-basierten Netzwerken mit automatischer Adressgenerierung - keine manuelle Adressverwaltung erforderlich. @@ -1550,6 +1550,7 @@ Staking aktiviert Zurzeit sind keine Validierer verfügbar. Bitte versuche es später noch einmal. Staking nicht verfügbar + Staking ist in Ihrer Region nicht verfügbar. Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Du die Verwendung Deines Tokens für das Staking genehmigst. Indem Du die Staking-Funktionalität nutzt, stimmst Du den %1$s und %2$s des Anbieters zu. Gesperrt diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index ec381bcad1..c1d9e544ef 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1421,6 +1421,7 @@ Staking activé Aucun validateur disponible pour le moment. Veuillez réessayer plus tard. Staking indisponible + Le staking est indisponible dans votre région Le réseau facturera des frais d’approbation de jeton pour vérifier que vous autorisez l’utilisation de votre jeton pour le jalonnement. En utilisant la fonctionnalité de staking, vous acceptez les %1$s et %2$s du fournisseur Bloqué diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index b851b50576..1a9a334e93 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -1520,6 +1520,7 @@ ステーキングが有効です 現在、利用可能なバリデーターは見つかりません。しばらくしてからもう一度お試しください。 ステーキングは利用できません + お住まいの地域ではステーキングをご利用いただけません ネットワークは、ステーキングのためにトークンの使用を承認していることを確認するために、トークン承認手数料を請求します。 ステーキング機能を使用すると、プロバイダーの%1$sと%2$sに同意したことになります ロック中 diff --git a/core/res/src/main/res/values-zh-rCN/strings.xml b/core/res/src/main/res/values-zh-rCN/strings.xml index d74f43edfc..e1ea9ae35f 100644 --- a/core/res/src/main/res/values-zh-rCN/strings.xml +++ b/core/res/src/main/res/values-zh-rCN/strings.xml @@ -1512,6 +1512,7 @@ 已启用质押 目前没有可用的验证节点。请稍后再试。 质押功能不可用 + 您所在的地区暂不支持质押功能 网络将收取代币批准费,以验证您是否授权使用您的代币进行质押。 使用质押功能即表示您同意提供商的 %1$s 和 %2$s 已锁定 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index e644cefa16..0f66cd5146 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -115,10 +115,17 @@ Enter address Invalid address Keep editing + You can not create more than 20 addresses. Delete one to add new. + Can\'t add new address + Contact name is required + Contact name contains invalid characters + Contact name must not exceed 50 characters + That name is already taken on this wallet New contact No contacts yet Contacts added will appear here Remove address + Save to Wallet This contact will be linked to this wallet’s address book. Select network Address book @@ -352,6 +359,7 @@ Get token Go to provider Go to token + Go to verification Got it Hide Hold to %s @@ -410,6 +418,7 @@ Privacy Policy %1$s-%2$s %1$s — %2$s + Rate Read more Receive Received @@ -701,6 +710,8 @@ Tangem feedback Can\'t send a transaction Coin description error + Review portfolio and explore earn opportunities + For You Update now Update the app to its latest version to ensure proper functionality Update needed @@ -732,6 +743,8 @@ Key Generation All cryptographic operations happen inside the secure chip, certified against cloning and physical tampering. Hardware-Level Security + Network activity is high. You can continue now or try again later when fees may be lower. + Network fee is higher than usual Add Existing Wallet Create New Wallet Order Tangem @@ -1402,6 +1415,7 @@ Memo Check your network connection Network fee info unreachable + from %1$s in %2$s You send From %s Gas limit From de3cdf0c474cb18a367990f14681ccd86442b29d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 30 Jun 2026 15:21:16 +0200 Subject: [PATCH 4/5] Updated on 2026-08-14 --- .../datasource/api/ethpool/P2PEthPoolApi.kt | 17 ++ .../request/P2PEthPoolAccountsListRequest.kt | 16 ++ .../P2PEthPoolAccountsListResponse.kt | 26 ++++ .../response/P2PEthPoolErrorResponse.kt | 2 +- .../P2PEthPoolAccountsListResponseTest.kt | 68 ++++++++ .../DefaultMultiStakingBalanceFetcher.kt | 52 +------ .../multi/P2PEthPoolAccountsFetcher.kt | 102 ++++++++++++ .../DefaultMultiStakingBalanceFetcherTest.kt | 145 +++++++++++++++++- 8 files changed, 380 insertions(+), 48 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolAccountsListRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolAccountsListResponse.kt create mode 100644 core/datasource/src/test/kotlin/com/tangem/datasource/api/ethpool/P2PEthPoolAccountsListResponseTest.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/multi/P2PEthPoolAccountsFetcher.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt index f76e0a7bfa..b0dacd4aed 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/P2PEthPoolApi.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.api.ethpool import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolAccountsListRequest import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolBroadcastRequest import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolTransactionRequest import com.tangem.datasource.api.ethpool.models.response.* @@ -94,4 +95,20 @@ interface P2PEthPoolApi { @Path("delegatorAddress") delegatorAddress: String, @Path("vaultAddress") vaultAddress: String, ): ApiResponse> + + /** + * Get account summaries for multiple delegators in a vault (batch). + * + * Designed to be called once per client to avoid rate-limit bursts. + * + * @param network Ethereum pool network: "mainnet" or "hoodi" + * @param vaultAddress Ethereum address of the vault + * @param body Delegator addresses to fetch (up to 255) + */ + @POST("api/v1/staking/pool/{network}/vaults/{vaultAddress}/accounts/list") + suspend fun getAccountsList( + @Path("network") network: String, + @Path("vaultAddress") vaultAddress: String, + @Body body: P2PEthPoolAccountsListRequest, + ): ApiResponse> } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolAccountsListRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolAccountsListRequest.kt new file mode 100644 index 0000000000..25762a5bae --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/request/P2PEthPoolAccountsListRequest.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.api.ethpool.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Request body for POST /api/v1/staking/pool/{network}/vaults/{vaultAddress}/accounts/list + * + * Batch fetch of staking balances for multiple delegator addresses within a single vault. + * Limit: up to 255 addresses per request. Addresses are deduplicated server-side. + */ +@JsonClass(generateAdapter = true) +data class P2PEthPoolAccountsListRequest( + @Json(name = "delegatorAddresses") + val delegatorAddresses: List, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolAccountsListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolAccountsListResponse.kt new file mode 100644 index 0000000000..552497b306 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolAccountsListResponse.kt @@ -0,0 +1,26 @@ +package com.tangem.datasource.api.ethpool.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Response for POST /api/v1/staking/pool/{network}/vaults/{vaultAddress}/accounts/list + * + * Each item is keyed by delegatorAddress and carries either a non-null [account] + * or a per-address [error] (e.g. code 127108 — invalid delegator address). + */ +@JsonClass(generateAdapter = true) +data class P2PEthPoolAccountsListResponse( + @Json(name = "list") + val list: List, +) + +@JsonClass(generateAdapter = true) +data class P2PEthPoolAccountListItem( + @Json(name = "delegatorAddress") + val delegatorAddress: String, + @Json(name = "account") + val account: P2PEthPoolAccountResponse?, + @Json(name = "error") + val error: P2PEthPoolErrorDetailsDTO?, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolErrorResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolErrorResponse.kt index c99f726d91..a376482c5b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolErrorResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/ethpool/models/response/P2PEthPoolErrorResponse.kt @@ -23,7 +23,7 @@ data class P2PEthPoolErrorDetailsDTO( @Json(name = "message") val message: String, // Human-readable error message @Json(name = "name") - val name: String, // Error name/type + val name: String?, // Error name/type @Json(name = "errors") val errors: List? = null, // Optional validation errors array ) \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/ethpool/P2PEthPoolAccountsListResponseTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/ethpool/P2PEthPoolAccountsListResponseTest.kt new file mode 100644 index 0000000000..dd96bd7ac5 --- /dev/null +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/ethpool/P2PEthPoolAccountsListResponseTest.kt @@ -0,0 +1,68 @@ +package com.tangem.datasource.api.ethpool + +import com.google.common.truth.Truth.assertThat +import com.squareup.moshi.Types +import com.tangem.datasource.api.common.MoshiConverter +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountsListResponse +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolResponse +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +internal class P2PEthPoolAccountsListResponseTest { + + private val adapter = MoshiConverter.networkMoshi.adapter>( + Types.newParameterizedType( + P2PEthPoolResponse::class.java, + P2PEthPoolAccountsListResponse::class.java, + ), + ) + + @Test + fun `decode batch payload with valid account and per-address error`() { + val response = requireNotNull(adapter.fromJson(SAMPLE_JSON)) + + val list = requireNotNull(response.result).list + assertThat(list).hasSize(2) + + val good = list.first { it.account != null } + val account = requireNotNull(good.account) + assertThat(account.stake.assets.compareTo(BigDecimal("1.2345"))).isEqualTo(0) + assertThat(account.availableToWithdraw).isGreaterThan(BigDecimal(15049)) + assertThat(account.exitQueue.requests).isEmpty() + + val bad = list.first { it.account == null } + assertThat(requireNotNull(bad.error).code).isEqualTo(127108) + } + + private companion object { + private val SAMPLE_JSON = """ + { + "error": null, + "result": { + "list": [ + { + "delegatorAddress": "0x008d3cd3e349Cd3D5F7c287b3BaF9e4f3E4ba99b", + "account": { + "delegatorAddress": "0x008d3cd3e349Cd3D5F7c287b3BaF9e4f3E4ba99b", + "vaultAddress": "0x4c09BC47db288F998b33CD63BCc1b6ddCCe13F33", + "stake": { "assets": "1.234500000000000000", "totalEarnedAssets": 0.0191 }, + "availableToUnstake": "0.000000000000000005", + "availableToWithdraw": 15049.547647281135, + "exitQueue": { "total": 0, "requests": [] } + }, + "error": null + }, + { + "delegatorAddress": "0xBADADDRESS", + "account": null, + "error": { + "code": 127108, + "message": "The provided delegator address is invalid or not properly formatted." + } + } + ] + } + } + """.trimIndent() + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt index d329e2bb77..2560fbfbeb 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt @@ -8,7 +8,6 @@ import com.tangem.data.common.api.safeApiCall import com.tangem.data.staking.store.P2PEthPoolBalancesStore import com.tangem.data.staking.store.StakeKitBalancesStore import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory -import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.ethpool.P2PEthPoolApi import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse import com.tangem.datasource.api.stakekit.StakeKitApi @@ -25,7 +24,6 @@ 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.P2PEthPoolStakingConfig import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -65,6 +63,11 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, ) : MultiStakingBalanceFetcher { + private val p2pAccountsFetcher = P2PEthPoolAccountsFetcher( + p2pEthPoolApi = p2pEthPoolApi, + dispatchers = dispatchers, + ) + override suspend fun invoke(params: MultiStakingBalanceFetcher.Params): Either { TangemLogger.i("Start fetching staking balances for params:\n$params") @@ -195,50 +198,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( vaults: List, addresses: Set, ): Set { - val responses = mutableSetOf() - - 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) { - TangemLogger.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 -> { - TangemLogger.w( - "Failed to fetch P2PEthPool balance for vault ${vault.vaultAddress}, " + - "address $address", - response.cause, - ) - } - } - }.onFailure { error -> - TangemLogger.w( - "Failed to fetch P2PEthPool balance for vault ${vault.vaultAddress}, address $address", - error, - ) - } - } - } - - return responses + return p2pAccountsFetcher.fetchBatch(vaults = vaults, addresses = addresses) } private inline fun checkIsSupportedByWalletOrElse(userWalletId: UserWalletId, ifNotSupported: (Throwable) -> Unit) { diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/P2PEthPoolAccountsFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/P2PEthPoolAccountsFetcher.kt new file mode 100644 index 0000000000..1b491ca10f --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/P2PEthPoolAccountsFetcher.kt @@ -0,0 +1,102 @@ +package com.tangem.data.staking.multi + +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.ethpool.P2PEthPoolApi +import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolAccountsListRequest +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountsListResponse +import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolResponse +import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig +import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope + +/** + * Fetches P2P ETH Pool account responses via the batch strategy: + * one POST per vault sending all delegator addresses at once. + * + * @property p2pEthPoolApi P2PEthPool API + * @property dispatchers coroutine dispatcher provider + * +[REDACTED_AUTHOR] + */ +internal class P2PEthPoolAccountsFetcher( + private val p2pEthPoolApi: P2PEthPoolApi, + private val dispatchers: CoroutineDispatcherProvider, +) { + + suspend fun fetchBatch(vaults: List, addresses: Set): Set = + coroutineScope { + val request = P2PEthPoolAccountsListRequest(delegatorAddresses = addresses.toList()) + + vaults + .map { vault -> + async(dispatchers.io) { + runSuspendCatching { + val response = p2pEthPoolApi.getAccountsList( + network = P2PEthPoolStakingConfig.activeNetwork.value, + vaultAddress = vault.vaultAddress, + body = request, + ) + + mapBatchVaultResponse(vault = vault, response = response) + }.getOrElse { error -> + TangemLogger.w( + "Failed to fetch P2PEthPool batch balances for vault ${vault.vaultAddress}", + error, + ) + emptyList() + } + } + } + .awaitAll() + .flatten() + .toSet() + } + + private fun mapBatchVaultResponse( + vault: P2PEthPoolVault, + response: ApiResponse>, + ): List { + return when (response) { + is ApiResponse.Success -> { + val data = response.data + if (data.error != null) { + TangemLogger.w( + "P2PEthPool batch API returned error for vault " + + "${vault.vaultAddress}: ${data.error}", + ) + emptyList() + } else { + val result = requireNotNull(data.result) { + "Result is null in successful response" + } + result.list.mapNotNull { item -> + if (item.error != null) { + TangemLogger.w( + "P2PEthPool batch item error for vault " + + "${vault.vaultAddress}, address " + + "${item.delegatorAddress}: ${item.error}", + ) + null + } else { + item.account + } + } + } + } + is ApiResponse.Error -> { + TangemLogger.w( + "Failed to fetch P2PEthPool batch balances for vault " + + "${vault.vaultAddress}", + response.cause, + ) + emptyList() + } + } + } +} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt index 3290286794..4b285df8bc 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt @@ -10,12 +10,15 @@ import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.ethpool.P2PEthPoolApi +import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolAccountsListRequest +import com.tangem.datasource.api.ethpool.models.response.* import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.token.P2PEthPoolVaultsStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.staking.StakingID +import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.test.core.assertEitherLeft import com.tangem.test.core.assertEitherRight @@ -26,6 +29,7 @@ import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance +import java.math.BigDecimal /** [REDACTED_AUTHOR] @@ -54,7 +58,14 @@ internal class DefaultMultiStakingBalanceFetcherTest { @BeforeEach fun resetMocks() { - clearMocks(userWalletsListRepository, stakingYieldsStore, stakeKitBalancesStore, stakeKitApi) + clearMocks( + userWalletsListRepository, + stakingYieldsStore, + stakeKitBalancesStore, + stakeKitApi, + p2pEthPoolApi, + p2pEthPoolVaultsStore, + ) } @Test @@ -359,6 +370,88 @@ internal class DefaultMultiStakingBalanceFetcherTest { assertEitherLeft(actual, expected) } + @Test + fun `fetch P2P balances via batch endpoint`() = runTest { + // Arrange + val params = MultiStakingBalanceFetcher.Params(userWalletId, setOf(p2pId1, p2pId2)) + + every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(userWallet)) + coEvery { p2pEthPoolVaultsStore.getSync() } returns listOf(vault(VAULT_A), vault(VAULT_B)) + + coEvery { p2pEthPoolApi.getAccountsList(any(), VAULT_A, any()) } returns + accountsListSuccess(accountResponse(ADDR_1, VAULT_A)) + coEvery { p2pEthPoolApi.getAccountsList(any(), VAULT_B, any()) } returns + accountsListSuccess(accountResponse(ADDR_2, VAULT_B)) + + // Actual + val actual = fetcher.invoke(params) + + // Assert + coVerify(exactly = 1) { + p2pEthPoolApi.getAccountsList( + network = any(), + vaultAddress = VAULT_A, + body = match { it.delegatorAddresses.containsAll(listOf(ADDR_1, ADDR_2)) }, + ) + } + coVerify(exactly = 1) { + p2pEthPoolApi.getAccountsList(network = any(), vaultAddress = VAULT_B, body = any()) + } + coVerify(inverse = true) { p2pEthPoolApi.getAccountInfo(any(), any(), any()) } + coVerify { p2PEthPoolBalancesStore.storeActual(userWalletId = userWalletId, values = any()) } + + assertEitherRight(actual) + } + + @Test + fun `fetch P2P batch maps per-item error to missing stakingId`() = runTest { + // Arrange + val params = MultiStakingBalanceFetcher.Params(userWalletId, setOf(p2pId1, p2pId2)) + + every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(userWallet)) + coEvery { p2pEthPoolVaultsStore.getSync() } returns listOf(vault(VAULT_A)) + + coEvery { p2pEthPoolApi.getAccountsList(any(), VAULT_A, any()) } returns + ApiResponse.Success( + P2PEthPoolResponse( + error = null, + result = P2PEthPoolAccountsListResponse( + list = listOf( + P2PEthPoolAccountListItem( + delegatorAddress = ADDR_1, + account = accountResponse(ADDR_1, VAULT_A), + error = null, + ), + P2PEthPoolAccountListItem( + delegatorAddress = ADDR_2, + account = null, + error = P2PEthPoolErrorDetailsDTO( + code = 127108, + message = "invalid", + name = null, + errors = null, + ), + ), + ), + ), + ), + ) + + // Actual + val actual = fetcher.invoke(params) + + // Assert + coVerify { p2PEthPoolBalancesStore.storeActual(userWalletId = userWalletId, values = any()) } + coVerify { + p2PEthPoolBalancesStore.storeError( + userWalletId = userWalletId, + stakingIds = match { it == setOf(p2pId2) }, + ) + } + + assertEitherRight(actual) + } + private companion object { val userWallet = MockUserWalletFactory.create() val userWalletId = userWallet.walletId @@ -370,5 +463,55 @@ internal class DefaultMultiStakingBalanceFetcherTest { ) val tonAndSolanaIds = setOf(tonId, solanaId) + + const val ADDR_1 = "0x1111111111111111111111111111111111111111" + const val ADDR_2 = "0x2222222222222222222222222222222222222222" + const val VAULT_A = "0xVaultAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + const val VAULT_B = "0xVaultBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" + + val p2pId1 = StakingID(integrationId = "p2p-ethereum-pooled", address = ADDR_1) + val p2pId2 = StakingID(integrationId = "p2p-ethereum-pooled", address = ADDR_2) + + fun vault(address: String) = P2PEthPoolVault( + vaultAddress = address, + displayName = "Vault", + apy = BigDecimal("4.5"), + baseApy = BigDecimal("4.0"), + capacity = BigDecimal("1000"), + totalAssets = BigDecimal("100"), + feePercent = BigDecimal("10"), + isPrivate = false, + isGenesis = false, + isSmoothingPool = true, + isErc20 = false, + tokenName = null, + tokenSymbol = null, + createdAt = 0L, + ) + + fun accountResponse(address: String, vaultAddress: String) = P2PEthPoolAccountResponse( + delegatorAddress = address, + vaultAddress = vaultAddress, + stake = P2PEthPoolStakeDTO(assets = BigDecimal("1.5"), totalEarnedAssets = BigDecimal("0.1")), + availableToUnstake = BigDecimal.ZERO, + availableToWithdraw = BigDecimal.ZERO, + exitQueue = P2PEthPoolExitQueueDTO(total = BigDecimal.ZERO, requests = emptyList()), + ) + + fun accountsListSuccess(vararg accounts: P2PEthPoolAccountResponse) = + ApiResponse.Success( + P2PEthPoolResponse( + error = null, + result = P2PEthPoolAccountsListResponse( + list = accounts.map { + P2PEthPoolAccountListItem( + delegatorAddress = it.delegatorAddress, + account = it, + error = null, + ) + }, + ), + ), + ) } } \ No newline at end of file From 2e4a443fa09e09b45137223fd524c8b1fa3dd1f7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 1 Jul 2026 17:26:01 +0000 Subject: [PATCH 5/5] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 0f32ca5313..7f9bf93423 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.39-1589" +tangemBlockchainSdk = "releases-6.0-1590" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.39-623" +tangemCardSdk = "releases-6.0-626" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "tangem-master-21" #tangemVico = "0.0.1" # Keep it! - used for local builds ^