diff --git a/imx-core-sdk-kotlin-jvm/src/main/kotlin/com/immutable/sdk/ImmutableConfig.kt b/imx-core-sdk-kotlin-jvm/src/main/kotlin/com/immutable/sdk/ImmutableConfig.kt index 5ce74f6..a778d45 100644 --- a/imx-core-sdk-kotlin-jvm/src/main/kotlin/com/immutable/sdk/ImmutableConfig.kt +++ b/imx-core-sdk-kotlin-jvm/src/main/kotlin/com/immutable/sdk/ImmutableConfig.kt @@ -3,24 +3,18 @@ package com.immutable.sdk internal object ImmutableConfig { private object Production { const val PUBLIC_API_URL = "https://api.x.immutable.com" - const val MOONPAY_BUY_CRYPTO_URL = "https://buy.moonpay.io" - const val MOONPAY_API_KEY = "pk_live_lgGxv3WyWjnWff44ch4gmolN0953" const val CORE_CONTRACT_ADDRESS = "0x5FDCCA53617f4d2b9134B29090C87D01058e27e9" const val REGISTRATION_CONTRACT_ADDRESS = "0x72a06bf2a1CE5e39cBA06c0CAb824960B587d64c" } private object Sandbox { const val PUBLIC_API_URL = "https://api.sandbox.x.immutable.com" - const val MOONPAY_BUY_CRYPTO_URL = "https://buy-staging.moonpay.io" - const val MOONPAY_API_KEY = "pk_test_nGdsu1IBkjiFzmEvN8ddf4gM9GNy5Sgz" const val CORE_CONTRACT_ADDRESS = "0x7917eDb51ecD6CdB3F9854c3cc593F33de10c623" const val REGISTRATION_CONTRACT_ADDRESS = "0x1C97Ada273C9A52253f463042f29117090Cd7D83" } private object Ropsten { const val PUBLIC_API_URL = "https://api.ropsten.x.immutable.com" - const val MOONPAY_BUY_CRYPTO_URL = "https://buy-staging.moonpay.io" - const val MOONPAY_API_KEY = "pk_test_nGdsu1IBkjiFzmEvN8ddf4gM9GNy5Sgz" const val CORE_CONTRACT_ADDRESS = "0x4527BE8f31E2ebFbEF4fCADDb5a17447B27d2aef" const val REGISTRATION_CONTRACT_ADDRESS = "0x6C21EC8DE44AE44D0992ec3e2d9f1aBb6207D864" } @@ -31,18 +25,6 @@ internal object ImmutableConfig { ImmutableXBase.Sandbox -> Sandbox.PUBLIC_API_URL } - fun getMoonpayApiKey(base: ImmutableXBase) = when (base) { - ImmutableXBase.Production -> Production.MOONPAY_API_KEY - ImmutableXBase.Ropsten -> Ropsten.MOONPAY_API_KEY - ImmutableXBase.Sandbox -> Sandbox.MOONPAY_API_KEY - } - - fun getBuyCryptoUrl(base: ImmutableXBase) = when (base) { - ImmutableXBase.Production -> Production.MOONPAY_BUY_CRYPTO_URL - ImmutableXBase.Ropsten -> Ropsten.MOONPAY_BUY_CRYPTO_URL - ImmutableXBase.Sandbox -> Sandbox.MOONPAY_BUY_CRYPTO_URL - } - fun getCoreContractAddress(base: ImmutableXBase) = when (base) { ImmutableXBase.Production -> Production.CORE_CONTRACT_ADDRESS ImmutableXBase.Ropsten -> Ropsten.CORE_CONTRACT_ADDRESS diff --git a/imx-core-sdk-kotlin-jvm/src/main/kotlin/com/immutable/sdk/ImmutableX.kt b/imx-core-sdk-kotlin-jvm/src/main/kotlin/com/immutable/sdk/ImmutableX.kt index 66f79be..5c34bca 100644 --- a/imx-core-sdk-kotlin-jvm/src/main/kotlin/com/immutable/sdk/ImmutableX.kt +++ b/imx-core-sdk-kotlin-jvm/src/main/kotlin/com/immutable/sdk/ImmutableX.kt @@ -1,7 +1,6 @@ package com.immutable.sdk import com.google.common.annotations.VisibleForTesting -import com.immutable.sdk.Constants.DEFAULT_MOONPAY_COLOUR_CODE import com.immutable.sdk.api.AssetsApi import com.immutable.sdk.api.BalancesApi import com.immutable.sdk.api.CollectionsApi @@ -1173,26 +1172,6 @@ class ImmutableX( ): CompletableFuture = com.immutable.sdk.workflows.transfer(transfers, signer, starkSigner) - /** - * Gets a URL to MoonPay that provides a service for buying crypto directly on Immutable in exchange for fiat. - * - * It is recommended to open this URL in a Chrome Custom Tab. - * - * @param signer represents the users L1 wallet to get the address - * @param colourCodeHex The color code in hex (e.g. #00818e) for the Moon pay widget main color. It is used for buttons, - * links and highlighted text. - * @throws Throwable if any error occurs - */ - fun buyCrypto( - signer: Signer, - colourCodeHex: String = DEFAULT_MOONPAY_COLOUR_CODE - ): CompletableFuture = - com.immutable.sdk.workflows.buyCrypto( - base = base, - signer = signer, - colourCodeHex = colourCodeHex - ) - @Suppress("TooGenericExceptionCaught") private fun apiCall(callName: String, call: () -> T): T { try { diff --git a/imx-core-sdk-kotlin-jvm/src/main/kotlin/com/immutable/sdk/workflows/BuyCryptoWorkflow.kt b/imx-core-sdk-kotlin-jvm/src/main/kotlin/com/immutable/sdk/workflows/BuyCryptoWorkflow.kt deleted file mode 100644 index 1bbdc9d..0000000 --- a/imx-core-sdk-kotlin-jvm/src/main/kotlin/com/immutable/sdk/workflows/BuyCryptoWorkflow.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.immutable.sdk.workflows - -import com.google.common.annotations.VisibleForTesting -import com.immutable.sdk.ImmutableConfig -import com.immutable.sdk.ImmutableXBase -import com.immutable.sdk.Signer -import com.immutable.sdk.api.UsersApi -import com.immutable.sdk.extensions.getJson -import com.immutable.sdk.extensions.getJsonArray -import com.immutable.sdk.extensions.toURLEncodedString -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import org.json.JSONObject -import org.openapitools.client.infrastructure.ClientException -import java.net.HttpURLConnection -import java.util.concurrent.CompletableFuture - -@VisibleForTesting -internal const val ID = "id" - -@VisibleForTesting -internal const val SIGNATURE = "signature" - -@VisibleForTesting -internal const val CURRENCY_CODE = "currency_code" - -private const val ENCODED_EQUAL_SIGN = "%3D" -private const val HASH_SIGN = "#" - -private const val MOONPAY = "moonpay" - -@VisibleForTesting -internal const val MOONPAY_BUY_URL = "%s/?%s&signature=%s" -private const val GET_TRANSACTION_ID_URL_PATH = "v2/exchanges" -private const val MOONPAY_GET_SIGNATURE_URL_PATH = "v2/moonpay/sign-url" -private const val GET_CURRENCIES = "v2/exchanges/currencies/fiat-to-crypto" - -private const val APPLICATION_JSON = "application/json" - -private const val HEADER_ACCEPT = "Accept" -private const val HEADER_CONTENT_TYPE = "Content-Type" - -/** - * Gets a URL to MoonPay that provides a service for buying crypto directly on Immutable in exchange for fiat. - * - * @throws Exception if anything error occurs - */ -@Suppress("TooGenericExceptionCaught", "LongParameterList") -internal fun buyCrypto( - base: ImmutableXBase, - signer: Signer, - client: OkHttpClient = OkHttpClient.Builder().build(), - colourCodeHex: String, - usersApi: UsersApi = UsersApi() -): CompletableFuture { - val future = CompletableFuture() - CompletableFuture.runAsync { - try { - val address = signer.getAddress().get() - check(isWalletRegistered(address, usersApi)) { - "Wallet is not registered. Call ImmutableXCore.registerOffChain() " + - "to register your wallet." - } - - val transactionId = - getTransactionId(walletAddress = address, base = base, client = client) - - // Get supported fiat to crypto currencies - val currencies = - getSupportedCurrencies(walletAddress = address, base = base, client = client) - future.complete( - getBuyCryptoUrl( - walletAddress = address, - transactionId = transactionId, - currencies = currencies, - base = base, - client = client, - colourCodeHex = colourCodeHex - ) - ) - } catch (e: Exception) { - future.completeExceptionally(e.cause ?: e) - } - } - return future -} - -@Suppress("TooGenericExceptionCaught", "InstanceOfCheckForException") -private fun isWalletRegistered(address: String, userApi: UsersApi): Boolean { - return try { - userApi.getUsers(address).accounts.isNotEmpty() - } catch (e: Exception) { - if (e is ClientException && e.statusCode == HttpURLConnection.HTTP_NOT_FOUND) false - else throw e - } -} - -private fun getTransactionId( - walletAddress: String, - base: ImmutableXBase, - client: OkHttpClient -): Long { - val response = post( - urlPath = GET_TRANSACTION_ID_URL_PATH, - jsonBody = Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build().adapter(GetTransactionIdRequest::class.java) - .toJson(GetTransactionIdRequest(walletAddress = walletAddress, provider = MOONPAY)), - base = base, - client = client - ) - return response.getLong(ID) -} - -private fun getSupportedCurrencies( - walletAddress: String, - base: ImmutableXBase, - client: OkHttpClient -): Map { - val request: Request = Request.Builder() - .url("${ImmutableConfig.getPublicApiUrl(base)}/$GET_CURRENCIES") - .get() - .addHeader(HEADER_ACCEPT, APPLICATION_JSON) - .addHeader(HEADER_CONTENT_TYPE, APPLICATION_JSON) - .build() - val response = client.newCall(request).execute() - val currencies = hashMapOf() - response.getJsonArray()?.let { array -> - for (i in 0 until array.length()) { - val code = array.getJSONObject(i).getString(CURRENCY_CODE) - currencies[code] = walletAddress - } - } - return currencies -} - -@Suppress("LongParameterList") -private fun getBuyCryptoUrl( - walletAddress: String, - transactionId: Long, - currencies: Map, - base: ImmutableXBase, - client: OkHttpClient, - colourCodeHex: String -): String { - val requestParams = "apiKey=${ImmutableConfig.getMoonpayApiKey(base)}" + - "&baseCurrencyCode=usd" + - "&colorCode=%23${colourCodeHex.replace(HASH_SIGN, "")}" + - "&externalTransactionId=$transactionId" + - "&walletAddress=$walletAddress" + - "&walletAddresses=" + - JSONObject(currencies).toURLEncodedString() - val response = post( - urlPath = MOONPAY_GET_SIGNATURE_URL_PATH, - jsonBody = Moshi.Builder() - .add(KotlinJsonAdapterFactory()) - .build() - .adapter(GetSignedMoonpayRequest::class.java) - .toJson(GetSignedMoonpayRequest(request = requestParams)), - base = base, - client = client - ) - val signature = response.getString(SIGNATURE) - // Remove equal sign at the end - .replace(ENCODED_EQUAL_SIGN, "") - return String.format( - MOONPAY_BUY_URL, - ImmutableConfig.getBuyCryptoUrl(base), - requestParams, - signature - ) -} - -@Suppress("TooGenericExceptionCaught") -private fun post( - urlPath: String, - jsonBody: String, - base: ImmutableXBase, - client: OkHttpClient -): JSONObject { - val request: Request = Request.Builder() - .url("${ImmutableConfig.getPublicApiUrl(base)}/$urlPath") - .post(jsonBody.toRequestBody()) - .addHeader(HEADER_ACCEPT, APPLICATION_JSON) - .addHeader(HEADER_CONTENT_TYPE, APPLICATION_JSON) - .build() - val response = client.newCall(request).execute() - return response.getJson() ?: JSONObject() -} - -@JsonClass(generateAdapter = true) -internal data class GetSignedMoonpayRequest(val request: String) - -@JsonClass(generateAdapter = true) -internal data class GetTransactionIdRequest( - @Json(name = "wallet_address") - val walletAddress: String, - val provider: String -) diff --git a/imx-core-sdk-kotlin-jvm/src/test/kotlin/com/immutable/sdk/ImmutableXTest.kt b/imx-core-sdk-kotlin-jvm/src/test/kotlin/com/immutable/sdk/ImmutableXTest.kt index dbaadf4..c1ec65e 100644 --- a/imx-core-sdk-kotlin-jvm/src/test/kotlin/com/immutable/sdk/ImmutableXTest.kt +++ b/imx-core-sdk-kotlin-jvm/src/test/kotlin/com/immutable/sdk/ImmutableXTest.kt @@ -18,7 +18,6 @@ import com.immutable.sdk.api.model.Collection import com.immutable.sdk.model.Erc721Asset import com.immutable.sdk.model.EthAsset import com.immutable.sdk.workflows.createTrade -import com.immutable.sdk.workflows.buyCrypto import com.immutable.sdk.workflows.cancelOrder import com.immutable.sdk.workflows.registerOffChain import com.immutable.sdk.workflows.createOrder @@ -222,14 +221,6 @@ class ImmutableXTest { ) } - @Test - fun testBuyCrypto() { - val future = CompletableFuture() - mockkStatic(::buyCrypto) - every { buyCrypto(any(), signer, any(), "colorCode", any()) } returns future - assertEquals(future, sdk.buyCrypto(signer, "colorCode")) - } - @Test fun testDeposit() { mockkStatic(::deposit) diff --git a/imx-core-sdk-kotlin-jvm/src/test/kotlin/com/immutable/sdk/workflows/BuyCryptoWorkflowTest.kt b/imx-core-sdk-kotlin-jvm/src/test/kotlin/com/immutable/sdk/workflows/BuyCryptoWorkflowTest.kt deleted file mode 100644 index cea95dc..0000000 --- a/imx-core-sdk-kotlin-jvm/src/test/kotlin/com/immutable/sdk/workflows/BuyCryptoWorkflowTest.kt +++ /dev/null @@ -1,216 +0,0 @@ -package com.immutable.sdk.workflows - -import com.immutable.sdk.ImmutableConfig -import com.immutable.sdk.ImmutableXBase -import com.immutable.sdk.Signer -import com.immutable.sdk.api.UsersApi -import com.immutable.sdk.api.model.GetUsersApiResponse -import com.immutable.sdk.extensions.getJson -import com.immutable.sdk.extensions.getJsonArray -import com.immutable.sdk.extensions.toURLEncodedString -import com.immutable.sdk.testFuture -import io.mockk.* -import io.mockk.impl.annotations.MockK -import okhttp3.Call -import okhttp3.OkHttpClient -import okhttp3.Response -import org.json.JSONArray -import org.json.JSONException -import org.json.JSONObject -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.openapitools.client.infrastructure.ClientException -import java.io.IOException -import java.net.HttpURLConnection -import java.net.URLEncoder -import java.util.concurrent.CompletableFuture - -private const val COLOUR_HEX = "81d8d0" -private const val ADDRESS = "0xa76e3eeb2f7143165618ab8feaabcd395b6fac7f" -private const val STARK_KEY = "0x01236456cb87e2c77dd6b43075c20888069a306aeff7ba36f654543aae80f678" -private const val BUY_CRYPTO_URL = "https://buy.crypto.com" -private const val MOONPAY_SIGNED_REQUEST = "1234567890abcdef=" -private const val TRANSACTION_ID = 8L -private const val BASE_URL = "https://ropsten.immutable.com" -private const val API_KEY = "pk_test_api" -private const val CURRENCY_CODE_VALUE = "usdc" -private const val ENCODED_CURRENCIES = "%7B%22eth_immutable" -private const val URL_ENCODED_JSON_STRING = "urlEncodedJsonString" - -class BuyCryptoWorkflowTest { - @MockK - private lateinit var client: OkHttpClient - - @MockK - private lateinit var signer: Signer - - @MockK - private lateinit var usersApi: UsersApi - - @MockK - private lateinit var getUsersApiResponse: GetUsersApiResponse - - @MockK - private lateinit var call: Call - - @MockK - private lateinit var response: Response - - @MockK - private lateinit var jsonObject: JSONObject - - @MockK - private lateinit var currencyJsonObject: JSONObject - - @MockK - private lateinit var jsonArray: JSONArray - - @MockK - private lateinit var base: ImmutableXBase - - private lateinit var addressFuture: CompletableFuture - - @Before - fun setUp() { - MockKAnnotations.init(this) - - addressFuture = CompletableFuture() - every { signer.getAddress() } returns addressFuture - addressFuture.complete(ADDRESS) - - every { usersApi.getUsers(any()) } returns getUsersApiResponse - every { getUsersApiResponse.accounts } returns arrayListOf(STARK_KEY) - - every { client.newCall(any()) } returns call - every { call.execute() } returns response - - mockkStatic("com.immutable.sdk.extensions.ResponseKt") - every { any().getJson() } returns jsonObject - every { any().getJsonArray() } returns jsonArray - every { jsonArray.length() } returns 1 - every { jsonArray.getJSONObject(0) } returns currencyJsonObject - every { currencyJsonObject.getString(CURRENCY_CODE) } returns CURRENCY_CODE_VALUE - - mockkStatic("com.immutable.sdk.extensions.JSONObjectKt") - every { any().toURLEncodedString() } returns URL_ENCODED_JSON_STRING - - mockkStatic(URLEncoder::class) - every { URLEncoder.encode(any(), any()) } returns ENCODED_CURRENCIES - - every { jsonObject.getLong(ID) } returns TRANSACTION_ID - every { jsonObject.getString(SIGNATURE) } returns MOONPAY_SIGNED_REQUEST - - mockkObject(ImmutableConfig) - every { ImmutableConfig.getPublicApiUrl(base) } returns BASE_URL - every { ImmutableConfig.getMoonpayApiKey(base) } returns API_KEY - every { ImmutableConfig.getBuyCryptoUrl(base) } returns BUY_CRYPTO_URL - } - - @After - fun tearDown() { - unmockkAll() - } - - private fun buyCrypto(): CompletableFuture = - buyCrypto( - base = base, - signer = signer, - client = client, - colourCodeHex = "#$COLOUR_HEX", - usersApi = usersApi - ) - - @Test - fun testBuyCryptoSuccess() { - val expectedRequestParams = "apiKey=$API_KEY" + - "&baseCurrencyCode=usd" + - "&colorCode=%23$COLOUR_HEX" + - "&externalTransactionId=$TRANSACTION_ID" + - "&walletAddress=$ADDRESS" + - "&walletAddresses=" + - URL_ENCODED_JSON_STRING - - testFuture( - future = buyCrypto(), - expectedResult = String.format( - MOONPAY_BUY_URL, - BUY_CRYPTO_URL, - expectedRequestParams, - MOONPAY_SIGNED_REQUEST - ), - expectedError = null - ) - } - - fun testBuyCryptoFailedOnAddress() { - every { addressFuture.get() } throws RuntimeException() - - testFuture( - future = buyCrypto(), - expectedResult = null, - expectedError = RuntimeException() - ) - } - - fun testBuyCryptoFailedOnApiCall() { - every { call.execute() } throws IOException() - - testFuture( - future = buyCrypto(), - expectedResult = null, - expectedError = IOException() - ) - } - - fun testBuyCryptoFailedOnGetSupportedCurrencies() { - every { currencyJsonObject.getString(CURRENCY_CODE) } throws JSONException("") - - testFuture( - future = buyCrypto(), - expectedResult = null, - expectedError = JSONException("") - ) - } - - fun testBuyCryptoFailedOnGetTransactionId() { - every { jsonObject.getLong(ID) } throws JSONException("") - - testFuture( - future = buyCrypto(), - expectedResult = null, - expectedError = JSONException("") - ) - } - - fun testBuyCryptoFailedOnGetBuyCryptoUrl() { - every { jsonObject.getString(SIGNATURE) } throws JSONException("") - - testFuture( - future = buyCrypto(), - expectedResult = null, - expectedError = JSONException("") - ) - } - - fun testBuyCryptoWalletNotRegistered_notFound() { - every { getUsersApiResponse.accounts } throws - ClientException(statusCode = HttpURLConnection.HTTP_NOT_FOUND) - - testFuture( - future = buyCrypto(), - expectedResult = null, - expectedError = ClientException() - ) - } - - fun testBuyCryptoWalletNotRegistered_emptyList() { - every { getUsersApiResponse.accounts } returns emptyList() - - testFuture( - future = buyCrypto(), - expectedResult = null, - expectedError = IllegalStateException() - ) - } -}