diff --git a/README.md b/README.md index 8855546d..ec9bccff 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Then add the following dependency to your module's build.gradle ```gradle dependencies { ... - implementation "com.metallicus:protonsdk:0.7.1" + implementation "com.metallicus:protonsdk:0.8.0" } ``` diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index c50afad9..339df764 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -23,8 +23,8 @@ const val kotlinVersion = "1.4.10" // TODO: 1.4.20 const val orchidVersion = "0.21.1" object ProtonSdk { - const val versionCode = 24 - const val versionName = "0.7.1" + const val versionCode = 25 + const val versionName = "0.8.0" } object BuildPlugins { diff --git a/protonsdk/src/androidTest/java/com/metallicus/protonsdk/ExampleInstrumentedTest.kt b/protonsdk/src/androidTest/java/com/metallicus/protonsdk/ExampleInstrumentedTest.kt index 650df991..e399e9f7 100644 --- a/protonsdk/src/androidTest/java/com/metallicus/protonsdk/ExampleInstrumentedTest.kt +++ b/protonsdk/src/androidTest/java/com/metallicus/protonsdk/ExampleInstrumentedTest.kt @@ -6,6 +6,17 @@ import org.hamcrest.CoreMatchers.containsString import org.junit.Assert.assertThat import org.junit.Test import org.junit.runner.RunWith +import timber.log.Timber +import java.security.InvalidAlgorithmParameterException +import java.security.InvalidKeyException +import java.security.NoSuchAlgorithmException +import java.util.* +import javax.crypto.BadPaddingException +import javax.crypto.Cipher +import javax.crypto.IllegalBlockSizeException +import javax.crypto.NoSuchPaddingException +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec /** * Instrumented test, which will execute on an Android device. @@ -20,4 +31,67 @@ class ExampleInstrumentedTest { val appContext = InstrumentationRegistry.getInstrumentation().targetContext assertThat(appContext.packageName, containsString("com.metallicus.protonsdk")) } + + @Test + fun checkAESDecrypt() { + val cipherTextHex = + "2a2049eee14c2bf2d73eae16d84affa1084e942f361ba846eb726ce56299e489d9d8afdcbd41bbca2fedc36c2ba5aef9373d757225e43654a2e98c99413c5f034f15bf7b9013b2dbe52951d2a4c537a5443a87914a5904571e7370a1c6d493cb9ce1a1d48dd80857a4d90bfe72b54ead940896192d6314d287606473734c17d75e988ce651e846286b9d2a06ecf100fd1832003948dd913e97cdde47cd6a1e9d06a1bd6397c55712721c627b9cc3c543f39cf6043da4241db23a6e7dfb14afe544da8bcffa26e078bf4a9580e54e044ab85b09ecabdc57ba9c5bc7008365be51" + val keyHex = "728f6bb27af36663bc51f3f7786d132d9ebf52bc7a46a4090e63a91ad058895a" + val ivHex = "4126bb121c0896cecf271af079c8c2d9" + + val cipherTextByteArray = cipherTextHex.hexStringToByteArray2() + val keyByteArray = keyHex.hexStringToByteArray2() + val ivByteArray = ivHex.hexStringToByteArray2() + + val decryptedByteArray: ByteArray = try { + cipherTextByteArray.aesDecrypt(keyByteArray, ivByteArray) + } catch (e: Exception) { + Timber.e(e) + byteArrayOf(0) + } + + assert(decryptedByteArray.isNotEmpty()) + } + + private fun String.hexStringToByteArray(): ByteArray { + val s = toUpperCase(Locale.ROOT) + val len = s.length + val result = ByteArray(len / 2) + for (i in 0 until len step 2) { + val firstIndex = indexOf(s[i]) + val secondIndex = indexOf(s[i + 1]) + + val octet = firstIndex.shl(4).or(secondIndex) + result[i.shr(1)] = octet.toByte() + } + return result + } + + private fun String.hexStringToByteArray2(): ByteArray { + val s = toUpperCase(Locale.ROOT) + val len = s.length + val data = ByteArray(len / 2) + var i = 0 + while (i < len) { + data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte() + i += 2 + } + return data + } + + @Throws( + NoSuchAlgorithmException::class, + NoSuchPaddingException::class, + InvalidKeyException::class, + InvalidAlgorithmParameterException::class, + IllegalBlockSizeException::class, + BadPaddingException::class + ) + private fun ByteArray.aesDecrypt(key: ByteArray, iv: ByteArray): ByteArray { + val keySpec = SecretKeySpec(key, "AES") + val ivSpec = IvParameterSpec(iv) + val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec) + return cipher.doFinal(this) + } } diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/AccountModule.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/AccountModule.kt index 563bc93a..0ac12274 100644 --- a/protonsdk/src/main/java/com/metallicus/protonsdk/AccountModule.kt +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/AccountModule.kt @@ -24,25 +24,42 @@ package com.metallicus.protonsdk import android.content.Context import android.net.Uri import android.util.Base64 -import com.google.gson.* +import com.google.common.primitives.Longs +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException import com.greymass.esr.ESR import com.greymass.esr.SigningRequest import com.greymass.esr.models.PermissionLevel import com.greymass.esr.models.TransactionContext -import com.metallicus.protonsdk.common.SecureKeys import com.metallicus.protonsdk.common.Prefs import com.metallicus.protonsdk.common.Resource +import com.metallicus.protonsdk.common.SecureKeys import com.metallicus.protonsdk.di.DaggerInjector +import com.metallicus.protonsdk.eosio.commander.BitUtils +import com.metallicus.protonsdk.eosio.commander.HexUtils import com.metallicus.protonsdk.eosio.commander.digest.Sha256 +import com.metallicus.protonsdk.eosio.commander.digest.Sha512 import com.metallicus.protonsdk.eosio.commander.ec.EosPrivateKey +import com.metallicus.protonsdk.eosio.commander.ec.EosPublicKey import com.metallicus.protonsdk.eosio.commander.model.types.TypeChainId import com.metallicus.protonsdk.model.* import com.metallicus.protonsdk.repository.AccountContactRepository import com.metallicus.protonsdk.repository.AccountRepository import com.metallicus.protonsdk.repository.ChainProviderRepository import com.metallicus.protonsdk.repository.ESRRepository +import kotlinx.coroutines.runBlocking import timber.log.Timber +import java.math.BigInteger +import java.security.InvalidAlgorithmParameterException +import java.security.InvalidKeyException +import java.security.NoSuchAlgorithmException import java.util.* +import javax.crypto.BadPaddingException +import javax.crypto.Cipher +import javax.crypto.IllegalBlockSizeException +import javax.crypto.NoSuchPaddingException +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec import javax.inject.Inject /** @@ -412,56 +429,104 @@ class AccountModule { } } - suspend fun decodeESR(chainAccount: ChainAccount, originalESRUrl: String): ProtonESR { + private suspend fun newSigningRequest(chainUrl: String): SigningRequest { + val esr = ESR(context) { account -> + runBlocking { + val response = chainProviderRepository.getAbi(chainUrl, account) + if (response.isSuccessful) { + val abiJson = response.body()?.get("abi") + abiJson?.toString().orEmpty() + } else { + response.errorBody()?.toString().orEmpty() + } + } + } + return SigningRequest(esr) + } + + val disallowedESRActions = listOf( + "updateauth", + "deleteauth", + "linkauth", + "unlinkauth", + "setabi", + "setcode", + "newaccount" + ) + + suspend fun decodeESR(chainAccount: ChainAccount, tokenContractMap: Map, originalESRUrl: String, esrSession: ESRSession?=null): ProtonESR { val originalESRUrlScheme = originalESRUrl.substringBefore(":") val esrUrl = "esr:" + originalESRUrl.substringAfter(":") val chainId = chainAccount.chainProvider.chainId val chainUrl = chainAccount.chainProvider.chainUrl - val esr = ESR(context) { account -> - val response = chainProviderRepository.getAbi(chainUrl, account) - if (response.isSuccessful) { - response.body()?.toString() - } else { - response.errorBody()?.toString() - } - } - - val signingRequest = SigningRequest(esr) + val signingRequest = newSigningRequest(chainUrl) signingRequest.load(esrUrl) // TODO: need chainId original string from esr request //val esrChainId = signingRequest.chainId.toVariant() //if (esrChainId == chainAccount.chainProvider.chainId) { - val requestAccountName = signingRequest.info["req_account"].orEmpty() + val returnPath = signingRequest.info["return_path"].orEmpty() + var requestAccount: Account? = null var requestKey = "" + + if (esrSession != null) { + requestAccount = esrSession.requester + requestKey = esrSession.id + } + + val actions = mutableListOf() if (signingRequest.isIdentity) { + val requestAccountName = signingRequest.info["req_account"].orEmpty() + + requestAccount = fetchAccount(chainId, chainUrl, requestAccountName) + val linkHexValue = signingRequest.infoPairs.find { it.key == "link" }?.hexValue.orEmpty() val linkCreate = signingRequest.decodeLinkCreate(linkHexValue) requestKey = linkCreate.requestKey - } - - val returnPath = signingRequest.info["return_path"].orEmpty() - - val requestAccount: Account? = if (requestAccountName.isNotEmpty()) { - fetchAccount(chainId, chainUrl, requestAccountName) } else { - null + val resolvedActions = signingRequest.resolveActions() + resolvedActions.forEach { + val name = it.name.name + if (!disallowedESRActions.contains(name)) { + val accountName = it.account.name + val data = it.data.data + + val type = if (name == "transfer") Type.TRANSFER else Type.CUSTOM + + val tokenContract = if (type == Type.TRANSFER) { + val quantity = data["quantity"] as String + if (quantity.isNotEmpty()) { + val symbol = quantity.split(" ")[1] + + tokenContractMap["$accountName:$symbol"] + } else { + null + } + } else { + null + } + + val esrAction = ESRAction(type, name, accountName, data, tokenContract) + actions.add(esrAction) + } + } } return ProtonESR( - requestKey, chainAccount, signingRequest, originalESRUrlScheme, requestAccount, - returnPath + returnPath, + requestKey, + actions ) } @@ -500,11 +565,13 @@ class AccountModule { suspend fun authorizeESR(pin: String, protonESR: ProtonESR): Resource { return try { + requireNotNull(protonESR.requestKey) + val resolvedSigningRequest = protonESR.signingRequest.resolve( PermissionLevel(protonESR.signingAccount.account.accountName, "active"), TransactionContext()) - protonESR.resolvedSigningRequest = resolvedSigningRequest +// protonESR.resolvedSigningRequest = resolvedSigningRequest val chainIdStr = protonESR.signingAccount.chainProvider.chainId val chainIdByteArray = TypeChainId(chainIdStr).bytes @@ -542,6 +609,21 @@ class AccountModule { val response = esrRepository.authorizeESR(callback.url, authParams) if (response.isSuccessful) { + val createdAt = Date().time + + val esrSession = ESRSession( + id = protonESR.requestKey, + signer = protonESR.signingAccount.account.accountName, + callbackUrl = callback.url, + receiveKey = sessionKey.toWif(), + receiveChannelUrl = sessionChannel.toString(), + createdAt = createdAt, + updatedAt = createdAt, + requester = protonESR.requestAccount + ) + + esrRepository.addESRSession(esrSession) + Resource.success(response.body()) } else { val msg = response.errorBody()?.string() @@ -557,4 +639,165 @@ class AccountModule { Resource.error(e.localizedMessage.orEmpty()) } } + + suspend fun getESRSessions(): List { + return esrRepository.getESRSessions() + } + + suspend fun updateESRSession(esrSession: ESRSession) { + esrRepository.updateESRSession(esrSession) + } + + suspend fun removeESRSession(esrSession: ESRSession) { + esrRepository.removeESRSession(esrSession) + } + + suspend fun removeAllESRSessions() { + esrRepository.removeAllESRSessions() + } + + @Throws(IllegalArgumentException::class) + suspend fun decodeESRSessionMessage(chainAccount: ChainAccount, tokenContractMap: Map, esrSession: ESRSession, message: String): ProtonESR { + //val chainId = chainAccount.chainProvider.chainId + val chainUrl = chainAccount.chainProvider.chainUrl + + val sealedMessageSigningRequest = newSigningRequest(chainUrl) + val sealedMessage = sealedMessageSigningRequest.decodeSealedMessage(message) + + val sealedPublicKey = EosPublicKey(sealedMessage.from) + + val sharedSecret = EosPrivateKey(esrSession.receiveKey).getSharedSecret(sealedPublicKey) + + val nonceLong = sealedMessage.nonce.toLong() + val nonceByteArray = Longs.toByteArray(nonceLong).reversedArray() + + val symmetricKey = nonceByteArray + sharedSecret + val symmetricSha512 = Sha512.from(symmetricKey).bytes + + val key = symmetricSha512.copyOfRange(0, 32) + val iv = symmetricSha512.copyOfRange(32, 48) + + val cipherTextByteArray = sealedMessage.cipherText.hexStringToByteArray() + + val esrByteArray: ByteArray = try { + cipherTextByteArray.aesDecrypt(key, iv) + } catch (e: Exception) { + Timber.e(e) + byteArrayOf(0) + } + + require(esrByteArray.isNotEmpty()) + + val esrUrl = String(esrByteArray) + + val protonESR = decodeESR(chainAccount, tokenContractMap, esrUrl, esrSession) + + return protonESR + } + + suspend fun authorizeESRActions(pin: String, protonESR: ProtonESR): Resource { + return try { + requireNotNull(protonESR.requestKey) + + val chainUrl = protonESR.signingAccount.chainProvider.chainUrl + + val chainInfoResponse = chainProviderRepository.getChainInfo(chainUrl) + if (chainInfoResponse.isSuccessful) { + chainInfoResponse.body()?.let { + val transactionContext = TransactionContext() + transactionContext.refBlockNum = + BigInteger(1, HexUtils.toBytes(it.lastIrreversibleBlockId.substring(0, 8))).toLong().and(0xFFFF) + transactionContext.refBlockPrefix = + BitUtils.uint32ToLong(HexUtils.toBytes(it.lastIrreversibleBlockId.substring(16, 24)), 0).and(0xFFFFFFFF) + transactionContext.expiration = it.getTimeAfterHeadBlockTime(60000) + + val resolvedSigningRequest = + protonESR.signingRequest.resolve( + PermissionLevel(protonESR.signingAccount.account.accountName, "active"), transactionContext) + + val chainIdStr = protonESR.signingAccount.chainProvider.chainId + val chainIdByteArray = TypeChainId(chainIdStr).bytes + + val transactionByteArray = resolvedSigningRequest.serializedTransaction + + val trailingByteArray = ByteArray(32) + + val unsignedTransactionDigest = chainIdByteArray + transactionByteArray + trailingByteArray + + val signature = signWithActiveKey(pin, unsignedTransactionDigest) + + val callback = resolvedSigningRequest.getCallback(listOf(signature)) + + val esrSession = esrRepository.getESRSession(protonESR.requestKey) + + val authParams = callback.payload + authParams["link_key"] = EosPrivateKey(esrSession.receiveKey).publicKey.toString() + authParams["link_ch"] = esrSession.receiveChannelUrl + authParams["link_name"] = getAppName() + //authParams.remove("SIG") + authParams["sig"] = signature + + val originalESRUrlScheme = protonESR.originESRUrlScheme + ":" + val req = authParams["req"]?.replace("esr:", originalESRUrlScheme) + authParams["req"] = req + + val response = esrRepository.authorizeESR(callback.url, authParams) + if (response.isSuccessful) { + esrSession.updatedAt = Date().time + esrRepository.updateESRSession(esrSession) + + Resource.success(response.body()) + } else { + val msg = response.errorBody()?.string() + val errorMsg = if (msg.isNullOrEmpty()) { + response.message() + } else { + msg + } + + Resource.error(errorMsg) + } + } ?: Resource.error("No Chain Info") + } else { + val msg = chainInfoResponse.errorBody()?.string() + val errorMsg = if (msg.isNullOrEmpty()) { + chainInfoResponse.message() + } else { + msg + } + + Resource.error(errorMsg) + } + } catch (e: Exception) { + Resource.error(e.localizedMessage.orEmpty()) + } + } + + private fun String.hexStringToByteArray(): ByteArray { + val s = toUpperCase(Locale.ROOT) + val len = s.length + val data = ByteArray(len / 2) + var i = 0 + while (i < len) { + data[i / 2] = ((Character.digit(s[i], 16) shl 4) + Character.digit(s[i + 1], 16)).toByte() + i += 2 + } + return data + } + + @Throws( + NoSuchAlgorithmException::class, + NoSuchPaddingException::class, + InvalidKeyException::class, + InvalidAlgorithmParameterException::class, + IllegalBlockSizeException::class, + BadPaddingException::class + ) + private fun ByteArray.aesDecrypt(key: ByteArray, iv: ByteArray): ByteArray { + val keySpec = SecretKeySpec(key, "AES") + val ivSpec = IvParameterSpec(iv) + val cipher = Cipher.getInstance("AES/CBC/PKCS7Padding") + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec) + return cipher.doFinal(this) + } } \ No newline at end of file diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/ActionsModule.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/ActionsModule.kt index 5bf1496e..72303419 100644 --- a/protonsdk/src/main/java/com/metallicus/protonsdk/ActionsModule.kt +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/ActionsModule.kt @@ -39,6 +39,7 @@ import com.metallicus.protonsdk.model.* import com.metallicus.protonsdk.model.Action as AccountAction import com.metallicus.protonsdk.repository.AccountContactRepository import com.metallicus.protonsdk.repository.ActionRepository +import com.metallicus.protonsdk.repository.ChainProviderRepository import timber.log.Timber import javax.inject.Inject @@ -49,6 +50,9 @@ class ActionsModule { @Inject lateinit var context: Context + @Inject + lateinit var chainProviderRepository: ChainProviderRepository + @Inject lateinit var actionRepository: ActionRepository @@ -286,7 +290,7 @@ class ActionsModule { private suspend fun signAndPushTransaction(chainUrl: String, pin: String, signedTransaction: SignedTransaction): Resource { return try { - val chainInfoResponse = actionRepository.getChainInfo(chainUrl) + val chainInfoResponse = chainProviderRepository.getChainInfo(chainUrl) if (chainInfoResponse.isSuccessful) { val chainInfo = chainInfoResponse.body() diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/Proton.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/Proton.kt index f942b4f9..ce29a6ba 100644 --- a/protonsdk/src/main/java/com/metallicus/protonsdk/Proton.kt +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/Proton.kt @@ -24,9 +24,7 @@ package com.metallicus.protonsdk import android.content.Context import androidx.lifecycle.* import com.google.gson.JsonObject -import com.metallicus.protonsdk.common.ProtonException -import com.metallicus.protonsdk.common.Resource -import com.metallicus.protonsdk.common.SingletonHolder +import com.metallicus.protonsdk.common.* import com.metallicus.protonsdk.di.DaggerInjector import com.metallicus.protonsdk.di.ProtonModule import com.metallicus.protonsdk.eosio.commander.digest.Sha256 @@ -34,7 +32,11 @@ import com.metallicus.protonsdk.eosio.commander.ec.EosPrivateKey import com.metallicus.protonsdk.eosio.commander.model.chain.Action as ChainAction import com.metallicus.protonsdk.model.* import kotlinx.coroutines.* +import okhttp3.* +import okhttp3.logging.HttpLoggingInterceptor +import okio.ByteString import timber.log.Timber +import java.util.concurrent.TimeUnit import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -495,7 +497,10 @@ class Proton private constructor(context: Context) { try { val activeAccount = getActiveAccountAsync() - emit(Resource.success(accountModule.decodeESR(activeAccount, esrUri))) + val tokenContracts = getTokenContractsAsync() + val tokenContractMap = tokenContracts.associateBy { "${it.contract}:${it.getSymbol()}" } + + emit(Resource.success(accountModule.decodeESR(activeAccount, tokenContractMap, esrUri))) } catch (e: ProtonException) { val error: Resource = Resource.error(e) emit(error) @@ -532,4 +537,249 @@ class Proton private constructor(context: Context) { emit(error) } } + + private class ESRSessionListener( + val onOpenCallback: (WebSocket, String, Int) -> Unit, + val onMessageCallback: (String) -> Unit, + val onClosingCallback: (String, Int) -> Unit, + val onClosedCallback: (String, Int) -> Unit, + val onFailureCallback: (String, Int) -> Unit + ): WebSocketListener() { + override fun onOpen(webSocket: WebSocket, response: Response) { + super.onOpen(webSocket, response) + Timber.d("ESRWebSocketListener onOpen - ${response.message}") + onOpenCallback(webSocket, response.message, response.code) + } + + override fun onMessage(webSocket: WebSocket, bytes: ByteString) { + super.onMessage(webSocket, bytes) + val text = bytes.hex() + Timber.d("ESRWebSocketListener onMessage - $text") + onMessageCallback(text) + } + + override fun onMessage(webSocket: WebSocket, text: String) { + super.onMessage(webSocket, text) + Timber.d("ESRWebSocketListener onMessage - $text") + onMessageCallback(text) + } + + override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { + super.onClosing(webSocket, code, reason) + Timber.d("ESRWebSocketListener onClosing - $reason") + onClosingCallback(reason, code) + } + + override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { + super.onClosed(webSocket, code, reason) + Timber.d("ESRWebSocketListener onClosed - $reason") + onClosedCallback(reason, code) + } + + override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) { + super.onFailure(webSocket, t, response) + val message = response?.message.orEmpty() + val code = response?.code ?: 0 + Timber.d("ESRWebSocketListener onFailure - ${response?.message} : ${t.localizedMessage}") + onFailureCallback(message, code) + } + } + + private val openESRSessions = mutableListOf>() + private fun getOpenESRSession(id: String): Pair? { + return openESRSessions.find { it.first == id } + } + private fun isESRSessionOpen(id: String): Boolean { + return openESRSessions.find { it.first == id } != null + } + private fun addOpenESRSession(id: String, webSocket: WebSocket) { + openESRSessions.add(Pair(id, webSocket)) + } + private fun removeOpenESRSession(id: String) { + getOpenESRSession(id)?.let { + it.second.cancel() // or close(1000, id)? + openESRSessions.remove(it) + } + } + private fun removeAllOpenESRSessions() { + openESRSessions.forEach { + it.second.cancel() // or close(1000, id)? + } + openESRSessions.clear() + } + + val esrSessionMessages = MutableLiveData>(mutableListOf()) + private fun addESRSessionMessage(esrSessionMessage: ESRSessionMessage) { + esrSessionMessages.value?.add(esrSessionMessage) + esrSessionMessages.postValue(esrSessionMessages.value) + } + private fun removeESRSessionMessage(esrSessionMessage: ESRSessionMessage) { + esrSessionMessages.value?.remove(esrSessionMessage) + esrSessionMessages.postValue(esrSessionMessages.value) + } + + fun initESRSessions(/*activeAccount*/) = protonCoroutineScope.launch { + try { + val esrSessionList = accountModule.getESRSessions(/*activeAccount*/) + esrSessionList.forEach { esrSession -> + val esrSessionId = esrSession.id + if (!isESRSessionOpen(esrSessionId)) { + val request = Request.Builder().url(esrSession.receiveChannelUrl).build() + + val logging = HttpLoggingInterceptor() + logging.level = HttpLoggingInterceptor.Level.BODY + + val httpClient = OkHttpClient.Builder() + .callTimeout(30, TimeUnit.SECONDS) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .addInterceptor(logging) + .build() + + httpClient.newWebSocket(request, ESRSessionListener( + onOpenCallback = { webSocket, message, _ -> + Timber.d("ESR Listener onOpen - $message") + + addOpenESRSession(esrSessionId, webSocket) + }, + onMessageCallback = { message -> + Timber.d("ESR Listener onMessage - $message") + + addESRSessionMessage(ESRSessionMessage(esrSession, message)) + }, + onClosingCallback = { reason, _ -> + Timber.d("ESR Listener onClosing - $reason") + }, + onClosedCallback = { reason, _ -> + Timber.d("ESR Listener onClosed - $reason") + + removeOpenESRSession(esrSessionId) + }, + onFailureCallback = { message, _ -> + Timber.d("ESR Listener onFailure - $message") + } + )) + } + } + } catch (e: Exception) { + Timber.e(e) + } + } + + fun decodeESRSessionMessage(esrSessionMessage: ESRSessionMessage): LiveData> = liveData { + emit(Resource.loading()) + + try { + val activeAccount = getActiveAccountAsync() + + val tokenContracts = getTokenContractsAsync() + val tokenContractMap = tokenContracts.associateBy { "${it.contract}:${it.getSymbol()}" } + + val esrSession = esrSessionMessage.esrSession + val message = esrSessionMessage.message + + emit(Resource.success(accountModule.decodeESRSessionMessage(activeAccount, tokenContractMap, esrSession, message))) + } catch (e: ProtonException) { + val error: Resource = Resource.error(e) + emit(error) + } catch (e: Exception) { + val error: Resource = Resource.error(e.localizedMessage.orEmpty()) + emit(error) + } + } + + fun cancelESRSessionMessage(esrSessionMessage: ESRSessionMessage, protonESR: ProtonESR): LiveData> = liveData { + emit(Resource.loading()) + + try { + val response = accountModule.cancelAuthorizeESR(protonESR) + + removeESRSessionMessage(esrSessionMessage) + + emit(response) + } catch (e: ProtonException) { + val error: Resource = Resource.error(e) + emit(error) + } catch (e: Exception) { + val error: Resource = Resource.error(e.localizedMessage.orEmpty()) + emit(error) + } + } + + fun authorizeESRSessionMessage(pin: String, esrSessionMessage: ESRSessionMessage, protonESR: ProtonESR): LiveData> = liveData { + emit(Resource.loading()) + + try { + val response = accountModule.authorizeESRActions(pin, protonESR) + + removeESRSessionMessage(esrSessionMessage) + + emit(response) + } catch (e: ProtonException) { + val error: Resource = Resource.error(e) + emit(error) + } catch (e: Exception) { + val error: Resource = Resource.error(e.localizedMessage.orEmpty()) + emit(error) + } + } + + fun getESRSessions(): LiveData>> = liveData { + emit(Resource.loading()) + + try { + val activeAccount = getActiveAccountAsync() + + val esrSessions = accountModule.getESRSessions(/*activeAccount*/) + + emit(Resource.success(esrSessions)) + } catch (e: ProtonException) { + val error: Resource> = Resource.error(e) + emit(error) + } catch (e: Exception) { + val error: Resource> = Resource.error(e.localizedMessage.orEmpty()) + emit(error) + } + } + + fun removeESRSession(esrSession: ESRSession): LiveData> = liveData { + emit(Resource.loading()) + + try { + val activeAccount = getActiveAccountAsync() + + removeOpenESRSession(esrSession.id) + + accountModule.removeESRSession(esrSession) + + emit(Resource.success(true)) + } catch (e: ProtonException) { + val error: Resource = Resource.error(e) + emit(error) + } catch (e: Exception) { + val error: Resource = Resource.error(e.localizedMessage.orEmpty()) + emit(error) + } + } + + fun removeAllESRSessions(): LiveData> = liveData { + emit(Resource.loading()) + + try { + val activeAccount = getActiveAccountAsync() + + removeAllOpenESRSessions() + + accountModule.removeAllESRSessions() + + emit(Resource.success(true)) + } catch (e: ProtonException) { + val error: Resource = Resource.error(e) + emit(error) + } catch (e: Exception) { + val error: Resource = Resource.error(e.localizedMessage.orEmpty()) + emit(error) + } + } } \ No newline at end of file diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/api/ProtonChainService.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/api/ProtonChainService.kt index ab6848b9..9ca43fb3 100644 --- a/protonsdk/src/main/java/com/metallicus/protonsdk/api/ProtonChainService.kt +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/api/ProtonChainService.kt @@ -76,7 +76,7 @@ interface ProtonChainService { ): Response @POST//("/v1/chain/get_abi") - fun getAbi( + suspend fun getAbi( @Url url: String, @Body body: AccountBody ): Response diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/common/Resource.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/common/Resource.kt index 3318dc23..f1aea83d 100755 --- a/protonsdk/src/main/java/com/metallicus/protonsdk/common/Resource.kt +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/common/Resource.kt @@ -1,7 +1,7 @@ package com.metallicus.protonsdk.common /** - * A generic class that a loading status, data, and optional message. + * A generic class that emulates loading, success, and error states with optional data and messages. */ data class Resource(val status: Status, val data: T?, val message: String?, val code: Int?) { companion object { diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/db/ESRSessionDao.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/db/ESRSessionDao.kt new file mode 100755 index 00000000..562896be --- /dev/null +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/db/ESRSessionDao.kt @@ -0,0 +1,28 @@ +package com.metallicus.protonsdk.db + +import androidx.room.* +import com.metallicus.protonsdk.model.ESRSession + +/** + * Interface for database access for [ESRSession] related operations + */ +@Dao +interface ESRSessionDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(esrSession: ESRSession) + + @Update + suspend fun update(esrSession: ESRSession) + + @Query("SELECT * FROM esrSession WHERE id = :id") + suspend fun findById(id: String): ESRSession + + @Query("SELECT * FROM esrSession") + suspend fun findAll(): List + + @Query("DELETE FROM esrSession WHERE id = :id") + suspend fun remove(id: String) + + @Query("DELETE FROM esrSession") + suspend fun removeAll() +} \ No newline at end of file diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/db/ProtonDb.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/db/ProtonDb.kt index 3a0f5167..a00d5368 100755 --- a/protonsdk/src/main/java/com/metallicus/protonsdk/db/ProtonDb.kt +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/db/ProtonDb.kt @@ -32,8 +32,9 @@ import com.metallicus.protonsdk.model.* Account::class, AccountContact::class, CurrencyBalance::class, - Action::class], - version = 20, + Action::class, + ESRSession::class], + version = 21, exportSchema = false ) abstract class ProtonDb : RoomDatabase() { @@ -43,4 +44,5 @@ abstract class ProtonDb : RoomDatabase() { abstract fun currencyBalanceDao(): CurrencyBalanceDao abstract fun accountContactDao(): AccountContactDao abstract fun actionDao(): ActionDao + abstract fun esrSessionDao(): ESRSessionDao } \ No newline at end of file diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/di/ProtonModule.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/di/ProtonModule.kt index 4d890d0d..7a11058f 100644 --- a/protonsdk/src/main/java/com/metallicus/protonsdk/di/ProtonModule.kt +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/di/ProtonModule.kt @@ -87,18 +87,24 @@ class ProtonModule { return db.actionDao() } + @Singleton + @Provides + fun provideESRSessionDao(db: ProtonDb): ESRSessionDao { + return db.esrSessionDao() + } + @Singleton @Provides fun provideProtonChainService(context: Context): ProtonChainService { -// val logging = HttpLoggingInterceptor() -// logging.level = HttpLoggingInterceptor.Level.BODY + val logging = HttpLoggingInterceptor() + logging.level = HttpLoggingInterceptor.Level.BODY val httpClient = OkHttpClient.Builder() .callTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(30, TimeUnit.SECONDS) -// .addInterceptor(logging) + .addInterceptor(logging) val gson = GsonBuilder() .registerTypeAdapterFactory(GsonEosTypeAdapterFactory()) diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/eosio/commander/ec/EosPrivateKey.java b/protonsdk/src/main/java/com/metallicus/protonsdk/eosio/commander/ec/EosPrivateKey.java index 8ab72815..13cfb3bc 100755 --- a/protonsdk/src/main/java/com/metallicus/protonsdk/eosio/commander/ec/EosPrivateKey.java +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/eosio/commander/ec/EosPrivateKey.java @@ -123,7 +123,7 @@ public EosPrivateKey(String base58Str) { mPublicKey = new EosPublicKey(findPubKey(mPrivateKey), mCurveParam); } - public byte[] getSharedSecret(EosPublicKey eosPublicKey, long nonce) { + public byte[] getSharedSecret(EosPublicKey eosPublicKey) { byte[] r = getBytes(); byte[] publicKeyBytes = eosPublicKey.getBytes(); CurveParam param = EcTools.getCurveParam(CurveParam.SECP256_K1); @@ -133,14 +133,7 @@ public byte[] getSharedSecret(EosPublicKey eosPublicKey, long nonce) { if (encodedx.length > 32) { encodedx = Arrays.copyOfRange(encodedx, 1, encodedx.length); } - - byte [] retval = encodedx; - if (nonce != 0L) { - byte[] nonceBytes = Longs.toByteArray(nonce); - retval = Bytes.concat(nonceBytes, encodedx); - } - - Sha512 sha512 = Sha512.from(retval); + Sha512 sha512 = Sha512.from(encodedx); return sha512.getBytes(); } diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/model/ESRAction.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/model/ESRAction.kt new file mode 100644 index 00000000..3e05370a --- /dev/null +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/model/ESRAction.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2020 Proton Chain LLC, Delaware + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.metallicus.protonsdk.model + +enum class Type { + TRANSFER, + CUSTOM +} + +class ESRAction( + val type: Type, + val name: String, + val accountName: String, + val data: Map?, + private val tokenContract: TokenContract?=null +) { + fun isTransfer(): Boolean { + return (type==Type.TRANSFER) + } + + fun getActionName(): String { + return if (isTransfer()) "Transfer" else name + } + + fun getActionAccountName(): String { + return if (isTransfer()) tokenContract?.name.orEmpty() else "@$accountName" + } + + fun getIconUrl(): String { + return tokenContract?.iconUrl.orEmpty() + } + + fun getTransferQuantity(): String? { + return data?.get("quantity") as String? + } + + fun getTransferQuantityAmount(): String { + val transferQuantity = getTransferQuantity() + return transferQuantity?.split(" ")?.get(0).orEmpty() + } + + fun getTransferQuantitySymbol(): String { + val transferQuantity = getTransferQuantity() + return transferQuantity?.split(" ")?.get(1).orEmpty() + } +} \ No newline at end of file diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/model/ESRSession.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/model/ESRSession.kt new file mode 100644 index 00000000..29467a6f --- /dev/null +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/model/ESRSession.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 Proton Chain LLC, Delaware + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.metallicus.protonsdk.model + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.google.gson.annotations.SerializedName + +@Entity +class ESRSession( + @PrimaryKey + @SerializedName("id") + var id: String, + @SerializedName("signer") + var signer: String, + @SerializedName("callbackUrl") + var callbackUrl: String, + @SerializedName("receiveKey") + var receiveKey: String, + @SerializedName("receiveChannelUrl") + var receiveChannelUrl: String, + @SerializedName("createdAt") + var createdAt: Long, + @SerializedName("updatedAt") + var updatedAt: Long, + + @SerializedName("requester") + @Embedded(prefix = "requester_") + var requester: Account? = null +) \ No newline at end of file diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/model/ESRSessionMessage.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/model/ESRSessionMessage.kt new file mode 100755 index 00000000..0ef6ae05 --- /dev/null +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/model/ESRSessionMessage.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 Proton Chain LLC, Delaware + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package com.metallicus.protonsdk.model + +data class ESRSessionMessage(val esrSession: ESRSession, val message: String) diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/model/ProtonESR.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/model/ProtonESR.kt index e30f8313..32036fa9 100644 --- a/protonsdk/src/main/java/com/metallicus/protonsdk/model/ProtonESR.kt +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/model/ProtonESR.kt @@ -21,18 +21,16 @@ */ package com.metallicus.protonsdk.model -import com.greymass.esr.ResolvedSigningRequest import com.greymass.esr.SigningRequest class ProtonESR( - val requestKey: String, val signingAccount: ChainAccount, val signingRequest: SigningRequest, val originESRUrlScheme: String, val requestAccount: Account? = null, val returnPath: String? = "", - var resolvedSigningRequest: ResolvedSigningRequest? = null//, - //val actions: List + val requestKey: String? = "", + val actions: List = emptyList() ) { fun getRequestAccountDisplayName(): String { return requestAccount?.accountContact?.getDisplayName() ?: "Unknown Requester" diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/repository/ActionRepository.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/repository/ActionRepository.kt index d45131e9..f10da2d1 100755 --- a/protonsdk/src/main/java/com/metallicus/protonsdk/repository/ActionRepository.kt +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/repository/ActionRepository.kt @@ -60,10 +60,6 @@ class ActionRepository @Inject constructor( return protonChainService.jsonToBin("$chainUrl/v1/chain/abi_json_to_bin", JsonToBinBody(code, action, args)) } - suspend fun getChainInfo(chainUrl: String): Response { - return protonChainService.getChainInfo("$chainUrl/v1/chain/get_info") - } - suspend fun getRequiredKeys(chainUrl: String, requiredKeysBody: RequiredKeysBody): Response { return protonChainService.getRequiredKeys("$chainUrl/v1/chain/get_required_keys", requiredKeysBody) } diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/repository/ChainProviderRepository.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/repository/ChainProviderRepository.kt index 9af6389d..b595d879 100755 --- a/protonsdk/src/main/java/com/metallicus/protonsdk/repository/ChainProviderRepository.kt +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/repository/ChainProviderRepository.kt @@ -25,6 +25,7 @@ import com.google.gson.JsonObject import com.metallicus.protonsdk.api.AccountBody import com.metallicus.protonsdk.api.ProtonChainService import com.metallicus.protonsdk.db.ChainProviderDao +import com.metallicus.protonsdk.model.ChainInfo import com.metallicus.protonsdk.model.ChainProvider import retrofit2.Response import javax.inject.Inject @@ -43,15 +44,19 @@ class ChainProviderRepository @Inject constructor( chainProviderDao.insert(chainProvider) } - suspend fun fetchChainProvider(protonChainUrl: String): Response { - return protonChainService.getChainProvider("$protonChainUrl/v1/chain/info") + suspend fun fetchChainProvider(chainUrl: String): Response { + return protonChainService.getChainProvider("$chainUrl/v1/chain/info") } suspend fun getChainProvider(id: String): ChainProvider { return chainProviderDao.findById(id) } - fun getAbi(chainUrl: String, accountName: String): Response { - return protonChainService.getAbi("$chainUrl/v1/get_abi", AccountBody(accountName)) + suspend fun getChainInfo(chainUrl: String): Response { + return protonChainService.getChainInfo("$chainUrl/v1/chain/get_info") + } + + suspend fun getAbi(chainUrl: String, accountName: String): Response { + return protonChainService.getAbi("$chainUrl/v1/chain/get_abi", AccountBody(accountName)) } } diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/repository/ESRRepository.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/repository/ESRRepository.kt index c637228c..551875ab 100755 --- a/protonsdk/src/main/java/com/metallicus/protonsdk/repository/ESRRepository.kt +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/repository/ESRRepository.kt @@ -22,13 +22,16 @@ package com.metallicus.protonsdk.repository import com.metallicus.protonsdk.api.* +import com.metallicus.protonsdk.db.ESRSessionDao +import com.metallicus.protonsdk.model.ESRSession import retrofit2.Response import javax.inject.Inject import javax.inject.Singleton @Singleton class ESRRepository @Inject constructor( - private val esrCallbackService: ESRCallbackService + private val esrCallbackService: ESRCallbackService, + private val esrSessionDao: ESRSessionDao ) { suspend fun cancelAuthorizeESR(url: String, error: String): Response { return esrCallbackService.cancelAuthorizeESR(url, CancelAuthorizeESRBody(error)) @@ -37,4 +40,28 @@ class ESRRepository @Inject constructor( suspend fun authorizeESR(url: String, params: Map): Response { return esrCallbackService.authorizeESR(url, params) } + + suspend fun getESRSession(id: String): ESRSession { + return esrSessionDao.findById(id) + } + + suspend fun getESRSessions(): List { + return esrSessionDao.findAll() + } + + suspend fun addESRSession(esrSession: ESRSession) { + esrSessionDao.insert(esrSession) + } + + suspend fun updateESRSession(esrSession: ESRSession) { + esrSessionDao.update(esrSession) + } + + suspend fun removeESRSession(esrSession: ESRSession) { + esrSessionDao.remove(esrSession.id) + } + + suspend fun removeAllESRSessions() { + esrSessionDao.removeAll() + } } diff --git a/protonsdk/src/main/java/com/metallicus/protonsdk/workers/InitChainProviderWorker.kt b/protonsdk/src/main/java/com/metallicus/protonsdk/workers/InitChainProviderWorker.kt index 03130c84..0a44f330 100644 --- a/protonsdk/src/main/java/com/metallicus/protonsdk/workers/InitChainProviderWorker.kt +++ b/protonsdk/src/main/java/com/metallicus/protonsdk/workers/InitChainProviderWorker.kt @@ -47,15 +47,15 @@ class InitChainProviderWorker @Suppress("BlockingMethodInNonBlockingContext") override suspend fun doWork(): Result { - val protonChainUrl = inputData.getString(PROTON_CHAIN_URL).orEmpty() + val chainUrl = inputData.getString(PROTON_CHAIN_URL).orEmpty() return try { - val response = chainProviderRepository.fetchChainProvider(protonChainUrl) + val response = chainProviderRepository.fetchChainProvider(chainUrl) if (response.isSuccessful) { chainProviderRepository.removeAll() val chainProvider = Gson().fromJson(response.body(), ChainProvider::class.java) - chainProvider.chainApiUrl = protonChainUrl + chainProvider.chainApiUrl = chainUrl chainProviderRepository.addChainProvider(chainProvider)