Updated on 2026-08-14
This commit is contained in:
parent
0ee3346f20
commit
c2632aed57
17 changed files with 769 additions and 4 deletions
|
|
@ -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'
|
||||
|
|
|
|||
BIN
blockchain/libs/ripple-core-0.0.1.jar
Normal file
BIN
blockchain/libs/ripple-core-0.0.1.jar
Normal file
Binary file not shown.
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue