Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-14 12:46:07 +03:00
parent ecb1c6d272
commit 822ed8eb80
14 changed files with 570 additions and 9 deletions

View file

@ -8,6 +8,8 @@ import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.intent.Intents
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeoutOrNull
import com.kaspersky.components.alluresupport.interceptors.step.ScreenshotStepInterceptor
import com.kaspersky.components.alluresupport.withForcedAllureSupport
import com.kaspersky.components.composesupport.config.addComposeSupport
@ -20,13 +22,14 @@ import com.tangem.common.rules.ApiEnvironmentRule
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.datasource.utils.WireMockRedirectInterceptor
import com.tangem.domain.promo.PromoRepository
import com.tangem.domain.promo.models.PromoId
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.tap.MainActivity
import dagger.hilt.android.testing.HiltAndroidRule
import io.qameta.allure.kotlin.Allure
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
@ -58,6 +61,12 @@ abstract class BaseTestCase : TestCase(
@Inject
lateinit var promoRepository: PromoRepository
@Inject
lateinit var walletManagersStore: WalletManagersStore
@Inject
lateinit var getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase
private val hiltRule = HiltAndroidRule(this)
private val apiEnvironmentRule = ApiEnvironmentRule()
private val permissionRule = GrantPermissionRule.grant(
@ -72,7 +81,11 @@ abstract class BaseTestCase : TestCase(
private val semanticTreePrinterRule = object : TestWatcher() {
override fun failed(e: Throwable?, description: Description?) {
runCatching { printAllRoots() }
runCatching {
runBlocking {
withTimeoutOrNull(SEMANTIC_TREE_PRINT_TIMEOUT_MS) { printAllRoots() }
}
}
}
}
@ -174,5 +187,6 @@ abstract class BaseTestCase : TestCase(
private companion object {
const val WIREMOCK_BASE_URL_ARG = "wiremockBaseUrl"
const val SEMANTIC_TREE_PRINT_TIMEOUT_MS = 5_000L
}
}

View file

@ -35,6 +35,7 @@ object TestConstants {
const val WAIT_UNTIL_TIMEOUT = 20_000L
const val WAIT_UNTIL_TIMEOUT_LONG = 30_000L
const val WAIT_UNTIL_TIMEOUT_VERY_LONG = 60_000L
const val MARKETS_MAIN_NETWORK_SUFFIX = "MAIN"
@ -43,4 +44,14 @@ object TestConstants {
const val USER_TOKENS_API_SCENARIO = "user_tokens_api"
const val QUOTES_API_SCENARIO = "quotes_api"
const val SEED_PHRASE_12 = "they cram join fantasy unfair observe true theory buffalo bus exchange walk"
const val SEED_PHRASE_15 = "genuine try deer upset connect sausage diary rule price shallow fit faculty leopard " +
"hawk when"
const val SEED_PHRASE_18 = "crush idle include refuse expose kiss slot budget uphold when dinner certain holiday " +
"slow word armor butter suffer"
const val SEED_PHRASE_21 = "employ space oval venue wash clog zebra cover icon wash assist word debris inform " +
"cable meadow add game meat rigid pride"
const val SEED_PHRASE_24 = "force visit fresh brown razor target ill scissors figure cave feel genre cargo category " +
"bread much nature basic fun iron benefit egg error prosper"
}

View file

@ -0,0 +1,88 @@
package com.tangem.common.utils
import com.tangem.utils.logging.TangemLogger
import org.json.JSONArray
import org.json.JSONObject
import org.junit.Assert.assertTrue
/**
* Compares addresses from the app (clipboard JSON) with reference addresses from the QA tools API.
*
* Both JSON arrays contain objects with fields: blockchain, derivationPath, token (nullable), addresses (array).
* Comparison normalizes and sorts both arrays before diffing.
*/
object AddressComparisonHelper {
fun compareAddresses(appJson: String, apiJson: String) {
val appEntries = parseAndNormalize(appJson)
val apiEntries = parseAndNormalize(apiJson)
val missingInApp = apiEntries - appEntries.toSet()
val extraInApp = appEntries - apiEntries.toSet()
if (missingInApp.isEmpty() && extraInApp.isEmpty()) {
TangemLogger.i("Address comparison passed: ${appEntries.size} entries match")
return
}
val report = buildString {
appendLine("Address comparison FAILED")
if (missingInApp.isNotEmpty()) {
appendLine("\nMissing in app (expected from API but not found):")
missingInApp.forEach { appendLine(" - $it") }
}
if (extraInApp.isNotEmpty()) {
appendLine("\nExtra in app (found in app but not in API):")
extraInApp.forEach { appendLine(" - $it") }
}
appendLine("\nApp entries: ${appEntries.size}, API entries: ${apiEntries.size}")
}
TangemLogger.e(report)
assertTrue(report, false)
}
private fun parseAndNormalize(json: String): List<AddressEntry> {
val array = JSONArray(json)
val entries = mutableListOf<AddressEntry>()
for (i in 0 until array.length()) {
val obj = array.getJSONObject(i)
entries.add(
AddressEntry(
blockchain = normalizeBlockchainName(obj.getString("blockchain")),
derivationPath = obj.getString("derivationPath").trim(),
token = obj.optString("token", null)?.trim()?.lowercase(),
addresses = parseAddresses(obj).sorted(),
),
)
}
return entries.sortedWith(
compareBy<AddressEntry> { it.blockchain }
.thenBy { it.derivationPath }
.thenBy { it.token },
)
}
private val blockchainNameOverrides = mapOf(
"chia network" to "chia",
)
private fun normalizeBlockchainName(name: String): String {
val normalized = name.trim().lowercase()
return blockchainNameOverrides[normalized] ?: normalized
}
private fun parseAddresses(obj: JSONObject): List<String> {
val addressesArray = obj.getJSONArray("addresses")
return (0 until addressesArray.length()).map { addressesArray.getString(it) }
}
private data class AddressEntry(
val blockchain: String,
val derivationPath: String,
val token: String?,
val addresses: List<String>,
)
}

View file

@ -16,10 +16,10 @@ fun getWcUri(
TangemLogger.i("Getting WC URI for network: $network")
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) // Таймаут подключения
.readTimeout(60, TimeUnit.SECONDS) // Таймаут чтения ответа
.writeTimeout(30, TimeUnit.SECONDS) // Таймаут записи
.callTimeout(90, TimeUnit.SECONDS) // Общий таймаут запроса
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.callTimeout(90, TimeUnit.SECONDS)
.build()
val request = Request.Builder()
@ -58,6 +58,59 @@ fun getWcUri(
}
}
fun getAddressesFromApi(
seedKey: String,
baseUrl: String = "[REDACTED_ENV_URL]",
): String? {
TangemLogger.i("Getting addresses for seed key: $seedKey")
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.callTimeout(90, TimeUnit.SECONDS)
.build()
val request = Request.Builder()
.url("$baseUrl/addresses")
.get()
.build()
return try {
client.newCall(request).execute().use { response ->
TangemLogger.i("Response code: ${response.code}")
if (response.isSuccessful) {
val body = response.body?.string() ?: ""
val contentType = response.header("Content-Type") ?: ""
if (!contentType.contains("application/json") && !body.trimStart().startsWith("{")) {
TangemLogger.e("Unexpected response (not JSON), Content-Type: $contentType, body: $body")
return null
}
val jsonObject = JSONObject(body)
val data = jsonObject.optJSONObject("data") ?: jsonObject
val seedData = data.optJSONArray(seedKey)
if (seedData != null) {
TangemLogger.i("Got addresses for $seedKey: ${seedData.length()} entries")
seedData.toString()
} else {
TangemLogger.e("No data found for seed key: $seedKey")
null
}
} else {
val errorBody = response.body?.string() ?: "No error body"
TangemLogger.e("Request failed: ${response.code}, body: $errorBody")
null
}
}
} catch (e: Exception) {
TangemLogger.e("Error getting addresses", e)
null
}
}
fun checkServiceHealth(
baseUrl: String = "[REDACTED_ENV_URL]"
): String? {

View file

@ -0,0 +1,122 @@
package com.tangem.scenarios
import android.view.KeyEvent
import androidx.test.core.app.ApplicationProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG
import com.tangem.common.utils.AddressComparisonHelper
import com.tangem.common.utils.getClipboardText
import com.tangem.screens.onMainScreen
import com.tangem.screens.onTesterMenuScreen
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.logging.TangemLogger
import io.qameta.allure.kotlin.Allure.step
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.Assert.assertNotNull
private const val WALLET_MANAGERS_SETTLE_MS = 5_000L
private const val WALLET_MANAGERS_POLL_INTERVAL_MS = 500L
fun BaseTestCase.verifyAddresses(seedPhrase: String, apiAddressesJson: String) {
var appAddressesJson: String? = null
step("Open 'Main Screen' with existing hot wallet") {
openMainScreenWithExistingHotWallet(seedPhrase)
}
step("Assert wallet balance = '$DASH_SIGN'") {
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) {
runCatching { onMainScreen { totalBalanceText.assertTextContains(DASH_SIGN) } }.isSuccess
}
}
step("Assert 'Organize tokens' button is enabled") {
composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) {
runCatching { onMainScreen { organizeTokensButton().assertIsEnabled() } }.isSuccess
}
}
step("Wait for all wallet managers to initialize") {
awaitWalletManagersStabilized()
}
step("Open tester menu") {
openTesterMenu()
}
step("Click on 'Addresses info' button") {
onTesterMenuScreen { addressesInfoButton.performClick() }
}
step("Click on 'JSON' tab") {
onTesterMenuScreen { jsonTab.performClick() }
}
step("Click on 'Copy' button") {
onTesterMenuScreen { copyButton.performClick() }
}
step("Get addresses JSON from clipboard") {
appAddressesJson = getClipboardText(ApplicationProvider.getApplicationContext())
assertNotNull("Clipboard is empty after copying addresses", appAddressesJson)
}
step("Compare app addresses with API reference") {
AddressComparisonHelper.compareAddresses(
appJson = requireNotNull(appAddressesJson),
apiJson = apiAddressesJson,
)
}
}
private const val TESTER_MENU_MAX_ATTEMPTS = 3
/**
* Presses 'Volume Down' twice to open tester menu.
* Retries up to [TESTER_MENU_MAX_ATTEMPTS] times if the menu doesn't appear.
*/
private fun BaseTestCase.openTesterMenu() {
repeat(TESTER_MENU_MAX_ATTEMPTS) { attempt ->
waitForIdle()
device.uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_DOWN)
device.uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_DOWN)
val opened = runCatching {
onTesterMenuScreen { addressesInfoButton.assertIsDisplayed() }
}.isSuccess
if (opened) {
TangemLogger.i("Tester menu opened on attempt ${attempt + 1}")
return
}
TangemLogger.w("Tester menu not opened on attempt ${attempt + 1}, retrying...")
}
error("Failed to open tester menu after $TESTER_MENU_MAX_ATTEMPTS attempts")
}
/**
* Polls [walletManagersStore] until the wallet manager count stops growing for [WALLET_MANAGERS_SETTLE_MS].
*
* Uses [getAllSync] with a polling interval instead of Flow, because the Flow only emits on changes
* if the count stabilizes, there would be no new emission to check the settle timeout against.
*/
private fun BaseTestCase.awaitWalletManagersStabilized() {
val walletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId
?: error("No selected wallet found")
var lastSize = -1
var stableStart = System.currentTimeMillis()
runBlocking {
withTimeout(WAIT_UNTIL_TIMEOUT_VERY_LONG) {
while (true) {
val currentSize = walletManagersStore.getAllSync(walletId).size
val now = System.currentTimeMillis()
if (currentSize != lastSize) {
TangemLogger.i("Wallet managers count: $currentSize (was $lastSize)")
lastSize = currentSize
stableStart = now
} else if (now - stableStart >= WALLET_MANAGERS_SETTLE_MS) {
TangemLogger.i("Wallet managers stabilized at $currentSize entries")
return@withTimeout
}
delay(WALLET_MANAGERS_POLL_INTERVAL_MS)
}
}
}
}

View file

@ -59,6 +59,57 @@ fun BaseTestCase.openMainScreen(
}
}
fun BaseTestCase.openMainScreenWithExistingHotWallet(seedPhrase: String) {
step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Click on 'Get started' button") {
onStoriesScreen { getStartedButton.clickWithAssertion() }
}
step("Click on 'Start with Mobile Wallet' button") {
onCreateWalletStartScreen { startWithMobileWalletButton.performClick() }
}
step("Click on 'Import existing wallet' button") {
onCreateMobileWalletScreen { importExistingWalletButton.performClick() }
}
step("Click on 'Phrase text field'") {
onImportWalletScreen { phraseTextField.performClick() }
}
step("Type seed phrase in 'Phrase text field'") {
onImportWalletScreen { phraseTextField.performTextReplacement(seedPhrase) }
}
step("Click on 'Import' button") {
onImportWalletScreen {
importButton.assertIsEnabled()
importButton.performClick()
}
}
step("Click on 'Continue' button") {
onImportWalletScreen {
continueButton.assertIsEnabled()
continueButton.performClick()
}
}
step("Click on 'Skip' button") {
onImportWalletScreen { skipButton.performClick() }
}
step("Click on 'Skip anyway' dialog button") {
onDialog { skipAnywayButton.performClick() }
}
step("Click on 'Finish' button") {
onImportWalletScreen {
finishButton.assertIsEnabled()
finishButton.performClick()
}
}
step("Assert 'Main' screen is displayed") {
onMainScreen { screenContainer.assertIsDisplayed() }
}
step("Dismiss Market Tooltip by clicking close button") {
onMarketsTooltipScreen { closeButton.clickWithAssertion() }
}
}
fun BaseTestCase.synchronizeAddresses(
balance: String? = null,
isBalanceAvailable: Boolean = true

View file

@ -0,0 +1,23 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.R
import com.tangem.core.ui.test.BaseButtonTestTags
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
class CreateMobileWalletPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<CreateMobileWalletPageObject>(semanticsProvider = semanticsProvider) {
val importExistingWalletButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.hw_import_existing_wallet))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onCreateMobileWalletScreen(function: CreateMobileWalletPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -17,6 +17,11 @@ class CreateWalletStartPageObject(semanticsProvider: SemanticsNodeInteractionsPr
hasText(getResourceString(OnboardingImplR.string.welcome_unlock_card))
useUnmergedTree = true
}
val startWithMobileWalletButton: KNode = child {
hasText(getResourceString(OnboardingImplR.string.welcome_create_wallet_mobile_title))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onCreateWalletStartScreen(function: CreateWalletStartPageObject.() -> Unit) =

View file

@ -64,6 +64,11 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.common_change))
}
val skipAnywayButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasText(getResourceString(R.string.access_code_alert_skip_ok))
}
}
internal fun BaseTestCase.onDialog(function: DialogPageObject.() -> Unit) =

View file

@ -0,0 +1,54 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.common.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.ImportWalletScreenTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
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.hasText as withText
class ImportWalletPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<ImportWalletPageObject>(semanticsProvider = semanticsProvider) {
val phraseTextField: KNode = child {
hasTestTag(ImportWalletScreenTestTags.PHRASE_TEXT_FIELD)
useUnmergedTree = true
}
val passphraseTextField: KNode = child {
hasTestTag(ImportWalletScreenTestTags.PASSPHRASE_TEXT_FIELD)
useUnmergedTree = true
}
val importButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyChild(withText(getResourceString(R.string.common_import)))
useUnmergedTree = true
}
val continueButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyChild(withText(getResourceString(R.string.common_continue)))
useUnmergedTree = true
}
val skipButton: KNode = child {
hasTestTag(TopAppBarTestTags.MORE_BUTTON)
hasText(getResourceString(R.string.common_skip))
useUnmergedTree = true
}
val finishButton: KNode = child {
hasTestTag(BaseButtonTestTags.BUTTON)
hasAnyChild(withText(getResourceString(R.string.common_finish)))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onImportWalletScreen(function: ImportWalletPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,39 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.feature.tester.impl.R as TesterImplR
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.TopAppBarTestTags
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
class TesterMenuPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<TesterMenuPageObject>(semanticsProvider = semanticsProvider) {
val backButton: KNode = child {
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
useUnmergedTree = true
}
val addressesInfoButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(TesterImplR.string.addresses_info))
useUnmergedTree = true
}
val jsonTab: KNode = child {
hasText("JSON")
useUnmergedTree = true
}
val copyButton: KNode = child {
hasContentDescription("Copy")
useUnmergedTree = true
}
}
internal fun BaseTestCase.onTesterMenuScreen(function: TesterMenuPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,86 @@
package com.tangem.tests.hotWallet
import com.tangem.common.BaseTestCase
import com.tangem.common.annotations.ApiEnv
import com.tangem.common.annotations.ApiEnvConfig
import com.tangem.common.constants.TestConstants.SEED_PHRASE_12
import com.tangem.common.constants.TestConstants.SEED_PHRASE_15
import com.tangem.common.constants.TestConstants.SEED_PHRASE_18
import com.tangem.common.constants.TestConstants.SEED_PHRASE_21
import com.tangem.common.constants.TestConstants.SEED_PHRASE_24
import com.tangem.common.utils.checkServiceHealth
import com.tangem.common.utils.getAddressesFromApi
import com.tangem.datasource.api.common.config.ApiConfig
import com.tangem.datasource.api.common.config.ApiEnvironment
import com.tangem.scenarios.verifyAddresses
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Assert.assertNotNull
import org.junit.Test
@HiltAndroidTest
class AddressesTest : BaseTestCase() {
private var apiAddressesJson: String? = null
private fun setupAddressTestHooks(seedKey: String) = setupHooks(
additionalBeforeAppLaunchSection = {
val status = checkServiceHealth()
assertNotNull("QA tools service is unreachable ([REDACTED_ENV_URL]", status)
apiAddressesJson = getAddressesFromApi(seedKey)
assertNotNull("Failed to fetch reference addresses for '$seedKey'", apiAddressesJson)
},
)
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
@AllureId("1792")
@DisplayName("Hot wallet: auto derivation addresses for seed 12")
@Test
fun seed12AddressesTest() {
setupAddressTestHooks("twelve").run {
verifyAddresses(SEED_PHRASE_12, requireNotNull(apiAddressesJson))
}
}
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
@AllureId("5106")
@DisplayName("Hot wallet: auto derivation addresses for seed 15")
@Test
fun seed15AddressesTest() {
setupAddressTestHooks("fifteen").run {
verifyAddresses(SEED_PHRASE_15, requireNotNull(apiAddressesJson))
}
}
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
@AllureId("5107")
@DisplayName("Hot wallet: auto derivation addresses for seed 18")
@Test
fun seed18AddressesTest() {
setupAddressTestHooks("eighteen").run {
verifyAddresses(SEED_PHRASE_18, requireNotNull(apiAddressesJson))
}
}
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
@AllureId("5108")
@DisplayName("Hot wallet: auto derivation addresses for seed 21")
@Test
fun seed21AddressesTest() {
setupAddressTestHooks("twenty_one").run {
verifyAddresses(SEED_PHRASE_21, requireNotNull(apiAddressesJson))
}
}
@ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD))
@AllureId("5109")
@DisplayName("Hot wallet: auto derivation addresses for seed 24")
@Test
fun seed24AddressesTest() {
setupAddressTestHooks("twenty_four").run {
verifyAddresses(SEED_PHRASE_24, requireNotNull(apiAddressesJson))
}
}
}