diff --git a/app/build.gradle b/app/build.gradle index e747f972..1c14dc80 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,4 +56,5 @@ dependencies { implementation project(':ethereumkit') implementation project(':erc20kit') implementation project(':uniswapkit') + implementation project(':oneinchkit') } diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/TransactionData.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/TransactionData.kt index 0dbdcdef..9c01f9e0 100644 --- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/TransactionData.kt +++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/models/TransactionData.kt @@ -1,9 +1,28 @@ package io.horizontalsystems.ethereumkit.models +import io.horizontalsystems.ethereumkit.core.toHexString import java.math.BigInteger +import java.util.* data class TransactionData( val to: Address, val value: BigInteger, val input: ByteArray -) +) { + override fun equals(other: Any?): Boolean { + return when { + this === other -> true + other is TransactionData -> to == other.to && value == other.value && input.contentEquals(other.input) + else -> false + } + } + + override fun hashCode(): Int { + return Objects.hash(to, value, input) + } + + override fun toString(): String { + return "TransactionData {to: ${to.hex}, value: $value, input: ${input.toHexString()}}" + } + +} diff --git a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/network/GsonTypeAdapters.kt b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/network/GsonTypeAdapters.kt index 157f7a80..23aa2dd6 100644 --- a/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/network/GsonTypeAdapters.kt +++ b/ethereumkit/src/main/java/io/horizontalsystems/ethereumkit/network/GsonTypeAdapters.kt @@ -14,12 +14,13 @@ import java.lang.reflect.Type import java.math.BigInteger import java.util.* -class BigIntegerTypeAdapter : TypeAdapter() { +class BigIntegerTypeAdapter(private val isHex: Boolean = true) : TypeAdapter() { override fun write(writer: JsonWriter, value: BigInteger?) { if (value == null) { writer.nullValue() } else { - writer.value(value.toHexString()) + val stringValue = if (isHex) value.toHexString() else value.toString() + writer.value(stringValue) } } @@ -28,7 +29,8 @@ class BigIntegerTypeAdapter : TypeAdapter() { reader.nextNull() return null } - return reader.nextString().hexStringToBigIntegerOrNull() + val stringValue = reader.nextString() + return if (isHex) stringValue.hexStringToBigIntegerOrNull() else BigInteger(stringValue) } } diff --git a/oneinchkit/build.gradle b/oneinchkit/build.gradle index 104959da..e9f9ac51 100644 --- a/oneinchkit/build.gradle +++ b/oneinchkit/build.gradle @@ -42,4 +42,16 @@ dependencies { testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + + implementation 'io.reactivex.rxjava2:rxjava:2.2.19' + implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + + implementation 'com.squareup.retrofit2:adapter-rxjava2:2.8.1' + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation 'com.squareup.okhttp3:logging-interceptor:4.7.2' + implementation 'com.squareup.retrofit2:converter-scalars:2.9.0' + implementation 'com.google.code.gson:gson:2.8.6' + + api project(':ethereumkit') } diff --git a/oneinchkit/src/main/java/io/horizontalsystems/oneinchkit/OneInchKit.kt b/oneinchkit/src/main/java/io/horizontalsystems/oneinchkit/OneInchKit.kt new file mode 100644 index 00000000..3593d185 --- /dev/null +++ b/oneinchkit/src/main/java/io/horizontalsystems/oneinchkit/OneInchKit.kt @@ -0,0 +1,143 @@ +package io.horizontalsystems.oneinchkit + +import com.google.gson.annotations.SerializedName +import io.horizontalsystems.ethereumkit.core.EthereumKit +import io.horizontalsystems.ethereumkit.core.EthereumKit.NetworkType +import io.horizontalsystems.ethereumkit.core.toHexString +import io.horizontalsystems.ethereumkit.models.Address +import io.reactivex.Single +import java.math.BigInteger +import java.util.* + +class OneInchKit( + private val evmKit: EthereumKit, + private val service: OneInchService +) { + + val smartContractAddress: Address = when (evmKit.networkType) { + NetworkType.EthMainNet -> Address("0x11111112542d85b3ef69ae05771c2dccff4faa26") + NetworkType.BscMainNet -> Address("0x11111112542d85b3ef69ae05771c2dccff4faa26") + else -> throw IllegalArgumentException("Invalid NetworkType: $evmKit.networkType") + } + + fun getApproveCallDataAsync(tokenAddress: Address, amount: BigInteger): Single { + return service.getApproveCallDataAsync(tokenAddress, amount) + } + + fun getQuoteAsync( + fromToken: Address, + toToken: Address, + amount: BigInteger, + protocols: List? = null, + gasPrice: Long? = null, + complexityLevel: Int? = null, + connectorTokens: List? = null, + gasLimit: Long? = null, + mainRouteParts: Int? = null, + parts: Int? = null + ): Single { + return service.getQuoteAsync(fromToken, toToken, amount, protocols, gasPrice, complexityLevel, connectorTokens, gasLimit, mainRouteParts, parts) + } + + fun getSwapAsync( + fromToken: Address, + toToken: Address, + amount: BigInteger, + slippagePercentage: Float, + protocols: List? = null, + recipient: Address? = null, + gasPrice: Long? = null, + burnChi: Boolean = false, + complexityLevel: Int? = null, + connectorTokens: List? = null, + allowPartialFill: Boolean = false, + gasLimit: Long? = null, + parts: Int? = null, + mainRouteParts: Int? = null + ): Single { + return service.getSwapAsync(fromToken, toToken, amount, evmKit.receiveAddress, slippagePercentage, protocols, recipient, gasPrice, burnChi, complexityLevel, connectorTokens, allowPartialFill, gasLimit, parts, mainRouteParts) + } + + companion object { + + fun getInstance(evmKit: EthereumKit): OneInchKit { + val service = OneInchService(evmKit.networkType) + return OneInchKit(evmKit, service) + } + + } + +} + +data class Token( + val symbol: String, + val name: String, + val decimals: Int, + val address: String, + val logoURI: String +) + +data class Quote( + val fromToken: Token, + val toToken: Token, + val fromTokenAmount: BigInteger, + val toTokenAmount: BigInteger, + @SerializedName("protocols") val route: List, + val estimatedGas: Long +) { + override fun toString(): String { + return "Quote {fromToken: ${fromToken.name}, toToken: ${toToken.name}, fromTokenAmount: $fromTokenAmount, toTokenAmount: $toTokenAmount}" + } +} + +data class SwapTransaction( + val from: Address, + val to: Address, + val data: ByteArray, + val value: BigInteger, + val gasPrice: Long, + @SerializedName("gas") val gasLimit: Long +) { + override fun toString(): String { + return "SwapTransaction {\nfrom: ${from.hex}, \nto: ${to.hex}, \ndata: ${data.toHexString()}, \nvalue: $value, \ngasPrice: $gasPrice, \ngasLimit: $gasLimit\n}" + } +} + +data class Swap( + val fromToken: Token, + val toToken: Token, + val fromTokenAmount: BigInteger, + val toTokenAmount: BigInteger, + @SerializedName("protocols") val route: List, + @SerializedName("tx") val transaction: SwapTransaction +) { + override fun toString(): String { + return "Swap {\nfromToken: ${fromToken.name}, \ntoToken: ${toToken.name}, \nfromTokenAmount: $fromTokenAmount, \ntoTokenAmount: $toTokenAmount, \ntx: $transaction\n}" + } +} + +data class ApproveCallData( + val data: ByteArray, + val gasPrice: Long, + val to: Address, + val value: BigInteger +) { + override fun equals(other: Any?): Boolean { + return when { + this === other -> true + other is ApproveCallData -> to == other.to && value == other.value && data.contentEquals(other.data) + else -> false + } + } + + override fun hashCode(): Int { + return Objects.hash(to, value, data) + } + + override fun toString(): String { + return "ApproveCallData {\nto: ${to.hex}, \nvalue: $value, \ndata: ${data.toHexString()}\n}" + } +} + +data class Spender(val address: Address) + diff --git a/oneinchkit/src/main/java/io/horizontalsystems/oneinchkit/OneInchService.kt b/oneinchkit/src/main/java/io/horizontalsystems/oneinchkit/OneInchService.kt new file mode 100644 index 00000000..6bf5d303 --- /dev/null +++ b/oneinchkit/src/main/java/io/horizontalsystems/oneinchkit/OneInchService.kt @@ -0,0 +1,142 @@ +package io.horizontalsystems.oneinchkit + +import com.google.gson.GsonBuilder +import io.horizontalsystems.ethereumkit.core.EthereumKit.NetworkType +import io.horizontalsystems.ethereumkit.models.Address +import io.horizontalsystems.ethereumkit.network.* +import io.reactivex.Single +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.GET +import retrofit2.http.Query +import java.math.BigInteger +import java.util.logging.Logger + +class OneInchService( + networkType: NetworkType +) { + private val logger = Logger.getLogger("OneInchService") + private val url = "https://api.1inch.exchange/v3.0/${networkType.getNetwork().id}/" + private val service: OneInchServiceApi + + init { + val loggingInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger { + override fun log(message: String) { + logger.info(message) + } + }).setLevel(HttpLoggingInterceptor.Level.BODY) + + val httpClient = OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + + val gson = GsonBuilder() + .setLenient() + .registerTypeAdapter(BigInteger::class.java, BigIntegerTypeAdapter(isHex = false)) + .registerTypeAdapter(Long::class.java, LongTypeAdapter()) + .registerTypeAdapter(Int::class.java, IntTypeAdapter()) + .registerTypeAdapter(ByteArray::class.java, ByteArrayTypeAdapter()) + .registerTypeAdapter(Address::class.java, AddressTypeAdapter()) + .create() + + val retrofit = Retrofit.Builder() + .baseUrl(url) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(GsonConverterFactory.create(gson)) + .client(httpClient.build()) + .build() + + service = retrofit.create(OneInchServiceApi::class.java) + } + + fun getApproveCallDataAsync(tokenAddress: Address, amount: BigInteger): Single { + return service.getApproveCallData(tokenAddress.hex, amount) + } + + fun getQuoteAsync( + fromToken: Address, + toToken: Address, + amount: BigInteger, + protocols: List? = null, + gasPrice: Long? = null, + complexityLevel: Int? = null, + connectorTokens: List? = null, + gasLimit: Long? = null, + mainRouteParts: Int? = null, + parts: Int? = null + ): Single { + return service.getQuote(fromToken.hex, toToken.hex, amount, protocols?.joinToString(","), gasPrice, complexityLevel, connectorTokens?.joinToString(","), gasLimit, parts, mainRouteParts) + } + + fun getSwapAsync( + fromTokenAddress: Address, + toTokenAddress: Address, + amount: BigInteger, + fromAddress: Address, + slippagePercentage: Float, + protocols: List? = null, + recipient: Address? = null, + gasPrice: Long? = null, + burnChi: Boolean? = null, + complexityLevel: Int? = null, + connectorTokens: List? = null, + allowPartialFill: Boolean? = null, + gasLimit: Long? = null, + parts: Int? = null, + mainRouteParts: Int? = null + ): Single { + return service.getSwap(fromTokenAddress.hex, toTokenAddress.hex, amount, fromAddress.hex, slippagePercentage, protocols?.joinToString(","), recipient?.hex, gasPrice, burnChi, complexityLevel, connectorTokens?.joinToString(","), allowPartialFill, gasLimit, parts, mainRouteParts) + } + + private interface OneInchServiceApi { + + @GET("approve/calldata") + fun getApproveCallData( + @Query("tokenAddress") tokenAddress: String, + @Query("amount") amount: BigInteger? = null, + @Query("infinity") infinity: Boolean? = null + ): Single + + @GET("approve/spender") + fun getApproveSpender(): Single + + @GET("quote") + fun getQuote( + @Query("fromTokenAddress") fromTokenAddress: String, + @Query("toTokenAddress") toTokenAddress: String, + @Query("amount") amount: BigInteger, + @Query("protocols") protocols: String? = null, + @Query("gasPrice") gasPrice: Long? = null, + @Query("complexityLevel") complexityLevel: Int? = null, + @Query("connectorTokens") connectorTokens: String? = null, + @Query("gasLimit") gasLimit: Long? = null, + @Query("parts") parts: Int? = null, + @Query("mainRouteParts") mainRouteParts: Int? = null + ): Single + + + @GET("swap") + fun getSwap( + @Query("fromTokenAddress") fromTokenAddress: String, + @Query("toTokenAddress") toTokenAddress: String, + @Query("amount") amount: BigInteger, + @Query("fromAddress") fromAddress: String, + @Query("slippage") slippagePercentage: Float, + @Query("protocols") protocols: String? = null, + @Query("destReceiver") recipient: String? = null, + @Query("gasPrice") gasPrice: Long? = null, + @Query("burnChi") burnChi: Boolean? = null, + @Query("complexityLevel ") complexityLevel: Int? = null, + @Query("connectorTokens") connectorTokens: String? = null, + @Query("allowPartialFill") allowPartialFill: Boolean? = null, + @Query("gasLimit") gasLimit: Long? = null, + @Query("parts") parts: Int? = null, + @Query("mainRouteParts") mainRouteParts: Int? = null + ): Single + + + } + +}