Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix error handling for LiFi swap quote responses #1353

Merged
merged 9 commits into from
Nov 18, 2024
63 changes: 52 additions & 11 deletions data/src/main/kotlin/com/vultisig/wallet/data/api/LiFiChainApi.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
package com.vultisig.wallet.data.api

import com.vultisig.wallet.data.api.models.LiFiSwapQuoteError
import com.vultisig.wallet.data.api.models.LiFiSwapQuoteJson
import com.vultisig.wallet.data.api.models.LiFiSwapQuoteDeserialized
import com.vultisig.wallet.data.api.models.THORChainSwapQuoteDeserialized
import com.vultisig.wallet.data.api.models.THORChainSwapQuoteError
import com.vultisig.wallet.data.utils.LiFiSwapQuoteResponseSerializer
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.http.HttpStatusCode
import io.ktor.http.isSuccess
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import javax.inject.Inject

interface LiFiChainApi {
Expand All @@ -16,11 +25,13 @@ interface LiFiChainApi {
fromAmount: String,
fromAddress: String,
toAddress: String,
) : LiFiSwapQuoteJson
) : LiFiSwapQuoteDeserialized
}

internal class LiFiChainApiImpl @Inject constructor(
private val httpClient: HttpClient,
private val liFiSwapQuoteResponseSerializer: LiFiSwapQuoteResponseSerializer,
private val json: Json,
) : LiFiChainApi {
override suspend fun getSwapQuote(
fromChain: String,
Expand All @@ -30,14 +41,44 @@ internal class LiFiChainApiImpl @Inject constructor(
fromAmount: String,
fromAddress: String,
toAddress: String,
): LiFiSwapQuoteJson = httpClient
.get("https://li.quest/v1/quote") {
parameter("fromChain", fromChain)
parameter("toChain", toChain)
parameter("fromToken", fromToken)
parameter("toToken", toToken)
parameter("fromAmount", fromAmount)
parameter("fromAddress", fromAddress)
parameter("toAddress", toAddress)
}.body()
): LiFiSwapQuoteDeserialized {
try {
val response = httpClient
.get("https://li.quest/v1/quote") {
parameter("fromChain", fromChain)
parameter("toChain", toChain)
parameter("fromToken", fromToken)
parameter("toToken", toToken)
parameter("fromAmount", fromAmount)
parameter("fromAddress", fromAddress)
parameter("toAddress", toAddress)
}
if (!response.status.isSuccess()) {
if (response.status == HttpStatusCode.NotFound) {
return LiFiSwapQuoteDeserialized.Error(
json.decodeFromString(
LiFiSwapQuoteError.serializer(),
response.body<String>()
)
)
}
return LiFiSwapQuoteDeserialized.Error(
LiFiSwapQuoteError(
response.status.description
)
)
}
val responseRawString = response.body<String>()
return json.decodeFromString(
liFiSwapQuoteResponseSerializer,
responseRawString
)
} catch (e: Exception) {
return LiFiSwapQuoteDeserialized.Error(
LiFiSwapQuoteError(
e.message ?: "Unknown error"
)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ package com.vultisig.wallet.data.api.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

sealed interface LiFiSwapQuoteDeserialized {
data class Result(val data: LiFiSwapQuoteJson) : LiFiSwapQuoteDeserialized
data class Error(val error: LiFiSwapQuoteError) : LiFiSwapQuoteDeserialized
}


@Serializable
data class LiFiSwapQuoteError(
@SerialName("message")
val message: String
)


@Serializable
data class LiFiSwapQuoteJson(
@SerialName("estimate")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.vultisig.wallet.data.api.MayaChainApi
import com.vultisig.wallet.data.api.OneInchApi
import com.vultisig.wallet.data.api.ThorChainApi
import com.vultisig.wallet.data.api.errors.SwapException
import com.vultisig.wallet.data.api.models.LiFiSwapQuoteDeserialized
import com.vultisig.wallet.data.api.models.OneInchSwapQuoteJson
import com.vultisig.wallet.data.api.models.OneInchSwapTxJson
import com.vultisig.wallet.data.api.models.THORChainSwapQuoteDeserialized
Expand Down Expand Up @@ -192,7 +193,7 @@ internal class SwapQuoteRepositoryImpl @Inject constructor(
val toToken =
dstToken.contractAddress.ifEmpty { dstToken.ticker }

val liFiQuote = liFiChainApi.getSwapQuote(
val liFiQuoteResponse = liFiChainApi.getSwapQuote(
fromChain = srcToken.chain.oneInchChainId().toString(),
toChain = dstToken.chain.oneInchChainId().toString(),
fromToken = fromToken,
Expand All @@ -201,21 +202,31 @@ internal class SwapQuoteRepositoryImpl @Inject constructor(
fromAddress = srcAddress,
toAddress = dstAddress,
)
liFiQuote.message?.let { throw SwapException.handleSwapException(it) }

return OneInchSwapQuoteJson(
dstAmount = liFiQuote.estimate.toAmount,
tx = OneInchSwapTxJson(
from = liFiQuote.transactionRequest.from,
to = liFiQuote.transactionRequest.to,
data = liFiQuote.transactionRequest.data,
gas = liFiQuote.transactionRequest.gasLimit.substring(startIndex = 2).hexToLong(),
value = liFiQuote.transactionRequest.value.substring(startIndex = 2).hexToLong()
.toString(),
gasPrice = liFiQuote.transactionRequest.gasPrice.substring(startIndex = 2)
.hexToLong().toString(),
)
)

when (liFiQuoteResponse) {
is LiFiSwapQuoteDeserialized.Error ->
throw SwapException.handleSwapException(liFiQuoteResponse.error.message)

is LiFiSwapQuoteDeserialized.Result -> {
val liFiQuote = liFiQuoteResponse.data
liFiQuote.message?.let { throw SwapException.handleSwapException(it) }
return OneInchSwapQuoteJson(
dstAmount = liFiQuote.estimate.toAmount,
tx = OneInchSwapTxJson(
from = liFiQuote.transactionRequest.from,
to = liFiQuote.transactionRequest.to,
data = liFiQuote.transactionRequest.data,
gas = liFiQuote.transactionRequest.gasLimit.substring(startIndex = 2)
.hexToLong(),
value = liFiQuote.transactionRequest.value.substring(startIndex = 2)
.hexToLong()
.toString(),
gasPrice = liFiQuote.transactionRequest.gasPrice.substring(startIndex = 2)
.hexToLong().toString(),
)
)
}
}
}

private val Coin.streamingInterval: String
Expand Down
30 changes: 30 additions & 0 deletions data/src/main/kotlin/com/vultisig/wallet/data/utils/Serializer.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.vultisig.wallet.data.utils

import com.vultisig.wallet.data.api.models.KeysignResponseSerializable
import com.vultisig.wallet.data.api.models.LiFiSwapQuoteError
import com.vultisig.wallet.data.api.models.LiFiSwapQuoteJson
import com.vultisig.wallet.data.api.models.LiFiSwapQuoteDeserialized
import com.vultisig.wallet.data.api.models.SplTokenJson
import com.vultisig.wallet.data.api.models.SplTokenResponseJson
import com.vultisig.wallet.data.api.models.THORChainSwapQuote
Expand All @@ -26,6 +29,7 @@ import kotlinx.serialization.json.jsonObject
import java.math.BigDecimal
import java.math.BigInteger
import javax.inject.Inject
import javax.inject.Singleton

interface DefaultSerializer<T> : KSerializer<T> {
override fun serialize(encoder: Encoder, value: T) {
Expand Down Expand Up @@ -117,6 +121,32 @@ class ThorChainSwapQuoteResponseJsonSerializerImpl @Inject constructor(private v
}
}

interface LiFiSwapQuoteResponseSerializer : DefaultSerializer<LiFiSwapQuoteDeserialized>

class LiFiSwapQuoteResponseSerializerImpl @Inject constructor(private val json: Json) :
LiFiSwapQuoteResponseSerializer{
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("LiFiSwapQuoteResponseSerializer")

override fun deserialize(decoder: Decoder): LiFiSwapQuoteDeserialized {
val input = decoder as JsonDecoder
val jsonObject = input.decodeJsonElement().jsonObject

return if (jsonObject.containsKey("estimate")) {
LiFiSwapQuoteDeserialized.Result(
json.decodeFromJsonElement<LiFiSwapQuoteJson>(jsonObject)
)
} else {
LiFiSwapQuoteDeserialized.Error(
json.decodeFromJsonElement<LiFiSwapQuoteError>(jsonObject)
)
}
}

override fun serialize(encoder: Encoder, value: LiFiSwapQuoteDeserialized) {
throw UnsupportedOperationException("Serialization is not required")
}
}
interface KeysignResponseSerializer : DefaultSerializer<tss.KeysignResponse>

class KeysignResponseSerializerImpl @Inject constructor() : KeysignResponseSerializer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,9 @@ internal interface SerializerModule {
fun bindCosmosThorChainResponseSerializer(
impl: CosmosThorChainResponseSerializerImpl,
): CosmosThorChainResponseSerializer

@Binds
fun bindLiFiSwapQuoteResponseSerializer(
impl: LiFiSwapQuoteResponseSerializerImpl,
): LiFiSwapQuoteResponseSerializer
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Binds
fun bindLiFiSwapQuoteResponseSerializer(
impl: LiFiSwapQuoteResponseSerializerImpl,
): LiFiSwapQuoteResponseSerializer
@Binds
@Singleton
fun bindLiFiSwapQuoteResponseSerializer(
impl: LiFiSwapQuoteResponseSerializerImpl,
): LiFiSwapQuoteResponseSerializer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}