diff --git a/blockchain/build.gradle b/blockchain/build.gradle index 4f75526d84..e74dfefe20 100644 --- a/blockchain/build.gradle +++ b/blockchain/build.gradle @@ -60,6 +60,14 @@ dependencies { implementation 'co.nstant.in:cbor:0.8' + implementation files('libs/ripple-core-0.0.1.jar') + //4 dependencies for ripple-core + implementation 'net.i2p.crypto:eddsa:0.3.0' + implementation 'org.bouncycastle:bcprov-jdk15on:1.61' + //noinspection DuplicatePlatformClasses + implementation 'org.json:json:20180813' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2' testImplementation "com.google.truth:truth:1.0" androidTestImplementation 'androidx.test.ext:junit:1.1.1' diff --git a/blockchain/libs/ripple-core-0.0.1.jar b/blockchain/libs/ripple-core-0.0.1.jar new file mode 100644 index 0000000000..617db52b2d Binary files /dev/null and b/blockchain/libs/ripple-core-0.0.1.jar differ diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt index d405ead446..ac90c477bd 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt @@ -7,6 +7,8 @@ import com.tangem.blockchain.ethereum.EthereumAddressValidator import com.tangem.blockchain.cardano.CardanoAddressFactory import com.tangem.blockchain.cardano.CardanoAddressValidator import com.tangem.blockchain.stellar.StellarAddressFactory +import com.tangem.blockchain.xrp.XrpAddressFactory +import com.tangem.blockchain.xrp.XrpAddressValidator import java.math.BigDecimal enum class Blockchain( @@ -22,7 +24,7 @@ enum class Blockchain( Ethereum("ETH", "ETH", 18, "Ethereum", 0), Rootstock("", "", 18, "", 0), Cardano("CARDANO", "ADA", 6, "Cardano", 0), - Ripple("", "", 6, "", 0), + XRP("", "XRP", 6, "XRP Ledger", 0), Binance("", "", 8, "", 0), Stellar("XLM", "XLM", 7, "Stellar", 0); @@ -40,7 +42,7 @@ enum class Blockchain( Ethereum -> EthereumAddressFactory.makeAddress(walletPublicKey) // Rootstock -> RootstockAddressFactory.makeAddress(cardPublicKey) Cardano -> CardanoAddressFactory.makeAddress(walletPublicKey) -// Ripple -> RippleAddressFactory.makeAddress(cardPublicKey) + XRP -> XrpAddressFactory.makeAddress(walletPublicKey) // Binance -> BinanceAddressFactory.makeAddress(cardPublicKey) Stellar -> StellarAddressFactory.makeAddress(walletPublicKey) else -> throw Exception("unsupported blockchain") @@ -55,7 +57,7 @@ enum class Blockchain( Ethereum -> EthereumAddressValidator.validate(address) // Rootstock -> RootstockAddressValidator.validate(address) Cardano -> CardanoAddressValidator.validate(address) -// Ripple -> RippleAddressValidator.validate(address) + XRP -> XrpAddressValidator.validate(address) // Binance -> BinanceAddressValidator.validate(address) // Stellar -> StellarAddressValidator.validate(address) else -> throw Exception("unsupported blockchain") diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt index 9705ab191d..3f57d8e038 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.ethereum.Chain import com.tangem.blockchain.ethereum.EthereumWalletManager import com.tangem.blockchain.cardano.CardanoWalletManager import com.tangem.blockchain.stellar.StellarWalletManager +import com.tangem.blockchain.xrp.XrpWalletManager import com.tangem.commands.Card object WalletManagerFactory { @@ -49,6 +50,13 @@ object WalletManagerFactory { walletConfig = WalletConfig(false, true) ) } + blockchainName.contains("xrp") -> { + return XrpWalletManager( + cardId = card.cardId, + walletPublicKey = walletPublicKey, + walletConfig = WalletConfig(true, true) + ) + } else -> return null } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/extensions/Amount.kt b/blockchain/src/main/java/com/tangem/blockchain/common/extensions/Amount.kt new file mode 100644 index 0000000000..fcb5b6fe14 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/common/extensions/Amount.kt @@ -0,0 +1,8 @@ +package com.tangem.blockchain.common.extensions + +import com.tangem.blockchain.common.Amount +import java.math.BigInteger + +fun Amount.bigIntegerValue(): BigInteger { + return this.value!!.movePointRight(this.decimals.toInt()).toBigInteger() +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/network/RetrofitBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/common/network/RetrofitBuilder.kt index 362e57726e..9834fc8cb9 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/network/RetrofitBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/network/RetrofitBuilder.kt @@ -43,4 +43,6 @@ const val API_STELLAR_RESERVE = "https://horizon.sui.li/" const val API_STELLAR_TESTNET = "https://horizon-testnet.stellar.org/" const val API_BLOCKCHAIN_INFO = "https://blockchain.info/" const val API_ADALITE = "https://explorer2.adalite.io" -const val API_ADALITE_RESERVE = "https://nodes.southeastasia.cloudapp.azure.com" \ No newline at end of file +const val API_ADALITE_RESERVE = "https://nodes.southeastasia.cloudapp.azure.com" +const val API_RIPPLED = "https://s1.ripple.com:51234" +const val API_RIPPLED_RESERVE = "https://s2.ripple.com:51234" \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/xrp/XrpAddress.kt b/blockchain/src/main/java/com/tangem/blockchain/xrp/XrpAddress.kt new file mode 100644 index 0000000000..2bd702f67f --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/xrp/XrpAddress.kt @@ -0,0 +1,38 @@ +package com.tangem.blockchain.xrp + +import com.ripple.encodings.addresses.Addresses +import com.tangem.common.extensions.calculateRipemd160 +import com.tangem.common.extensions.calculateSha256 +import com.tangem.common.extensions.toCompressedPublicKey + +class XrpAddressFactory { + companion object { + fun makeAddress(walletPublicKey: ByteArray): String { + val canonicalPublicKey = canonizePublicKey(walletPublicKey) + val publicKeyHash = canonicalPublicKey.calculateSha256().calculateRipemd160() + return Addresses.encodeAccountID(publicKeyHash) + } + + fun canonizePublicKey(publicKey: ByteArray): ByteArray { + val compressedPublicKey = publicKey.toCompressedPublicKey() + return if (compressedPublicKey.size == 32) { + byteArrayOf(0xED.toByte()) + compressedPublicKey + } else { + compressedPublicKey + } + } + } +} + +class XrpAddressValidator { + companion object { + fun validate(address: String): Boolean { + return try { + Addresses.decodeAccountID(address) + address.startsWith("r") + } catch (excpetion: Exception) { + false + } + } + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/xrp/XrpTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/xrp/XrpTransactionBuilder.kt new file mode 100644 index 0000000000..2ef51aa2e5 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/xrp/XrpTransactionBuilder.kt @@ -0,0 +1,54 @@ +package com.tangem.blockchain.xrp + +import com.ripple.core.coretypes.AccountID +import com.ripple.core.coretypes.Amount +import com.ripple.core.coretypes.uint.UInt32 +import com.ripple.crypto.ecdsa.ECDSASignature +import com.ripple.utils.HashUtils +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.extensions.bigIntegerValue +import com.tangem.blockchain.xrp.override.XrpPayment +import com.tangem.blockchain.xrp.override.XrpSignedTransaction +import org.bitcoinj.core.ECKey +import java.math.BigInteger + +class XrpTransactionBuilder(private val walletPublicKey: ByteArray) { + private val canonicalPublicKey = XrpAddressFactory.canonizePublicKey(walletPublicKey) + var sequence: Long? = null + private var transaction: XrpSignedTransaction? = null + + fun buildToSign(transactionData: TransactionData): ByteArray { + val payment = XrpPayment() + payment.`as`(AccountID.Account, transactionData.sourceAddress) + payment.`as`(AccountID.Destination, transactionData.destinationAddress) + payment.`as`(Amount.Amount, transactionData.amount.bigIntegerValue().toString()) + payment.`as`(UInt32.Sequence, sequence) + payment.`as`(Amount.Fee, transactionData.fee!!.bigIntegerValue().toString()) + + transaction = payment.prepare(canonicalPublicKey) + + return if (canonicalPublicKey[0] == 0xED.toByte()) { + transaction!!.signingData + } else { + HashUtils.halfSha512(transaction!!.signingData) + } + } + + fun buildToSend(signature: ByteArray): String { + if (canonicalPublicKey[0] == 0xED.toByte()) { + transaction!!.addSign(signature) + } else { + val derSignature = encodeDerSignature(signature) + transaction!!.addSign(derSignature) + } + return transaction!!.tx_blob + } + + private fun encodeDerSignature(signature: ByteArray): ByteArray { + val r = BigInteger(1, signature.copyOfRange(0, 32)) + val s = BigInteger(1, signature.copyOfRange(32, 64)) + val canonicalS = ECKey.ECDSASignature(r, s).toCanonicalised().s + val ecdsaSignature = ECDSASignature(r, canonicalS) + return ecdsaSignature.encodeToDER() + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/xrp/XrpWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/xrp/XrpWalletManager.kt new file mode 100644 index 0000000000..f2ea7fa7d3 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/xrp/XrpWalletManager.kt @@ -0,0 +1,86 @@ +package com.tangem.blockchain.xrp + +import android.util.Log +import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.extensions.Result +import com.tangem.blockchain.common.extensions.SimpleResult +import com.tangem.blockchain.wallets.CurrencyWallet +import com.tangem.blockchain.xrp.network.XrpInfoResponse +import com.tangem.blockchain.xrp.network.XrpNetworkManager +import com.tangem.tasks.TaskEvent + +class XrpWalletManager( + private val cardId: String, + private val walletPublicKey: ByteArray, + walletConfig: WalletConfig +) : WalletManager, + TransactionSender, + FeeProvider { + + override val blockchain = Blockchain.XRP + private val address = blockchain.makeAddress(walletPublicKey) + private val currencyWallet = CurrencyWallet(walletConfig, address) + override var wallet: Wallet = currencyWallet + private val transactionBuilder = XrpTransactionBuilder(walletPublicKey) + private val networkManager = XrpNetworkManager() + + override suspend fun update() { + val result = networkManager.getInfo(address) + when (result) { + is Result.Success -> updateWallet(result.data) + is Result.Failure -> updateError(result.error) + } + } + + private fun updateWallet(response: XrpInfoResponse) { + Log.d(this::class.java.simpleName, "Balance is ${response.balance}") + currencyWallet.balances[AmountType.Reserve]?.value = response.reserveBase + + if (!response.accountFound) { + updateError(Exception("Account not found")) //TODO rework, add reserve + return + } + currencyWallet.balances[AmountType.Coin]?.value = response.balance - response.reserveBase + transactionBuilder.sequence = response.sequence + + if (response.hasUnconfirmed) { + if (currencyWallet.pendingTransactions.isEmpty()) { + currencyWallet.pendingTransactions.add(TransactionData( + Amount(blockchain.currency, decimals = blockchain.decimals), + null, + "unknown", + currencyWallet.address)) + } + } else { + currencyWallet.pendingTransactions.clear() + } + } + + private fun updateError(error: Throwable?) { + Log.e(this::class.java.simpleName, error?.message ?: "") + } + + override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult { + val transactionHash = transactionBuilder.buildToSign(transactionData) + + when (val signerResponse = signer.sign(arrayOf(transactionHash), cardId)) { + is TaskEvent.Event -> { + val transactionToSend = transactionBuilder.buildToSend(signerResponse.data.signature) + return networkManager.sendTransaction(transactionToSend) + } + is TaskEvent.Completion -> return SimpleResult.Failure(signerResponse.error) + } + } + + override suspend fun getFee(amount: Amount, source: String, destination: String): Result> { + val result = networkManager.getFee() + when (result) { + is Result.Failure -> return result + is Result.Success -> return Result.Success(listOf( + Amount(result.data.minimalFee, blockchain), + Amount(result.data.normalFee, blockchain), + Amount(result.data.priorityFee, blockchain) + )) + } + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/xrp/network/XrpNetworkManager.kt b/blockchain/src/main/java/com/tangem/blockchain/xrp/network/XrpNetworkManager.kt new file mode 100644 index 0000000000..c7aa738a23 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/xrp/network/XrpNetworkManager.kt @@ -0,0 +1,92 @@ +package com.tangem.blockchain.xrp.network + +import com.tangem.blockchain.bitcoin.network.BitcoinFee +import com.tangem.blockchain.common.extensions.Result +import com.tangem.blockchain.common.extensions.SimpleResult +import com.tangem.blockchain.common.network.API_RIPPLED +import com.tangem.blockchain.common.network.API_RIPPLED_RESERVE +import com.tangem.blockchain.common.network.createRetrofitInstance +import com.tangem.blockchain.xrp.network.rippled.RippledApi +import com.tangem.blockchain.xrp.network.rippled.RippledProvider +import retrofit2.HttpException +import java.io.IOException +import java.math.BigDecimal + +class XrpNetworkManager { + private val rippledProvider by lazy { + val api = createRetrofitInstance(API_RIPPLED) + .create(RippledApi::class.java) + RippledProvider(api) + } + + private val rippledReserveProvider by lazy { + val api = createRetrofitInstance(API_RIPPLED_RESERVE) + .create(RippledApi::class.java) + RippledProvider(api) + } + + var provider = rippledProvider + + private fun changeProvider() { + provider = if (provider == rippledProvider) rippledReserveProvider else rippledProvider + } + + suspend fun getInfo(address: String): Result { + val result = provider.getInfo(address) + when (result) { + is Result.Success -> return result + is Result.Failure -> { + if (result.error is IOException || result.error is HttpException) { + changeProvider() + return provider.getInfo(address) + } else { + return result + } + } + } + } + + suspend fun sendTransaction(transaction: String): SimpleResult { + val result = provider.sendTransaction(transaction) + when (result) { + is SimpleResult.Success -> return result + is SimpleResult.Failure -> { + if (result.error is IOException || result.error is HttpException) { + changeProvider() + return provider.sendTransaction(transaction) + } else { + return result + } + } + } + } + + suspend fun getFee(): Result { + val result = provider.getFee() + when (result) { + is Result.Success -> return result + is Result.Failure -> { + if (result.error is IOException || result.error is HttpException) { + changeProvider() + return provider.getFee() + } else { + return result + } + } + } + } +} + +data class XrpInfoResponse( + val balance: BigDecimal = BigDecimal.ZERO, + val sequence: Long = 0, + val hasUnconfirmed: Boolean = false, + val reserveBase: BigDecimal, + val accountFound: Boolean = true +) + +data class XrpFeeResponse( + val minimalFee: BigDecimal, + val normalFee: BigDecimal, + val priorityFee: BigDecimal +) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/xrp/network/rippled/RippledApi.kt b/blockchain/src/main/java/com/tangem/blockchain/xrp/network/rippled/RippledApi.kt new file mode 100644 index 0000000000..e881830e69 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/xrp/network/rippled/RippledApi.kt @@ -0,0 +1,38 @@ +package com.tangem.blockchain.xrp.network.rippled + +import retrofit2.http.Body +import retrofit2.http.Headers +import retrofit2.http.POST + +interface RippledApi { + @Headers("Content-Type: application/json") + @POST("./") + suspend fun getAccount(@Body rippledBody: RippledBody): RippledAccountResponse + + @Headers("Content-Type: application/json") + @POST("./") + suspend fun getServerState(@Body rippledBody: RippledBody = serverStateBody): RippledStateResponse + + @Headers("Content-Type: application/json") + @POST("./") + suspend fun getFee(@Body rippledBody: RippledBody = feeBody): RippledFeeResponse + + @Headers("Content-Type: application/json") + @POST("./") + suspend fun submitTransaction(@Body rippledBody: RippledBody): RippledSubmitResponse +} + +enum class RippledMethod(val value: String) { + ACCOUNT_INFO("account_info"), + SERVER_STATE("server_state"), + FEE("fee"), + SUBMIT("submit") +} + +data class RippledBody( + val method: String, + val params: HashMap = HashMap() //TODO =null? +) + +val serverStateBody = RippledBody(RippledMethod.SERVER_STATE.value) +val feeBody = RippledBody(RippledMethod.FEE.value) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/xrp/network/rippled/RippledProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/xrp/network/rippled/RippledProvider.kt new file mode 100644 index 0000000000..ee0c499ce0 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/xrp/network/rippled/RippledProvider.kt @@ -0,0 +1,101 @@ +package com.tangem.blockchain.xrp.network.rippled + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.extensions.Result +import com.tangem.blockchain.common.extensions.SimpleResult +import com.tangem.blockchain.common.extensions.retryIO +import com.tangem.blockchain.xrp.network.XrpFeeResponse +import com.tangem.blockchain.xrp.network.XrpInfoResponse +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import java.math.BigDecimal + +class RippledProvider(private val api: RippledApi) { + private val decimals = Blockchain.XRP.decimals.toInt() + + suspend fun getInfo(address: String): Result { + return try { + coroutineScope { + val accountBody = makeAccountBody(address, validated = true) + val accountDeferred = retryIO { async { api.getAccount(accountBody) } } + + val unconfirmedBody = makeAccountBody(address, validated = false) + val unconfirmedDeferred = retryIO { async { api.getAccount(unconfirmedBody) } } + + val stateDeferred = retryIO { async { api.getServerState() } } + + val accountData = accountDeferred.await() + val unconfirmedData = unconfirmedDeferred.await() + val serverState = stateDeferred.await() + + val reserveBase = serverState.result!!.state!!.validatedLedger!!.reserveBase!! + .toBigDecimal().movePointLeft(decimals) + + if (accountData.result!!.errorCode == 19) { + Result.Success(XrpInfoResponse( + reserveBase = reserveBase, + accountFound = false + )) + } else { + val confirmedBalance = + accountData.result!!.accountData!!.balance!!.toBigDecimal() + .movePointLeft(decimals) + val unconfirmedBalance = + unconfirmedData.result!!.accountData!!.balance!!.toBigDecimal() + .movePointLeft(decimals) + + Result.Success(XrpInfoResponse( + balance = confirmedBalance, + sequence = accountData.result!!.accountData!!.sequence!!, + hasUnconfirmed = confirmedBalance != unconfirmedBalance, + reserveBase = reserveBase + )) + } + + } + } catch (exception: Exception) { + Result.Failure(exception) + } + } + + suspend fun getFee(): Result { + return try { + val feeData = retryIO { api.getFee() } + Result.Success(XrpFeeResponse( + feeData.result!!.feeData!!.minimalFee!!.toBigDecimal().movePointLeft(decimals), + feeData.result!!.feeData!!.normalFee!!.toBigDecimal().movePointLeft(decimals), + feeData.result!!.feeData!!.priorityFee!!.toBigDecimal().movePointLeft(decimals) + )) + } catch (exception: Exception) { + Result.Failure(exception) + } + } + + suspend fun sendTransaction(transaction: String): SimpleResult { + return try { + val submitBody = makeSubmitBody(transaction) + val submitData = retryIO { api.submitTransaction(submitBody) } + if (submitData.result!!.resultCode == 0) { + SimpleResult.Success + } else { + SimpleResult.Failure(Exception(submitData.result!!.resultMessage + ?: submitData.result!!.errorException)) + } + } catch (exception: Exception) { + SimpleResult.Failure(exception) + } + } +} + +private fun makeAccountBody(address: String, validated: Boolean): RippledBody { + val params = HashMap() + params["account"] = address + params["ledger_index"] = if (validated) "validated" else "current" + return RippledBody(RippledMethod.ACCOUNT_INFO.value, params) +} + +private fun makeSubmitBody(transaction: String): RippledBody { + val params = HashMap() + params["tx_blob"] = transaction + return RippledBody(RippledMethod.SUBMIT.value, params) +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/xrp/network/rippled/RippledResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/xrp/network/rippled/RippledResponse.kt new file mode 100644 index 0000000000..47d91f3634 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/xrp/network/rippled/RippledResponse.kt @@ -0,0 +1,103 @@ +package com.tangem.blockchain.xrp.network.rippled + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +// Rippled account +@JsonClass(generateAdapter = true) +data class RippledAccountResponse( + @Json(name = "result") + var result: RippledAccountResult? = null +) + +@JsonClass(generateAdapter = true) +data class RippledAccountResult( + @Json(name = "account_data") + var accountData: RippledAccountData? = null, + + @Json(name = "error_code") + var errorCode: Int? = null +) + +@JsonClass(generateAdapter = true) +data class RippledAccountData( + @Json(name = "Balance") + var balance: String? = null, + + @Json(name = "Sequence") + var sequence: Long? = null +) + +// Rippled state +@JsonClass(generateAdapter = true) +data class RippledStateResponse( + @Json(name = "result") + var result: RippledStateResult? = null +) + +@JsonClass(generateAdapter = true) +data class RippledStateResult( + @Json(name = "state") + var state: RippledState? = null +) + +@JsonClass(generateAdapter = true) +data class RippledState( + @Json(name = "validated_ledger") + var validatedLedger: RippledLedger? = null +) + +@JsonClass(generateAdapter = true) +data class RippledLedger( + @Json(name = "reserve_base") + var reserveBase: Long? = null +) + +// Rippled fee +@JsonClass(generateAdapter = true) +data class RippledFeeResponse( + @Json(name = "result") + var result: RippledFeeResult? = null +) + +@JsonClass(generateAdapter = true) +data class RippledFeeResult( + @Json(name = "drops") + var feeData: RippledFeeData? = null +) + +@JsonClass(generateAdapter = true) +data class RippledFeeData( + //enough to put tx to queue + @Json(name = "minimum_fee") + var minimalFee: String? = null, + + //enough to put tx to current ledger + @Json(name = "open_ledger_fee") + var normalFee: String? = null, + + @Json(name = "median_fee") + var priorityFee: String? = null +) + +// Rippled submit +@JsonClass(generateAdapter = true) +data class RippledSubmitResponse( + @Json(name = "result") + var result: RippledSubmitResult? = null +) + +@JsonClass(generateAdapter = true) +data class RippledSubmitResult( + @Json(name = "engine_result_code") + var resultCode: Int? = null, + + @Json(name = "engine_result_message") + var resultMessage: String? = null, + + @Json(name = "error") + var error: String? = null, + + @Json(name = "error_exception") + var errorException: String? = null +) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/xrp/override/XrpBase58.java b/blockchain/src/main/java/com/tangem/blockchain/xrp/override/XrpBase58.java new file mode 100644 index 0000000000..a48e47e808 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/xrp/override/XrpBase58.java @@ -0,0 +1,115 @@ +package com.tangem.blockchain.xrp.override; + +import java.math.BigInteger; + +/** + * Created by Ilia on 15.02.2018. + */ + +public class XrpBase58 { + private static final char[] BASE58 = "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz".toCharArray(); + + private static final int BASE58_CHUNK_DIGITS = 10;//how many base 58 digits fits in long + private static final BigInteger BASE58_CHUNK_MOD = BigInteger.valueOf(0x5fa8624c7fba400L); //58^BASE58_CHUNK_DIGITS + private static final byte[] BASE58_VALUES = new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, -1, -1, -1, -1, -1, + -1, 9, 10, 11, 12, 13, 14, 15, 16, -1, 17, 18, 19, 20, 21, -1, + 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1, + -1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; + + public static byte[] decodeBase58(String input) { + if (input == null) { + return null; + } + input = input.trim(); + if (input.length() == 0) { + return new byte[0]; + } + BigInteger resultNum = BigInteger.ZERO; + int nLeadingZeros = 0; + while (nLeadingZeros < input.length() && input.charAt(nLeadingZeros) == BASE58[0]) { + nLeadingZeros++; + } + long acc = 0; + int nDigits = 0; + int p = nLeadingZeros; + while (p < input.length()) { + int v = BASE58_VALUES[input.charAt(p) & 0xff]; + if (v >= 0) { + acc *= 58; + acc += v; + nDigits++; + if (nDigits == BASE58_CHUNK_DIGITS) { + resultNum = resultNum.multiply(BASE58_CHUNK_MOD).add(BigInteger.valueOf(acc)); + acc = 0; + nDigits = 0; + } + p++; + } else { + break; + } + } + if (nDigits > 0) { + long mul = 58; + while (--nDigits > 0) { + mul *= 58; + } + resultNum = resultNum.multiply(BigInteger.valueOf(mul)).add(BigInteger.valueOf(acc)); + } + final int BASE58_SPACE = -2; + while (p < input.length() && BASE58_VALUES[input.charAt(p) & 0xff] == BASE58_SPACE) { + p++; + } + if (p < input.length()) { + return null; + } + byte[] plainNumber = resultNum.toByteArray(); + int plainNumbersOffs = plainNumber[0] == 0 ? 1 : 0; + byte[] result = new byte[nLeadingZeros + plainNumber.length - plainNumbersOffs]; + System.arraycopy(plainNumber, plainNumbersOffs, result, nLeadingZeros, plainNumber.length - plainNumbersOffs); + return result; + } + + public static String encodeBase58(byte[] input) { + if (input == null) { + return null; + } + StringBuilder str = new StringBuilder((input.length * 350) / 256 + 1); + BigInteger bn = new BigInteger(1, input); + long rem; + while (true) { + BigInteger[] divideAndRemainder = bn.divideAndRemainder(BASE58_CHUNK_MOD); + bn = divideAndRemainder[0]; + rem = divideAndRemainder[1].longValue(); + if (bn.compareTo(BigInteger.ZERO) == 0) { + break; + } + for (int i = 0; i < BASE58_CHUNK_DIGITS; i++) { + str.append(BASE58[(int) (rem % 58)]); + rem /= 58; + } + } + while (rem != 0) { + str.append(BASE58[(int) (rem % 58)]); + rem /= 58; + } + str.reverse(); + int nLeadingZeros = 0; + while (nLeadingZeros < input.length && input[nLeadingZeros] == 0) { + str.insert(0, BASE58[0]); + nLeadingZeros++; + } + return str.toString(); + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/xrp/override/XrpPayment.java b/blockchain/src/main/java/com/tangem/blockchain/xrp/override/XrpPayment.java new file mode 100644 index 0000000000..1fa01213a5 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/xrp/override/XrpPayment.java @@ -0,0 +1,18 @@ +package com.tangem.blockchain.xrp.override; + + +import com.ripple.core.types.known.tx.txns.Payment; + +public class XrpPayment extends Payment { + + public XrpPayment() { + super(); + } + + public XrpSignedTransaction prepare(byte[] pubKeyBytes) { + XrpSignedTransaction tx = XrpSignedTransaction.fromTx(this); + tx.prepare(pubKeyBytes); + return tx; + } + +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/xrp/override/XrpSignedTransaction.java b/blockchain/src/main/java/com/tangem/blockchain/xrp/override/XrpSignedTransaction.java new file mode 100644 index 0000000000..926a176adb --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/xrp/override/XrpSignedTransaction.java @@ -0,0 +1,81 @@ +package com.tangem.blockchain.xrp.override; + +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.Blob; +import com.ripple.core.coretypes.STObject; +import com.ripple.core.coretypes.hash.HalfSha512; +import com.ripple.core.coretypes.hash.prefixes.HashPrefix; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.BytesList; +import com.ripple.core.serialized.MultiSink; +import com.ripple.core.types.known.tx.Transaction; +import com.ripple.core.types.known.tx.signed.SignedTransaction; + +import java.util.Arrays; + +public class XrpSignedTransaction extends SignedTransaction { + private XrpSignedTransaction(Transaction of) { + txn = (Transaction) STObject.fromBytes(of.toBytes()); + } + + protected XrpSignedTransaction() { + } + + public static XrpSignedTransaction fromTx(Transaction tx) { + return new XrpSignedTransaction(tx); + } + + public void prepare(byte[] pubKeyBytes) { + prepare(pubKeyBytes, null, null, null); + } + + public void prepare(byte[] pubKeyBytes, + Amount fee, + UInt32 Sequence, + UInt32 lastLedgerSequence) { + + Blob pubKey = new Blob(pubKeyBytes); + + // This won't always be specified + if (lastLedgerSequence != null) { + txn.put(UInt32.LastLedgerSequence, lastLedgerSequence); + } + if (Sequence != null) { + txn.put(UInt32.Sequence, Sequence); + } + if (fee != null) { + txn.put(Amount.Fee, fee); + } + + txn.signingPubKey(pubKey); + + if (Transaction.CANONICAL_FLAG_DEPLOYED) { + txn.setCanonicalSignatureFlag(); + } + + txn.checkFormat(); + signingData = txn.signingData(); + if (previousSigningData != null && Arrays.equals(signingData, previousSigningData)) { + return; + } + } + + public void addSign(byte[] signature) { + try { + txn.txnSignature(new Blob(signature)); + + BytesList blob = new BytesList(); + HalfSha512 id = HalfSha512.prefixed256(HashPrefix.transactionID); + + txn.toBytesSink(new MultiSink(blob, id)); + tx_blob = blob.bytesHex(); + hash = id.finish(); + } catch (Exception e) { + // electric paranoia + previousSigningData = null; + throw new RuntimeException(e); + } /*else {*/ + previousSigningData = signingData; + // } + } +} diff --git a/tangem-core/src/main/java/com/tangem/common/extensions/ByteArray.kt b/tangem-core/src/main/java/com/tangem/common/extensions/ByteArray.kt index 39646d7a16..f7e416d700 100644 --- a/tangem-core/src/main/java/com/tangem/common/extensions/ByteArray.kt +++ b/tangem-core/src/main/java/com/tangem/common/extensions/ByteArray.kt @@ -1,6 +1,7 @@ package com.tangem.common.extensions import org.spongycastle.crypto.digests.RIPEMD160Digest +import org.spongycastle.jce.ECNamedCurveTable import java.nio.ByteBuffer import java.security.MessageDigest import java.util.* @@ -42,4 +43,14 @@ fun ByteArray.calculateRipemd160(): ByteArray { val out = ByteArray(20) digest.doFinal(out, 0) return out +} + +fun ByteArray.toCompressedPublicKey(): ByteArray { + return if (this.size == 65) { + val spec = ECNamedCurveTable.getParameterSpec("secp256k1") + val publicKeyPoint = spec.curve.decodePoint(this) + publicKeyPoint.getEncoded(true) + } else { + this + } } \ No newline at end of file