Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/src/main/java/cash/p/terminal/core/di/UseCaseModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import cash.p.terminal.core.usecase.GetMoneroWalletFilesNameUseCase
import cash.p.terminal.core.usecase.GetRestoreHeightForWalletUseCase
import cash.p.terminal.core.usecase.MoneroWalletUseCase
import cash.p.terminal.core.usecase.FetchSwapQuotesUseCase
import cash.p.terminal.core.usecase.IterativeExactOutSearch
import cash.p.terminal.core.usecase.RescanMoneroUseCase
import cash.p.terminal.core.usecase.RescanZcashUseCase
import cash.p.terminal.core.usecase.ResolvePayCoreNavigationUseCase
Expand Down Expand Up @@ -40,6 +41,7 @@ val useCaseModule = module {
singleOf(::UpdateSwapProviderTransactionsStatusUseCase)
singleOf(::SyncPendingMultiSwapUseCase)
factoryOf(::FetchSwapQuotesUseCase)
factoryOf(::IterativeExactOutSearch)
factoryOf(::ResolveTransactionItemUseCase)
factoryOf(::ResolvePayCoreNavigationUseCase)
factoryOf(::ValidateMoneroMnemonicUseCase)
Expand Down
148 changes: 116 additions & 32 deletions app/src/main/java/cash/p/terminal/core/usecase/FetchSwapQuotesUseCase.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package cash.p.terminal.core.usecase

import cash.p.terminal.modules.multiswap.SwapAmountDirection
import cash.p.terminal.modules.multiswap.SwapExecutionMode
import cash.p.terminal.modules.multiswap.SwapProviderQuote
import cash.p.terminal.modules.multiswap.sortedByBestAmountOut
import cash.p.terminal.modules.multiswap.providers.IExactOutSwapProvider
import cash.p.terminal.modules.multiswap.providers.IMultiSwapProvider
import cash.p.terminal.modules.multiswap.sortedByBest
import cash.p.terminal.wallet.Token
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
Expand All @@ -12,71 +15,152 @@ import timber.log.Timber
import java.math.BigDecimal
import kotlin.coroutines.cancellation.CancellationException

class FetchSwapQuotesUseCase {

class FetchSwapQuotesUseCase(
private val iterativeExactOutSearch: IterativeExactOutSearch,
) {
suspend operator fun invoke(
providers: List<IMultiSwapProvider>,
tokenIn: Token,
tokenOut: Token,
amountIn: BigDecimal,
amount: BigDecimal,
direction: SwapAmountDirection,
settings: Map<String, Any?> = emptyMap(),
onProviderError: ((IMultiSwapProvider, Throwable) -> Unit)? = null,
): List<SwapProviderQuote> = coroutineScope {
val supported = findSupportedProviders(providers, tokenIn, tokenOut)
val supported = findSupportedProviders(providers, tokenIn, tokenOut, direction)
if (supported.isEmpty()) return@coroutineScope emptyList()

fetchQuotes(supported, tokenIn, tokenOut, amountIn, settings, onProviderError)
.sortedByBestAmountOut()
supported.map { supportedProvider ->
async {
fetchQuote(
supportedProvider,
tokenIn,
tokenOut,
amount,
direction,
settings,
onProviderError,
)
}
}.awaitAll().filterNotNull().sortedByBest(direction)
}

suspend fun findSupportedProviders(
providers: List<IMultiSwapProvider>,
tokenIn: Token,
tokenOut: Token,
): List<IMultiSwapProvider> = coroutineScope {
direction: SwapAmountDirection = SwapAmountDirection.In,
): List<SupportedProvider> = coroutineScope {
providers.map { provider ->
async {
try {
withTimeoutOrNull(TIMEOUT_MS) {
if (provider.supports(tokenIn, tokenOut)) provider else null
withTimeoutOrNull(SUPPORTS_TIMEOUT_MS) {
resolveSupport(provider, tokenIn, tokenOut, direction)
}
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Timber.d(e, "supports error: ${provider.id}")
} catch (error: CancellationException) {
throw error
} catch (error: Throwable) {
Timber.d(error, "supports error: ${provider.id}")
null
}
}
}.awaitAll().filterNotNull()
}

private suspend fun fetchQuotes(
providers: List<IMultiSwapProvider>,
suspend fun invalidateSearchCache() {
iterativeExactOutSearch.invalidate()
}

private suspend fun resolveSupport(
provider: IMultiSwapProvider,
tokenIn: Token,
tokenOut: Token,
amountIn: BigDecimal,
direction: SwapAmountDirection,
): SupportedProvider? = when (direction) {
SwapAmountDirection.In -> provider.takeIf { it.supports(tokenIn, tokenOut) }
?.let { SupportedProvider(it, nativeExactOut = false) }

SwapAmountDirection.Out -> {
val exactOutProvider = provider as? IExactOutSwapProvider
when {
exactOutProvider?.supportsExactOut(tokenIn, tokenOut) == true ->
SupportedProvider(provider, nativeExactOut = true)

exactOutProvider != null -> null

provider.supports(tokenIn, tokenOut) ->
SupportedProvider(provider, nativeExactOut = false)

else -> null
}
}
}

private suspend fun fetchQuote(
supportedProvider: SupportedProvider,
tokenIn: Token,
tokenOut: Token,
amount: BigDecimal,
direction: SwapAmountDirection,
settings: Map<String, Any?>,
onProviderError: ((IMultiSwapProvider, Throwable) -> Unit)?,
) = coroutineScope {
providers.map { provider ->
async {
try {
withTimeoutOrNull(TIMEOUT_MS) {
val quote = provider.fetchQuote(tokenIn, tokenOut, amountIn, settings)
SwapProviderQuote(provider = provider, swapQuote = quote)
): SwapProviderQuote? {
val provider = supportedProvider.provider
return try {
withTimeoutOrNull(timeout(direction)) {
when {
direction == SwapAmountDirection.In -> SwapProviderQuote(
provider = provider,
swapQuote = provider.fetchQuote(tokenIn, tokenOut, amount, settings),
)

supportedProvider.nativeExactOut -> {
val exactOutProvider = provider as IExactOutSwapProvider
SwapProviderQuote(
provider = provider,
swapQuote = exactOutProvider.fetchQuoteExactOut(
tokenIn,
tokenOut,
amount,
settings,
),
executionMode = SwapExecutionMode.NativeExactOut,
amountOutAccuracy = exactOutProvider.exactOutAccuracy,
)
}
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
onProviderError?.invoke(provider, e)
?: Timber.d(e, "fetchQuoteError: ${provider.id}")
null

else -> iterativeExactOutSearch.search(
provider,
tokenIn,
tokenOut,
amount,
settings,
onProviderError,
)
}
}
}.awaitAll().filterNotNull()
} catch (error: CancellationException) {
throw error
} catch (error: Throwable) {
onProviderError?.invoke(provider, error)
?: Timber.d(error, "fetchQuoteError: ${provider.id}")
null
}
}

private fun timeout(direction: SwapAmountDirection): Long = when (direction) {
SwapAmountDirection.In -> EXACT_IN_TIMEOUT_MS
SwapAmountDirection.Out -> EXACT_OUT_TIMEOUT_MS
}

data class SupportedProvider(
val provider: IMultiSwapProvider,
val nativeExactOut: Boolean,
)

private companion object {
const val TIMEOUT_MS = 5000L
const val SUPPORTS_TIMEOUT_MS = 5_000L
const val EXACT_IN_TIMEOUT_MS = 5_000L
const val EXACT_OUT_TIMEOUT_MS = 12_000L
}
}
Loading
Loading