Updated on 2026-08-14
This commit is contained in:
parent
b6ab30c56e
commit
040b5f0cf6
11 changed files with 404 additions and 0 deletions
|
|
@ -152,6 +152,7 @@ dependencies {
|
||||||
implementation(projects.domain.manageTokens)
|
implementation(projects.domain.manageTokens)
|
||||||
implementation(projects.domain.nft)
|
implementation(projects.domain.nft)
|
||||||
implementation(projects.domain.nft.models)
|
implementation(projects.domain.nft.models)
|
||||||
|
implementation(projects.domain.offramp)
|
||||||
implementation(projects.domain.onramp)
|
implementation(projects.domain.onramp)
|
||||||
implementation(projects.domain.promo)
|
implementation(projects.domain.promo)
|
||||||
implementation(projects.domain.promo.models)
|
implementation(projects.domain.promo.models)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.tangem.tap.data
|
||||||
|
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.offramp.repository.OfframpRepository
|
||||||
|
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
||||||
|
import com.tangem.tap.network.exchangeServices.SellService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default implementation of [OfframpRepository]
|
||||||
|
*
|
||||||
|
* @property sellService sell service for getting offramp URL
|
||||||
|
*/
|
||||||
|
internal class DefaultOfframpRepository(
|
||||||
|
private val sellService: SellService,
|
||||||
|
) : OfframpRepository {
|
||||||
|
|
||||||
|
override fun getOfframpUrl(
|
||||||
|
cryptoCurrency: CryptoCurrency,
|
||||||
|
fiatCurrencyCode: String,
|
||||||
|
walletAddress: String,
|
||||||
|
): String? {
|
||||||
|
return sellService.getUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyName = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
package com.tangem.tap.di.domain
|
package com.tangem.tap.di.domain
|
||||||
|
|
||||||
|
import com.tangem.domain.offramp.GetOfframpUrlUseCase
|
||||||
|
import com.tangem.domain.offramp.repository.OfframpRepository
|
||||||
import com.tangem.domain.onramp.*
|
import com.tangem.domain.onramp.*
|
||||||
import com.tangem.domain.onramp.repositories.*
|
import com.tangem.domain.onramp.repositories.*
|
||||||
import com.tangem.domain.promo.PromoRepository
|
import com.tangem.domain.promo.PromoRepository
|
||||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||||
|
import com.tangem.tap.data.DefaultOfframpRepository
|
||||||
|
import com.tangem.tap.network.exchangeServices.SellService
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
|
@ -275,4 +279,16 @@ internal object OnrampDomainModule {
|
||||||
promoRepository = promoRepository,
|
promoRepository = promoRepository,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideOfframpRepository(sellService: SellService): OfframpRepository {
|
||||||
|
return DefaultOfframpRepository(sellService)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideGetOfframpUrlUseCase(offrampRepository: OfframpRepository): GetOfframpUrlUseCase {
|
||||||
|
return GetOfframpUrlUseCase(offrampRepository)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
package com.tangem.tap.data
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
|
||||||
|
import com.tangem.tap.network.exchangeServices.SellService
|
||||||
|
import io.mockk.*
|
||||||
|
import org.junit.jupiter.api.AfterEach
|
||||||
|
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 DefaultOfframpRepositoryTest {
|
||||||
|
|
||||||
|
private val sellService: SellService = mockk()
|
||||||
|
private val repository = DefaultOfframpRepository(sellService)
|
||||||
|
|
||||||
|
private val cryptoCurrency: CryptoCurrency = mockk()
|
||||||
|
private val fiatCurrencyCode = "USD"
|
||||||
|
private val walletAddress = "0x1234567890abcdef"
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setUp() {
|
||||||
|
mockkObject(MutableAppThemeModeHolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
fun tearDown() {
|
||||||
|
clearMocks(sellService)
|
||||||
|
unmockkObject(MutableAppThemeModeHolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getOfframpUrl should return url when sellService returns url with light theme`() {
|
||||||
|
// Arrange
|
||||||
|
val expectedUrl = "https://moonpay.com/sell?address=$walletAddress&theme=light"
|
||||||
|
every { MutableAppThemeModeHolder.isDarkThemeActive } returns false
|
||||||
|
every {
|
||||||
|
sellService.getUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyName = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
isDarkTheme = false,
|
||||||
|
)
|
||||||
|
} returns expectedUrl
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = repository.getOfframpUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyCode = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result).isEqualTo(expectedUrl)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
sellService.getUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyName = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
isDarkTheme = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getOfframpUrl should return url when sellService returns url with dark theme`() {
|
||||||
|
// Arrange
|
||||||
|
val expectedUrl = "https://moonpay.com/sell?address=$walletAddress&theme=dark"
|
||||||
|
every { MutableAppThemeModeHolder.isDarkThemeActive } returns true
|
||||||
|
every {
|
||||||
|
sellService.getUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyName = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
isDarkTheme = true,
|
||||||
|
)
|
||||||
|
} returns expectedUrl
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = repository.getOfframpUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyCode = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result).isEqualTo(expectedUrl)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
sellService.getUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyName = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
isDarkTheme = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getOfframpUrl should return null when sellService returns null`() {
|
||||||
|
// Arrange
|
||||||
|
every { MutableAppThemeModeHolder.isDarkThemeActive } returns false
|
||||||
|
every {
|
||||||
|
sellService.getUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyName = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
isDarkTheme = false,
|
||||||
|
)
|
||||||
|
} returns null
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = repository.getOfframpUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyCode = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result).isNull()
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
sellService.getUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyName = fiatCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
isDarkTheme = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.tangem.core.analytics.models.event
|
||||||
|
|
||||||
|
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offramp (withdraw/sell) analytics events
|
||||||
|
*/
|
||||||
|
sealed class OfframpAnalyticsEvent(
|
||||||
|
event: String,
|
||||||
|
params: Map<String, String> = emptyMap(),
|
||||||
|
) : AnalyticsEvent(category = "Token / Withdraw", event = event, params = params) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Withdraw screen opened event
|
||||||
|
*/
|
||||||
|
data object ScreenOpened : OfframpAnalyticsEvent("Withdraw Screen Opened")
|
||||||
|
}
|
||||||
1
domain/offramp/.gitignore
vendored
Normal file
1
domain/offramp/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/build
|
||||||
18
domain/offramp/build.gradle.kts
Normal file
18
domain/offramp/build.gradle.kts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
plugins {
|
||||||
|
alias(deps.plugins.kotlin.jvm)
|
||||||
|
id("configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Test>().configureEach {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
/** Domain modules */
|
||||||
|
api(projects.domain.core)
|
||||||
|
api(projects.domain.models)
|
||||||
|
|
||||||
|
/** Test libraries */
|
||||||
|
testImplementation(projects.test.core)
|
||||||
|
testRuntimeOnly(deps.test.junit5.engine)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.tangem.domain.offramp
|
||||||
|
|
||||||
|
import arrow.core.Either
|
||||||
|
import arrow.core.raise.either
|
||||||
|
import arrow.core.raise.ensure
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
|
import com.tangem.domain.offramp.repository.OfframpRepository
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use case for getting offramp (sell crypto) URL
|
||||||
|
*
|
||||||
|
* @property offrampRepository repository for offramp operations
|
||||||
|
*/
|
||||||
|
class GetOfframpUrlUseCase(
|
||||||
|
private val offrampRepository: OfframpRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
operator fun invoke(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrencyCode: String): Either<Error, String> =
|
||||||
|
either {
|
||||||
|
val walletAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
|
||||||
|
ensure(walletAddress != null) { Error.WalletAddressNotFound }
|
||||||
|
|
||||||
|
val url = offrampRepository.getOfframpUrl(
|
||||||
|
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||||
|
fiatCurrencyCode = appCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
)
|
||||||
|
ensure(url != null) { Error.UrlNotAvailable }
|
||||||
|
|
||||||
|
url
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Offramp use case errors */
|
||||||
|
sealed class Error {
|
||||||
|
/** Wallet address not found in currency status */
|
||||||
|
data object WalletAddressNotFound : Error()
|
||||||
|
|
||||||
|
/** Offramp URL is not available for this currency */
|
||||||
|
data object UrlNotAvailable : Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.tangem.domain.offramp.repository
|
||||||
|
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository for offramp (sell crypto) operations
|
||||||
|
*/
|
||||||
|
interface OfframpRepository {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get offramp (sell) URL for the given cryptocurrency
|
||||||
|
*
|
||||||
|
* @param cryptoCurrency crypto currency to sell
|
||||||
|
* @param fiatCurrencyCode fiat currency code (e.g., "USD", "EUR")
|
||||||
|
* @param walletAddress wallet address for the refund
|
||||||
|
* @return URL for offramp service or null if not available
|
||||||
|
*/
|
||||||
|
fun getOfframpUrl(cryptoCurrency: CryptoCurrency, fiatCurrencyCode: String, walletAddress: String): String?
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
package com.tangem.domain.offramp
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||||
|
import com.tangem.domain.models.network.NetworkAddress
|
||||||
|
import com.tangem.domain.offramp.repository.OfframpRepository
|
||||||
|
import io.mockk.clearMocks
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.junit.jupiter.api.TestInstance
|
||||||
|
|
||||||
|
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||||
|
class GetOfframpUrlUseCaseTest {
|
||||||
|
|
||||||
|
private val offrampRepository: OfframpRepository = mockk()
|
||||||
|
private val useCase = GetOfframpUrlUseCase(offrampRepository)
|
||||||
|
|
||||||
|
private val cryptoCurrency: CryptoCurrency = mockk()
|
||||||
|
private val appCurrencyCode = "USD"
|
||||||
|
private val walletAddress = "0x1234567890abcdef"
|
||||||
|
private val expectedUrl = "https://moonpay.com/sell?address=$walletAddress"
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun resetMocks() {
|
||||||
|
clearMocks(offrampRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `invoke should return url when wallet address and url are available`() {
|
||||||
|
// Arrange
|
||||||
|
val cryptoCurrencyStatus = createCryptoCurrencyStatus(walletAddress = walletAddress)
|
||||||
|
every {
|
||||||
|
offrampRepository.getOfframpUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyCode = appCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
)
|
||||||
|
} returns expectedUrl
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = useCase(cryptoCurrencyStatus, appCurrencyCode)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result.isRight()).isTrue()
|
||||||
|
assertThat(result.getOrNull()).isEqualTo(expectedUrl)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
offrampRepository.getOfframpUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyCode = appCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `invoke should return WalletAddressNotFound error when network address is null`() {
|
||||||
|
// Arrange
|
||||||
|
val cryptoCurrencyStatus = createCryptoCurrencyStatus(networkAddress = null)
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = useCase(cryptoCurrencyStatus, appCurrencyCode)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result.isLeft()).isTrue()
|
||||||
|
assertThat(result.leftOrNull()).isEqualTo(GetOfframpUrlUseCase.Error.WalletAddressNotFound)
|
||||||
|
|
||||||
|
verify(exactly = 0) {
|
||||||
|
offrampRepository.getOfframpUrl(any(), any(), any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `invoke should return UrlNotAvailable error when repository returns null`() {
|
||||||
|
// Arrange
|
||||||
|
val cryptoCurrencyStatus = createCryptoCurrencyStatus(walletAddress = walletAddress)
|
||||||
|
every {
|
||||||
|
offrampRepository.getOfframpUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyCode = appCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
)
|
||||||
|
} returns null
|
||||||
|
|
||||||
|
// Act
|
||||||
|
val result = useCase(cryptoCurrencyStatus, appCurrencyCode)
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assertThat(result.isLeft()).isTrue()
|
||||||
|
assertThat(result.leftOrNull()).isEqualTo(GetOfframpUrlUseCase.Error.UrlNotAvailable)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
offrampRepository.getOfframpUrl(
|
||||||
|
cryptoCurrency = cryptoCurrency,
|
||||||
|
fiatCurrencyCode = appCurrencyCode,
|
||||||
|
walletAddress = walletAddress,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createCryptoCurrencyStatus(
|
||||||
|
walletAddress: String? = null,
|
||||||
|
networkAddress: NetworkAddress? = null,
|
||||||
|
): CryptoCurrencyStatus {
|
||||||
|
val resolvedNetworkAddress = networkAddress ?: walletAddress?.let { address ->
|
||||||
|
mockk<NetworkAddress> {
|
||||||
|
every { defaultAddress } returns mockk {
|
||||||
|
every { value } returns address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val statusValue: CryptoCurrencyStatus.Value = mockk {
|
||||||
|
every { this@mockk.networkAddress } returns resolvedNetworkAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockk {
|
||||||
|
every { currency } returns cryptoCurrency
|
||||||
|
every { value } returns statusValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -349,6 +349,7 @@ include(":domain:manage-tokens")
|
||||||
include(":domain:manage-tokens:models")
|
include(":domain:manage-tokens:models")
|
||||||
include(":domain:onramp")
|
include(":domain:onramp")
|
||||||
include(":domain:onramp:models")
|
include(":domain:onramp:models")
|
||||||
|
include(":domain:offramp")
|
||||||
include(":domain:promo")
|
include(":domain:promo")
|
||||||
include(":domain:promo:models")
|
include(":domain:promo:models")
|
||||||
include(":domain:nft")
|
include(":domain:nft")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue