Updated on 2026-08-14

This commit is contained in:
Tangem 2020-02-11 13:22:13 +03:00
parent 0ee3346f20
commit c2632aed57
17 changed files with 769 additions and 4 deletions

View file

@ -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'

Binary file not shown.

View file

@ -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")

View file

@ -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
}
}

View file

@ -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()
}

View file

@ -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"
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"

View file

@ -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
}
}
}
}

View file

@ -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()
}
}

View file

@ -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<List<Amount>> {
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)
))
}
}
}

View file

@ -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<XrpInfoResponse> {
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<XrpFeeResponse> {
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
)

View file

@ -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<String, String> = HashMap() //TODO =null?
)
val serverStateBody = RippledBody(RippledMethod.SERVER_STATE.value)
val feeBody = RippledBody(RippledMethod.FEE.value)

View file

@ -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<XrpInfoResponse> {
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<XrpFeeResponse> {
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<String, String>()
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<String, String>()
params["tx_blob"] = transaction
return RippledBody(RippledMethod.SUBMIT.value, params)
}

View file

@ -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
)

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
// }
}
}

View file

@ -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
}
}