Updated on 2026-08-14
This commit is contained in:
parent
a50cb15228
commit
e2cd6813a0
19 changed files with 4599 additions and 0 deletions
|
|
@ -0,0 +1,194 @@
|
|||
package com.tangem.data.yield.supply
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumUtils
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runCurrent
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultYieldModuleAddressProviderTest {
|
||||
|
||||
private val walletManager: WalletManager = mockk()
|
||||
private val walletManagersFacade: WalletManagersFacade = mockk()
|
||||
|
||||
private val provider = DefaultYieldModuleAddressProvider(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("abcdef012345")
|
||||
private val otherWalletId = UserWalletId("fedcba543210")
|
||||
private val network = network()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
clearMocks(walletManager, walletManagersFacade)
|
||||
provider.invalidate(null)
|
||||
coEvery {
|
||||
walletManagersFacade.getOrCreateWalletManager(any(), any(), any())
|
||||
} returns walletManager
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-zero address WHEN getOrFetch THEN returns and caches it`() = runTest {
|
||||
// Arrange
|
||||
coEvery { walletManager.getYieldModuleAddress() } returns ADDRESS
|
||||
|
||||
// Act
|
||||
val first = provider.getOrFetch(userWalletId, network)
|
||||
val second = provider.getOrFetch(userWalletId, network)
|
||||
|
||||
// Assert
|
||||
assertThat(first).isEqualTo(ADDRESS)
|
||||
assertThat(second).isEqualTo(ADDRESS)
|
||||
coVerify(exactly = 1) { walletManagersFacade.getOrCreateWalletManager(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN zero address WHEN getOrFetch THEN returns null and does not cache`() = runTest {
|
||||
// Arrange
|
||||
coEvery { walletManager.getYieldModuleAddress() } returns EthereumUtils.ZERO_ADDRESS
|
||||
|
||||
// Act
|
||||
val first = provider.getOrFetch(userWalletId, network)
|
||||
val second = provider.getOrFetch(userWalletId, network)
|
||||
|
||||
// Assert — null result is never cached, so the manager is queried again
|
||||
assertThat(first).isNull()
|
||||
assertThat(second).isNull()
|
||||
coVerify(exactly = 2) { walletManager.getYieldModuleAddress() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN missing wallet manager WHEN getOrFetch THEN throws`() = runTest {
|
||||
// Arrange
|
||||
coEvery { walletManagersFacade.getOrCreateWalletManager(any(), any(), any()) } returns null
|
||||
|
||||
// Act
|
||||
val error = runCatching { provider.getOrFetch(userWalletId, network) }.exceptionOrNull()
|
||||
|
||||
// Assert
|
||||
assertThat(error).isInstanceOf(IllegalStateException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cached address WHEN invalidate for that wallet THEN it is refetched`() = runTest {
|
||||
// Arrange
|
||||
coEvery { walletManager.getYieldModuleAddress() } returns ADDRESS
|
||||
provider.getOrFetch(userWalletId, network)
|
||||
|
||||
// Act
|
||||
provider.invalidate(userWalletId)
|
||||
provider.getOrFetch(userWalletId, network)
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 2) { walletManagersFacade.getOrCreateWalletManager(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN two cached wallets WHEN invalidate one THEN only that one is refetched`() = runTest {
|
||||
// Arrange
|
||||
coEvery { walletManager.getYieldModuleAddress() } returns ADDRESS
|
||||
provider.getOrFetch(userWalletId, network)
|
||||
provider.getOrFetch(otherWalletId, network)
|
||||
|
||||
// Act
|
||||
provider.invalidate(userWalletId)
|
||||
provider.getOrFetch(userWalletId, network) // refetched
|
||||
provider.getOrFetch(otherWalletId, network) // still cached
|
||||
|
||||
// Assert — 2 initial fetches + 1 refetch for the invalidated wallet only
|
||||
coVerify(exactly = 3) { walletManagersFacade.getOrCreateWalletManager(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cached addresses WHEN invalidate all THEN every wallet is refetched`() = runTest {
|
||||
// Arrange
|
||||
coEvery { walletManager.getYieldModuleAddress() } returns ADDRESS
|
||||
provider.getOrFetch(userWalletId, network)
|
||||
provider.getOrFetch(otherWalletId, network)
|
||||
|
||||
// Act
|
||||
provider.invalidate(null)
|
||||
provider.getOrFetch(userWalletId, network)
|
||||
provider.getOrFetch(otherWalletId, network)
|
||||
|
||||
// Assert — 2 initial + 2 after a full invalidation
|
||||
coVerify(exactly = 4) { walletManagersFacade.getOrCreateWalletManager(any(), any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN two concurrent fetches for the same key WHEN one is in flight THEN manager is created once`() = runTest {
|
||||
// Arrange — io dispatcher we control so both callers reach the mutex before the cache is populated
|
||||
val testDispatcher = StandardTestDispatcher(testScheduler)
|
||||
val concurrentProvider = DefaultYieldModuleAddressProvider(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(
|
||||
main = testDispatcher,
|
||||
mainImmediate = testDispatcher,
|
||||
io = testDispatcher,
|
||||
default = testDispatcher,
|
||||
single = testDispatcher,
|
||||
),
|
||||
)
|
||||
val proceed = CompletableDeferred<Unit>()
|
||||
coEvery { walletManager.getYieldModuleAddress() } coAnswers {
|
||||
proceed.await()
|
||||
ADDRESS
|
||||
}
|
||||
|
||||
// Act — both pass the lock-free pre-check; one holds the lock and fetches, the other waits on it
|
||||
val first = launch { concurrentProvider.getOrFetch(userWalletId, network) }
|
||||
val second = launch { concurrentProvider.getOrFetch(userWalletId, network) }
|
||||
runCurrent()
|
||||
// At the barrier both callers have passed the lock-free pre-check (cache still empty): one holds the mutex and
|
||||
// awaits the gate, the other is blocked on the lock. Asserting neither completed proves the second did NOT
|
||||
// short-circuit on the outer pre-check, so it must hit the in-lock double-check once released.
|
||||
assertThat(first.isCompleted).isFalse()
|
||||
assertThat(second.isCompleted).isFalse()
|
||||
proceed.complete(Unit)
|
||||
advanceUntilIdle()
|
||||
first.join()
|
||||
second.join()
|
||||
|
||||
// Assert — the second caller is served from cache via the in-lock double-check
|
||||
coVerify(exactly = 1) { walletManagersFacade.getOrCreateWalletManager(any(), any(), any()) }
|
||||
coVerify(exactly = 1) { walletManager.getYieldModuleAddress() }
|
||||
}
|
||||
|
||||
private fun network(): Network {
|
||||
val derivationPath = Network.DerivationPath.None
|
||||
return Network(
|
||||
id = Network.ID(value = "ethereum", derivationPath = derivationPath),
|
||||
name = "Ethereum",
|
||||
currencySymbol = "ETH",
|
||||
derivationPath = derivationPath,
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.Unspecified("UNSPECIFIED"),
|
||||
hasFiatFeeRate = true,
|
||||
canHandleTokens = true,
|
||||
transactionExtrasType = Network.TransactionExtrasType.NONE,
|
||||
nameResolvingType = Network.NameResolvingType.NONE,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ADDRESS = "0x1234567890abcdef1234567890abcdef12345678"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.data.yield.supply
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.yield.supply.YieldSupplyError
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.io.IOException
|
||||
|
||||
internal class DefaultYieldSupplyErrorResolverTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN a YieldSupplyError WHEN resolve THEN returns the same instance`() {
|
||||
// Arrange
|
||||
val error = YieldSupplyError.DataError(IOException("boom"))
|
||||
|
||||
// Act
|
||||
val result = DefaultYieldSupplyErrorResolver.resolve(error)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isSameInstanceAs(error)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a generic throwable WHEN resolve THEN wraps it into DataError`() {
|
||||
// Arrange
|
||||
val throwable = IllegalStateException("unexpected")
|
||||
|
||||
// Act
|
||||
val result = DefaultYieldSupplyErrorResolver.resolve(throwable)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(YieldSupplyError.DataError(throwable))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,470 @@
|
|||
package com.tangem.data.yield.supply
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionStatus
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.core.analytics.api.AnalyticsExceptionHandler
|
||||
import com.tangem.data.yield.supply.converters.YieldMarketTokenConverter
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.tangemTech.YieldSupplyApi
|
||||
import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.YieldModuleStatusResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
|
||||
import com.tangem.datasource.api.tangemTech.models.YieldTokenChartResponse
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.get
|
||||
import com.tangem.datasource.local.preferences.utils.store
|
||||
import com.tangem.datasource.local.yieldsupply.YieldMarketsStore
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
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.io.IOException
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultYieldSupplyRepositoryTest {
|
||||
|
||||
private val yieldSupplyApi: YieldSupplyApi = mockk()
|
||||
private val store: YieldMarketsStore = mockk(relaxed = true)
|
||||
private val walletManagersFacade: WalletManagersFacade = mockk()
|
||||
private val analyticsExceptionHandler: AnalyticsExceptionHandler = mockk(relaxed = true)
|
||||
private val appPreferencesStore: AppPreferencesStore = mockk()
|
||||
|
||||
private val repository = DefaultYieldSupplyRepository(
|
||||
yieldSupplyApi = yieldSupplyApi,
|
||||
store = store,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
analyticsExceptionHandler = analyticsExceptionHandler,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("abcdef012345")
|
||||
private val token = token()
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
clearMocks(yieldSupplyApi, store, walletManagersFacade, analyticsExceptionHandler)
|
||||
}
|
||||
|
||||
// region markets
|
||||
@Test
|
||||
fun `GIVEN cached dtos WHEN getCachedMarkets THEN returns enriched domain`() = runTest {
|
||||
// Arrange
|
||||
coEvery { store.getSyncOrNull() } returns listOf(marketDto(chainId = 1))
|
||||
|
||||
// Act
|
||||
val result = repository.getCachedMarkets()
|
||||
|
||||
// Assert — chainId 1 is enriched to its network id
|
||||
assertThat(result).hasSize(1)
|
||||
assertThat(result.first().backendId).isEqualTo("ethereum")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty cache WHEN getCachedMarkets THEN returns empty list`() = runTest {
|
||||
// Arrange
|
||||
coEvery { store.getSyncOrNull() } returns null
|
||||
|
||||
// Act
|
||||
val result = repository.getCachedMarkets()
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cached dto with unmapped chain id WHEN getCachedMarkets THEN backend id is null`() = runTest {
|
||||
// Arrange — chainId -1 (the converter's default for a DTO without a chainId) maps to no network
|
||||
coEvery { store.getSyncOrNull() } returns listOf(marketDto(chainId = -1))
|
||||
|
||||
// Act
|
||||
val result = repository.getCachedMarkets()
|
||||
|
||||
// Assert
|
||||
assertThat(result).hasSize(1)
|
||||
assertThat(result.first().backendId).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN api returns markets WHEN updateMarkets THEN stores dtos and returns domain`() = runTest {
|
||||
// Arrange
|
||||
val dto = marketDto(chainId = 1)
|
||||
coEvery { yieldSupplyApi.getYieldMarkets(any()) } returns ApiResponse.Success(
|
||||
YieldMarketsResponse(marketDtos = listOf(dto), lastUpdated = "now"),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = repository.updateMarkets()
|
||||
|
||||
// Assert
|
||||
assertThat(result).containsExactly(YieldMarketTokenConverter.convert(dto))
|
||||
coVerify(exactly = 1) { store.store(listOf(dto)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN store flow WHEN getMarketsFlow THEN emits enriched domain`() = runTest {
|
||||
// Arrange
|
||||
every { store.get() } returns flowOf(listOf(marketDto(chainId = 1)))
|
||||
|
||||
// Act
|
||||
val result = repository.getMarketsFlow().first()
|
||||
|
||||
// Assert
|
||||
assertThat(result.first().backendId).isEqualTo("ethereum")
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region token status / chart
|
||||
@Test
|
||||
fun `GIVEN evm token WHEN getTokenStatus THEN returns converted market token`() = runTest {
|
||||
// Arrange
|
||||
val dto = marketDto(chainId = 1)
|
||||
coEvery { yieldSupplyApi.getYieldTokenStatus(1, token.contractAddress) } returns ApiResponse.Success(dto)
|
||||
|
||||
// Act
|
||||
val result = repository.getTokenStatus(token)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(YieldMarketTokenConverter.convert(dto))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-evm token WHEN getTokenStatus THEN throws`() = runTest {
|
||||
// Arrange
|
||||
val nonEvm = token(rawId = "unknown-network-xyz")
|
||||
|
||||
// Act
|
||||
val error = runCatching { repository.getTokenStatus(nonEvm) }.exceptionOrNull()
|
||||
|
||||
// Assert
|
||||
assertThat(error).isInstanceOf(IllegalStateException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN evm token WHEN getTokenChart THEN returns converted chart`() = runTest {
|
||||
// Arrange
|
||||
coEvery { yieldSupplyApi.getYieldTokenChart(1, token.contractAddress) } returns ApiResponse.Success(
|
||||
chartResponse(),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = repository.getTokenChart(token)
|
||||
|
||||
// Assert
|
||||
assertThat(result.avr).isEqualTo(4.25)
|
||||
assertThat(result.y).containsExactly(3.5).inOrder()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-evm token WHEN getTokenChart THEN throws`() = runTest {
|
||||
// Arrange
|
||||
val nonEvm = token(rawId = "unknown-network-xyz")
|
||||
|
||||
// Act
|
||||
val error = runCatching { repository.getTokenChart(nonEvm) }.exceptionOrNull()
|
||||
|
||||
// Assert
|
||||
assertThat(error).isInstanceOf(IllegalStateException::class.java)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region isYieldSupplySupported
|
||||
@Test
|
||||
fun `GIVEN supported yield provider WHEN isYieldSupplySupported THEN returns true`() = runTest {
|
||||
// Arrange — WalletManager itself implements YieldSupplyProvider
|
||||
val walletManager = mockk<WalletManager> { every { isSupported() } returns true }
|
||||
coEvery { walletManagersFacade.getOrCreateWalletManager(any(), any(), any()) } returns walletManager
|
||||
|
||||
// Act
|
||||
val result = repository.isYieldSupplySupported(userWalletId, token)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN unsupported yield provider WHEN isYieldSupplySupported THEN returns false`() = runTest {
|
||||
// Arrange
|
||||
val walletManager = mockk<WalletManager> { every { isSupported() } returns false }
|
||||
coEvery { walletManagersFacade.getOrCreateWalletManager(any(), any(), any()) } returns walletManager
|
||||
|
||||
// Act
|
||||
val result = repository.isYieldSupplySupported(userWalletId, token)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no wallet manager WHEN isYieldSupplySupported THEN sends analytics and returns false`() = runTest {
|
||||
// Arrange
|
||||
coEvery { walletManagersFacade.getOrCreateWalletManager(any(), any(), any()) } returns null
|
||||
|
||||
// Act
|
||||
val result = repository.isYieldSupplySupported(userWalletId, token)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isFalse()
|
||||
verify { analyticsExceptionHandler.sendException(any()) }
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region activate / deactivate
|
||||
@Test
|
||||
fun `GIVEN api returns active WHEN activateProtocol THEN returns true`() = runTest {
|
||||
// Arrange
|
||||
coEvery {
|
||||
yieldSupplyApi.activateYieldModule(body = any(), userWalletId = any())
|
||||
} returns ApiResponse.Success(statusResponse(isActive = true))
|
||||
|
||||
// Act
|
||||
val result = repository.activateProtocol(userWalletId, token, ADDRESS)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-evm token WHEN activateProtocol THEN throws`() = runTest {
|
||||
// Arrange
|
||||
val nonEvm = token(rawId = "unknown-network-xyz")
|
||||
|
||||
// Act
|
||||
val error = runCatching { repository.activateProtocol(userWalletId, nonEvm, ADDRESS) }.exceptionOrNull()
|
||||
|
||||
// Assert
|
||||
assertThat(error).isInstanceOf(IllegalStateException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN api returns inactive WHEN deactivateProtocol THEN returns false`() = runTest {
|
||||
// Arrange
|
||||
coEvery { yieldSupplyApi.deactivateYieldModule(any()) } returns ApiResponse.Success(
|
||||
statusResponse(isActive = false),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = repository.deactivateProtocol(token, ADDRESS)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN non-evm token WHEN deactivateProtocol THEN throws`() = runTest {
|
||||
// Arrange
|
||||
val nonEvm = token(rawId = "unknown-network-xyz")
|
||||
|
||||
// Act
|
||||
val error = runCatching { repository.deactivateProtocol(nonEvm, ADDRESS) }.exceptionOrNull()
|
||||
|
||||
// Assert
|
||||
assertThat(error).isInstanceOf(IllegalStateException::class.java)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region pending status (in-memory)
|
||||
@Test
|
||||
fun `GIVEN saved pending status WHEN getTokenProtocolPendingStatus THEN returns it`() = runTest {
|
||||
// Arrange
|
||||
val status = YieldSupplyPendingStatus.Enter(txIds = listOf("0x1"), createdAt = 1L)
|
||||
repository.saveTokenProtocolPendingStatus(userWalletId, token, status)
|
||||
|
||||
// Act
|
||||
val result = repository.getTokenProtocolPendingStatus(userWalletId, token)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN saved then cleared WHEN getTokenProtocolPendingStatus THEN returns null`() = runTest {
|
||||
// Arrange
|
||||
repository.saveTokenProtocolPendingStatus(
|
||||
userWalletId,
|
||||
token,
|
||||
YieldSupplyPendingStatus.Enter(txIds = listOf("0x1"), createdAt = 1L),
|
||||
)
|
||||
|
||||
// Act
|
||||
repository.saveTokenProtocolPendingStatus(userWalletId, token, null)
|
||||
val result = repository.getTokenProtocolPendingStatus(userWalletId, token)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN saved status WHEN flow collected THEN emits the status`() = runTest {
|
||||
// Arrange
|
||||
val status = YieldSupplyPendingStatus.Exit(txIds = listOf("0x9"), createdAt = 1L)
|
||||
repository.saveTokenProtocolPendingStatus(userWalletId, token, status)
|
||||
|
||||
// Act
|
||||
val emitted = repository.getTokenProtocolPendingStatusFlow(userWalletId, token).first()
|
||||
|
||||
// Assert
|
||||
assertThat(emitted).isEqualTo(status)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region pending tx hashes
|
||||
@Test
|
||||
fun `GIVEN unconfirmed and confirmed txs WHEN getPendingTxHashes THEN returns only unconfirmed hashes`() = runTest {
|
||||
// Arrange
|
||||
val walletManager = mockk<WalletManager> {
|
||||
every { wallet.recentTransactions } returns mutableListOf(
|
||||
tx(TransactionStatus.Unconfirmed, "0xUnconfirmed"),
|
||||
tx(TransactionStatus.Confirmed, "0xConfirmed"),
|
||||
)
|
||||
}
|
||||
coEvery { walletManagersFacade.getOrCreateWalletManager(any(), any<Network>()) } returns walletManager
|
||||
|
||||
// Act
|
||||
val result = repository.getPendingTxHashes(userWalletId, token)
|
||||
|
||||
// Assert
|
||||
assertThat(result).containsExactly("0xUnconfirmed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no wallet manager WHEN getPendingTxHashes THEN returns empty`() = runTest {
|
||||
// Arrange
|
||||
coEvery { walletManagersFacade.getOrCreateWalletManager(any(), any<Network>()) } returns null
|
||||
|
||||
// Act
|
||||
val result = repository.getPendingTxHashes(userWalletId, token)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region promo banner preference
|
||||
@Test
|
||||
fun `GIVEN stored flag WHEN getShouldShowYieldPromoBanner THEN emits it`() = runTest {
|
||||
// Arrange
|
||||
mockkStatic("com.tangem.datasource.local.preferences.utils.PreferencesDataStoreExtKt")
|
||||
try {
|
||||
every {
|
||||
appPreferencesStore.get(PreferencesKeys.YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY, true)
|
||||
} returns flowOf(false)
|
||||
|
||||
// Act
|
||||
val result = repository.getShouldShowYieldPromoBanner().first()
|
||||
|
||||
// Assert
|
||||
assertThat(result).isFalse()
|
||||
} finally {
|
||||
unmockkStatic("com.tangem.datasource.local.preferences.utils.PreferencesDataStoreExtKt")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN setShouldShowYieldPromoBanner THEN stores the value`() = runTest {
|
||||
// Arrange
|
||||
mockkStatic("com.tangem.datasource.local.preferences.utils.PreferencesDataStoreExtKt")
|
||||
try {
|
||||
coEvery {
|
||||
appPreferencesStore.store(PreferencesKeys.YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY, false)
|
||||
} returns Unit
|
||||
|
||||
// Act
|
||||
repository.setShouldShowYieldPromoBanner(false)
|
||||
|
||||
// Assert
|
||||
coVerify { appPreferencesStore.store(PreferencesKeys.YIELD_SUPPLY_SHOULD_SHOW_MAIN_PROMO_KEY, false) }
|
||||
} finally {
|
||||
unmockkStatic("com.tangem.datasource.local.preferences.utils.PreferencesDataStoreExtKt")
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
private fun tx(status: TransactionStatus, hash: String): TransactionData.Uncompiled = mockk {
|
||||
every { this@mockk.status } returns status
|
||||
every { this@mockk.hash } returns hash
|
||||
}
|
||||
|
||||
private fun marketDto(chainId: Int) = YieldSupplyMarketTokenDto(
|
||||
tokenAddress = "0xToken",
|
||||
tokenSymbol = "USDT",
|
||||
tokenName = "Tether",
|
||||
apy = BigDecimal("5.5"),
|
||||
decimals = 6,
|
||||
isActive = true,
|
||||
chainId = chainId,
|
||||
maxFeeNative = BigDecimal("0.005"),
|
||||
maxFeeUSD = BigDecimal("12.34"),
|
||||
)
|
||||
|
||||
private fun chartResponse() = YieldTokenChartResponse(
|
||||
underlying = "USDT",
|
||||
market = "aave",
|
||||
bucketSizeDays = 1,
|
||||
period = "30d",
|
||||
data = listOf(YieldTokenChartResponse.DataPoint(bucketIndex = 0, avgApy = BigDecimal("3.5"))),
|
||||
averageApy = BigDecimal("4.25"),
|
||||
)
|
||||
|
||||
private fun statusResponse(isActive: Boolean) = YieldModuleStatusResponse(
|
||||
tokenAddress = "0xToken",
|
||||
chainId = 1,
|
||||
isActive = isActive,
|
||||
activatedAt = null,
|
||||
deactivatedAt = null,
|
||||
)
|
||||
|
||||
private fun token(rawId: String = "ethereum", contractAddress: String = "0xToken"): CryptoCurrency.Token {
|
||||
val derivationPath = Network.DerivationPath.None
|
||||
val network = Network(
|
||||
id = Network.ID(value = rawId, derivationPath = derivationPath),
|
||||
name = "Net",
|
||||
currencySymbol = "ETH",
|
||||
derivationPath = derivationPath,
|
||||
isTestnet = false,
|
||||
standardType = Network.StandardType.Unspecified("UNSPECIFIED"),
|
||||
hasFiatFeeRate = true,
|
||||
canHandleTokens = true,
|
||||
transactionExtrasType = Network.TransactionExtrasType.NONE,
|
||||
nameResolvingType = Network.NameResolvingType.NONE,
|
||||
)
|
||||
return CryptoCurrency.Token(
|
||||
id = CryptoCurrency.ID(
|
||||
prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX,
|
||||
body = CryptoCurrency.ID.Body.NetworkId(rawId),
|
||||
suffix = CryptoCurrency.ID.Suffix.RawID(rawId),
|
||||
),
|
||||
network = network,
|
||||
name = "TEST_TOKEN",
|
||||
symbol = "TTK",
|
||||
decimals = 6,
|
||||
iconUrl = null,
|
||||
isCustom = false,
|
||||
contractAddress = contractAddress,
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val ADDRESS = "0x1111111111111111111111111111111111111111"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.tangem.data.yield.supply.converters
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.tangemTech.models.YieldSupplyMarketTokenDto
|
||||
import com.tangem.domain.yield.supply.models.YieldMarketToken
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class YieldMarketTokenConverterTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN fully populated dto WHEN convert THEN maps every field`() {
|
||||
// Arrange
|
||||
val dto = YieldSupplyMarketTokenDto(
|
||||
tokenAddress = "0xToken",
|
||||
tokenSymbol = "USDT",
|
||||
tokenName = "Tether",
|
||||
apy = BigDecimal("5.5"),
|
||||
decimals = 6,
|
||||
isActive = true,
|
||||
chainId = 1,
|
||||
maxFeeNative = BigDecimal("0.005"),
|
||||
maxFeeUSD = BigDecimal("12.34"),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = YieldMarketTokenConverter.convert(dto)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(
|
||||
YieldMarketToken(
|
||||
tokenAddress = "0xToken",
|
||||
chainId = 1,
|
||||
apy = BigDecimal("5.5"),
|
||||
isActive = true,
|
||||
maxFeeNative = BigDecimal("0.005"),
|
||||
maxFeeUSD = BigDecimal("12.34"),
|
||||
backendId = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN dto with null fields WHEN convert THEN applies defaults`() {
|
||||
// Arrange
|
||||
val dto = YieldSupplyMarketTokenDto()
|
||||
|
||||
// Act
|
||||
val result = YieldMarketTokenConverter.convert(dto)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(
|
||||
YieldMarketToken(
|
||||
tokenAddress = "",
|
||||
chainId = -1,
|
||||
apy = BigDecimal.ZERO,
|
||||
isActive = false,
|
||||
maxFeeNative = BigDecimal.ZERO,
|
||||
maxFeeUSD = BigDecimal.ZERO,
|
||||
backendId = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.data.yield.supply.converters
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.api.tangemTech.models.YieldTokenChartResponse
|
||||
import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class YieldTokenChartConverterTest {
|
||||
|
||||
@Test
|
||||
fun `GIVEN response with data points WHEN convert THEN splits avgApy into y and bucketIndex into x preserving order`() {
|
||||
// Arrange
|
||||
val response = response(
|
||||
averageApy = BigDecimal("4.25"),
|
||||
points = listOf(
|
||||
YieldTokenChartResponse.DataPoint(bucketIndex = 0, avgApy = BigDecimal("3.5")),
|
||||
YieldTokenChartResponse.DataPoint(bucketIndex = 1, avgApy = BigDecimal("4.0")),
|
||||
YieldTokenChartResponse.DataPoint(bucketIndex = 2, avgApy = BigDecimal("5.0")),
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = YieldTokenChartConverter.convert(response)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(
|
||||
YieldSupplyMarketChartData(
|
||||
y = listOf(3.5, 4.0, 5.0),
|
||||
x = listOf(0.0, 1.0, 2.0),
|
||||
avr = 4.25,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN response with empty data WHEN convert THEN returns empty y and x with average`() {
|
||||
// Arrange
|
||||
val response = response(averageApy = BigDecimal("1.0"), points = emptyList())
|
||||
|
||||
// Act
|
||||
val result = YieldTokenChartConverter.convert(response)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(
|
||||
YieldSupplyMarketChartData(y = emptyList(), x = emptyList(), avr = 1.0),
|
||||
)
|
||||
}
|
||||
|
||||
private fun response(averageApy: BigDecimal, points: List<YieldTokenChartResponse.DataPoint>) =
|
||||
YieldTokenChartResponse(
|
||||
underlying = "USDT",
|
||||
market = "aave",
|
||||
bucketSizeDays = 1,
|
||||
period = "30d",
|
||||
data = points,
|
||||
averageApy = averageApy,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
package com.tangem.data.yield.supply.promo
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.data.yield.supply.promo.converter.YieldBoostPromoConverter
|
||||
import com.tangem.data.yield.supply.promo.converter.YieldBoostStatusConverter
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.promotion.models.PromotionsResponse
|
||||
import com.tangem.datasource.api.promotion.models.YieldBoostStatusResponse
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.yieldsupply.promo.YieldBoostPromoStore
|
||||
import com.tangem.datasource.local.yieldsupply.promo.YieldBoostStatusStore
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.yield.supply.models.YieldBoostPromo
|
||||
import com.tangem.domain.yield.supply.models.YieldBoostStatus
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.clearMocks
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
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.io.IOException
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultYieldPromoRepositoryTest {
|
||||
|
||||
private val tangemApi: TangemTechApi = mockk()
|
||||
private val promoStore: YieldBoostPromoStore = mockk(relaxed = true)
|
||||
private val statusStore: YieldBoostStatusStore = mockk(relaxed = true)
|
||||
|
||||
private val repository = DefaultYieldPromoRepository(
|
||||
tangemApi = tangemApi,
|
||||
promoStore = promoStore,
|
||||
statusStore = statusStore,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val userWalletId = UserWalletId("abcdef012345")
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
clearMocks(tangemApi, promoStore, statusStore)
|
||||
}
|
||||
|
||||
// region getYieldBoostPromo
|
||||
@Test
|
||||
fun `GIVEN cached promo and no refresh WHEN getYieldBoostPromo THEN returns cache without api`() = runTest {
|
||||
// Arrange
|
||||
val cached = YieldBoostPromo.None
|
||||
coEvery { promoStore.getSyncOrNull(userWalletId) } returns cached
|
||||
|
||||
// Act
|
||||
val result = repository.getYieldBoostPromo(userWalletId, forceRefresh = false)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(cached)
|
||||
coVerify(exactly = 0) { tangemApi.getPromotions(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no cache WHEN getYieldBoostPromo THEN fetches stores and returns converted`() = runTest {
|
||||
// Arrange
|
||||
val dto = matchingPromoDto()
|
||||
coEvery { promoStore.getSyncOrNull(userWalletId) } returns null
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } returns ApiResponse.Success(
|
||||
PromotionsResponse(promotions = listOf(dto)),
|
||||
)
|
||||
val expected = YieldBoostPromoConverter.convert(dto)
|
||||
|
||||
// Act
|
||||
val result = repository.getYieldBoostPromo(userWalletId, forceRefresh = false)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(expected)
|
||||
coVerify(exactly = 1) { promoStore.store(userWalletId, expected) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cached promo and force refresh WHEN getYieldBoostPromo THEN fetches anyway`() = runTest {
|
||||
// Arrange
|
||||
coEvery { promoStore.getSyncOrNull(userWalletId) } returns YieldBoostPromo.None
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } returns ApiResponse.Success(
|
||||
PromotionsResponse(promotions = listOf(matchingPromoDto())),
|
||||
)
|
||||
|
||||
// Act
|
||||
repository.getYieldBoostPromo(userWalletId, forceRefresh = true)
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { tangemApi.getPromotions(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no matching promo name WHEN getYieldBoostPromo THEN returns None`() = runTest {
|
||||
// Arrange
|
||||
coEvery { promoStore.getSyncOrNull(userWalletId) } returns null
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } returns ApiResponse.Success(
|
||||
PromotionsResponse(promotions = listOf(PromotionsResponse.PromotionDto(name = "other", all = null))),
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = repository.getYieldBoostPromo(userWalletId, forceRefresh = false)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(YieldBoostPromo.None)
|
||||
coVerify(exactly = 1) { promoStore.store(userWalletId, YieldBoostPromo.None) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fetch fails and cache present WHEN getYieldBoostPromo THEN falls back to cache`() = runTest {
|
||||
// Arrange — force refresh so the initial cache check is skipped and the fetch is attempted
|
||||
val cached = YieldBoostPromo.None
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } throws IOException("network")
|
||||
coEvery { promoStore.getSyncOrNull(userWalletId) } returns cached
|
||||
|
||||
// Act
|
||||
val result = repository.getYieldBoostPromo(userWalletId, forceRefresh = true)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(cached)
|
||||
coVerify(exactly = 0) { promoStore.store(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fetch fails and no cache WHEN getYieldBoostPromo THEN rethrows`() = runTest {
|
||||
// Arrange
|
||||
coEvery { tangemApi.getPromotions(any(), any()) } throws IOException("network")
|
||||
coEvery { promoStore.getSyncOrNull(userWalletId) } returns null
|
||||
|
||||
// Act
|
||||
val error = runCatching { repository.getYieldBoostPromo(userWalletId, forceRefresh = true) }
|
||||
.exceptionOrNull()
|
||||
|
||||
// Assert
|
||||
assertThat(error).isInstanceOf(IOException::class.java)
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region getYieldBoostStatus
|
||||
@Test
|
||||
fun `GIVEN cached status and no refresh WHEN getYieldBoostStatus THEN returns cache without api`() = runTest {
|
||||
// Arrange
|
||||
val cached = YieldBoostStatus.NotStarted
|
||||
coEvery { statusStore.getSyncOrNull(userWalletId) } returns cached
|
||||
|
||||
// Act
|
||||
val result = repository.getYieldBoostStatus(userWalletId, forceRefresh = false)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(cached)
|
||||
coVerify(exactly = 0) { tangemApi.getYieldBoostStatus(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no cache WHEN getYieldBoostStatus THEN fetches stores and returns converted`() = runTest {
|
||||
// Arrange
|
||||
val response = statusResponse()
|
||||
coEvery { statusStore.getSyncOrNull(userWalletId) } returns null
|
||||
coEvery { tangemApi.getYieldBoostStatus(any()) } returns ApiResponse.Success(response)
|
||||
val expected = YieldBoostStatusConverter.convert(response)
|
||||
|
||||
// Act
|
||||
val result = repository.getYieldBoostStatus(userWalletId, forceRefresh = false)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(expected)
|
||||
coVerify(exactly = 1) { statusStore.store(userWalletId, expected) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN cached status and force refresh WHEN getYieldBoostStatus THEN fetches anyway`() = runTest {
|
||||
// Arrange
|
||||
coEvery { statusStore.getSyncOrNull(userWalletId) } returns YieldBoostStatus.NotStarted
|
||||
coEvery { tangemApi.getYieldBoostStatus(any()) } returns ApiResponse.Success(statusResponse())
|
||||
|
||||
// Act
|
||||
repository.getYieldBoostStatus(userWalletId, forceRefresh = true)
|
||||
|
||||
// Assert
|
||||
coVerify(exactly = 1) { tangemApi.getYieldBoostStatus(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fetch fails and cache present WHEN getYieldBoostStatus THEN falls back to cache`() = runTest {
|
||||
// Arrange — force refresh so the initial cache check is skipped and the fetch is attempted
|
||||
val cached = YieldBoostStatus.NotStarted
|
||||
coEvery { tangemApi.getYieldBoostStatus(any()) } throws IOException("network")
|
||||
coEvery { statusStore.getSyncOrNull(userWalletId) } returns cached
|
||||
|
||||
// Act
|
||||
val result = repository.getYieldBoostStatus(userWalletId, forceRefresh = true)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(cached)
|
||||
coVerify(exactly = 0) { statusStore.store(any(), any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN fetch fails and no cache WHEN getYieldBoostStatus THEN rethrows`() = runTest {
|
||||
// Arrange
|
||||
coEvery { tangemApi.getYieldBoostStatus(any()) } throws IOException("network")
|
||||
coEvery { statusStore.getSyncOrNull(userWalletId) } returns null
|
||||
|
||||
// Act
|
||||
val error = runCatching { repository.getYieldBoostStatus(userWalletId, forceRefresh = true) }
|
||||
.exceptionOrNull()
|
||||
|
||||
// Assert
|
||||
assertThat(error).isInstanceOf(IOException::class.java)
|
||||
}
|
||||
// endregion
|
||||
|
||||
private fun matchingPromoDto() = PromotionsResponse.PromotionDto(
|
||||
name = "yield-apr-boost",
|
||||
all = PromotionsResponse.PromotionDto.All(
|
||||
timeline = PromotionsResponse.PromotionDto.Timeline(
|
||||
start = "2026-06-15T00:00:00.000Z",
|
||||
end = "2027-06-15T22:00:00.000Z",
|
||||
),
|
||||
tokens = listOf(
|
||||
PromotionsResponse.PromotionDto.PromoToken(
|
||||
tokenAddress = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
||||
tokenSymbol = "USDC",
|
||||
tokenName = "USD Coin",
|
||||
networkId = "ethereum",
|
||||
),
|
||||
),
|
||||
status = "active",
|
||||
link = "https://example.com/terms",
|
||||
),
|
||||
)
|
||||
|
||||
private fun statusResponse() = YieldBoostStatusResponse(
|
||||
tokenName = "USD Coin",
|
||||
networkId = "ethereum",
|
||||
moduleAddress = "0xModule",
|
||||
userAddress = "0xUser",
|
||||
contractAddress = "0xContract",
|
||||
promoEnrollmentStatus = "NOT_STARTED",
|
||||
qualificationEndDate = null,
|
||||
disqualificationReason = null,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue