Updated on 2026-08-14
This commit is contained in:
parent
894e601444
commit
1c0dfe48cd
23 changed files with 1004 additions and 47 deletions
|
|
@ -10,20 +10,20 @@ import org.bitcoinj.script.ScriptBuilder
|
|||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
class BitcoinTransactionBuilder(
|
||||
open class BitcoinTransactionBuilder(
|
||||
private val walletPublicKey: ByteArray, private val testNet: Boolean = false
|
||||
) {
|
||||
|
||||
private lateinit var transaction: Transaction
|
||||
private var networkParameters: NetworkParameters? = null
|
||||
var unspentOutputs: List<UnspentTransaction>? = null
|
||||
protected var networkParameters: NetworkParameters? = null
|
||||
var unspentOutputs: List<BitcoinUnspentOutput>? = null
|
||||
|
||||
fun buildToSign(
|
||||
open fun buildToSign(
|
||||
transactionData: TransactionData): Result<List<ByteArray>> {
|
||||
|
||||
if (unspentOutputs == null) return Result.Failure(Exception("Currently there's an unconfirmed transaction"))
|
||||
|
||||
val change: BigDecimal = calculateChange(transactionData)
|
||||
val change: BigDecimal = calculateChange(transactionData, unspentOutputs!!)
|
||||
|
||||
networkParameters = if (testNet) {
|
||||
NetworkParameters.fromID(NetworkParameters.ID_TESTNET)
|
||||
|
|
@ -40,27 +40,13 @@ class BitcoinTransactionBuilder(
|
|||
return Result.Success(hashesForSign)
|
||||
}
|
||||
|
||||
private fun calculateChange(transactionData: TransactionData): BigDecimal {
|
||||
val fullAmount = unspentOutputs!!.map { it.amount }.reduce { acc, number -> acc + number }
|
||||
return fullAmount - (transactionData.amount.value!! + (transactionData.fee?.value
|
||||
?: 0.toBigDecimal()))
|
||||
}
|
||||
|
||||
fun buildToSend(signedTransaction: ByteArray): ByteArray {
|
||||
open fun buildToSend(signedTransaction: ByteArray): ByteArray {
|
||||
for (index in transaction.inputs.indices) {
|
||||
transaction.inputs[index].scriptSig = createScript(index, signedTransaction, walletPublicKey)
|
||||
}
|
||||
return transaction.bitcoinSerialize()
|
||||
}
|
||||
|
||||
private fun createScript(index: Int, signedTransaction: ByteArray, publicKey: ByteArray): Script {
|
||||
val r = BigInteger(1, signedTransaction.copyOfRange(index * 64, 32 + index * 64))
|
||||
val s = BigInteger(1, signedTransaction.copyOfRange(32 + index * 64, 64 + index * 64))
|
||||
val canonicalS = ECKey.ECDSASignature(r, s).toCanonicalised().s
|
||||
val signature = TransactionSignature(r, canonicalS)
|
||||
return ScriptBuilder.createInputScript(signature, ECKey.fromPublicOnly(publicKey))
|
||||
}
|
||||
|
||||
fun getEstimateSize(transactionData: TransactionData): Result<Int> {
|
||||
val buildTransactionResult = buildToSign(transactionData)
|
||||
when (buildTransactionResult) {
|
||||
|
|
@ -72,14 +58,28 @@ class BitcoinTransactionBuilder(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun calculateChange(transactionData: TransactionData, unspentOutputs: List<BitcoinUnspentOutput>): BigDecimal {
|
||||
val fullAmount = unspentOutputs!!.map { it.amount }.reduce { acc, number -> acc + number }
|
||||
return fullAmount - (transactionData.amount.value!! + (transactionData.fee?.value
|
||||
?: 0.toBigDecimal()))
|
||||
}
|
||||
|
||||
open fun createScript(index: Int, signedTransaction: ByteArray, publicKey: ByteArray): Script {
|
||||
val r = BigInteger(1, signedTransaction.copyOfRange(index * 64, 32 + index * 64))
|
||||
val s = BigInteger(1, signedTransaction.copyOfRange(32 + index * 64, 64 + index * 64))
|
||||
val canonicalS = ECKey.ECDSASignature(r, s).toCanonicalised().s
|
||||
val signature = TransactionSignature(r, canonicalS)
|
||||
return ScriptBuilder.createInputScript(signature, ECKey.fromPublicOnly(publicKey))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TransactionData.toBitcoinJTransaction(networkParameters: NetworkParameters?,
|
||||
unspentOutputs: List<UnspentTransaction>,
|
||||
unspentOutputs: List<BitcoinUnspentOutput>,
|
||||
change: BigDecimal): Transaction {
|
||||
val transaction = Transaction(networkParameters)
|
||||
for (utxo in unspentOutputs) {
|
||||
transaction.addInput(Sha256Hash.wrap(utxo.hash), utxo.outputIndex, Script(utxo.outputScript))
|
||||
transaction.addInput(Sha256Hash.wrap(utxo.transactionHash), utxo.outputIndex, Script(utxo.outputScript))
|
||||
}
|
||||
transaction.addOutput(
|
||||
Coin.parseCoin(this.amount.value!!.toPlainString()),
|
||||
|
|
@ -93,9 +93,9 @@ internal fun TransactionData.toBitcoinJTransaction(networkParameters: NetworkPar
|
|||
return transaction
|
||||
}
|
||||
|
||||
class UnspentTransaction(
|
||||
class BitcoinUnspentOutput(
|
||||
val amount: BigDecimal,
|
||||
val outputIndex: Long,
|
||||
val hash: ByteArray,
|
||||
val transactionHash: ByteArray,
|
||||
val outputScript: ByteArray
|
||||
)
|
||||
|
|
@ -2,7 +2,7 @@ package com.tangem.blockchain.blockchains.bitcoin
|
|||
|
||||
import android.util.Log
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinAddressResponse
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinNetworkManager
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinProvider
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
|
|
@ -10,14 +10,14 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.extensions.toHexString
|
||||
import java.math.BigDecimal
|
||||
|
||||
class BitcoinWalletManager(
|
||||
open class BitcoinWalletManager(
|
||||
cardId: String,
|
||||
wallet: Wallet,
|
||||
private val transactionBuilder: BitcoinTransactionBuilder,
|
||||
private val networkManager: BitcoinNetworkManager
|
||||
private val networkManager: BitcoinProvider
|
||||
) : WalletManager(cardId, wallet), TransactionSender {
|
||||
|
||||
private val blockchain = wallet.blockchain
|
||||
protected val blockchain = wallet.blockchain
|
||||
|
||||
override suspend fun update() {
|
||||
val response = networkManager.getInfo(wallet.address)
|
||||
|
|
@ -30,7 +30,7 @@ class BitcoinWalletManager(
|
|||
private fun updateWallet(response: BitcoinAddressResponse) {
|
||||
Log.d(this::class.java.simpleName, "Balance is ${response.balance}")
|
||||
wallet.amounts[AmountType.Coin]?.value = response.balance
|
||||
transactionBuilder.unspentOutputs = response.unspentTransactions
|
||||
transactionBuilder.unspentOutputs = response.unspentOutputs
|
||||
if (response.hasUnconfirmed) {
|
||||
if (wallet.transactions.isEmpty()) wallet.addIncomingTransaction()
|
||||
} else {
|
||||
|
|
@ -44,8 +44,7 @@ class BitcoinWalletManager(
|
|||
}
|
||||
|
||||
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
|
||||
val buildTransactionResult = transactionBuilder.buildToSign(transactionData)
|
||||
when (buildTransactionResult) {
|
||||
when (val buildTransactionResult = transactionBuilder.buildToSign(transactionData)) {
|
||||
is Result.Failure -> return SimpleResult.Failure(buildTransactionResult.error)
|
||||
is Result.Success -> {
|
||||
when (val signerResponse = signer.sign(buildTransactionResult.data.toTypedArray(), cardId)) {
|
||||
|
|
@ -60,8 +59,8 @@ class BitcoinWalletManager(
|
|||
}
|
||||
|
||||
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {
|
||||
when (val result = networkManager.getFee()) {
|
||||
is Result.Failure -> return result
|
||||
when (val feeResult = networkManager.getFee()) {
|
||||
is Result.Failure -> return feeResult
|
||||
is Result.Success -> {
|
||||
val feeValue = BigDecimal.ONE.movePointLeft(blockchain.decimals())
|
||||
amount.value = amount.value!! - feeValue
|
||||
|
|
@ -72,9 +71,9 @@ class BitcoinWalletManager(
|
|||
is Result.Failure -> return sizeResult
|
||||
is Result.Success -> {
|
||||
val transactionSize = sizeResult.data.toBigDecimal()
|
||||
val minFee = result.data.minimalPerKb.calculateFee(transactionSize)
|
||||
val normalFee = result.data.normalPerKb.calculateFee(transactionSize)
|
||||
val priorityFee = result.data.priorityPerKb.calculateFee(transactionSize)
|
||||
val minFee = feeResult.data.minimalPerKb.calculateFee(transactionSize)
|
||||
val normalFee = feeResult.data.normalPerKb.calculateFee(transactionSize)
|
||||
val priorityFee = feeResult.data.priorityPerKb.calculateFee(transactionSize)
|
||||
return Result.Success(
|
||||
listOf(Amount(minFee, blockchain),
|
||||
Amount(normalFee, blockchain),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoin.network
|
||||
|
||||
import com.tangem.blockchain.blockchains.bitcoin.UnspentTransaction
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockchainInfoApi
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockcypherApi
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.api.EstimatefeeApi
|
||||
|
|
@ -90,7 +90,7 @@ class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinPro
|
|||
data class BitcoinAddressResponse(
|
||||
val balance: BigDecimal,
|
||||
val hasUnconfirmed: Boolean,
|
||||
val unspentTransactions: List<UnspentTransaction>?
|
||||
val unspentOutputs: List<BitcoinUnspentOutput>?
|
||||
)
|
||||
|
||||
data class BitcoinFee(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoin.network
|
||||
|
||||
import com.tangem.blockchain.blockchains.bitcoin.UnspentTransaction
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockchainInfoApi
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.api.EstimatefeeApi
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
|
|
@ -29,7 +29,7 @@ class BlockchainInfoProvider(
|
|||
val unconfirmedTransactions = addressData.transactions?.find { it.blockHeight == 0L } != null
|
||||
|
||||
val bitcoinUnspents = unspents.unspentOutputs.map {
|
||||
UnspentTransaction(
|
||||
BitcoinUnspentOutput(
|
||||
it.amount!!.toBigDecimal().movePointLeft(decimals),
|
||||
it.outputIndex!!.toLong(),
|
||||
it.hash!!.hexToBytes(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoin.network
|
||||
|
||||
import com.tangem.blockchain.blockchains.bitcoin.UnspentTransaction
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockcypherApi
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockcypherBody
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherFee
|
||||
|
|
@ -26,7 +26,7 @@ class BlockcypherProvider(private val api: BlockcypherApi, isTestNet: Boolean) :
|
|||
try {
|
||||
val addressData: BlockcypherResponse = retryIO { api.getAddressData(blockchain, network, address) }
|
||||
val unspents = addressData.txrefs?.map {
|
||||
UnspentTransaction(
|
||||
BitcoinUnspentOutput(
|
||||
it.amount!!.toBigDecimal().movePointLeft(decimals),
|
||||
it.outputIndex!!.toLong(),
|
||||
it.hash!!.hexToBytes(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoincash
|
||||
|
||||
import com.tangem.blockchain.blockchains.bitcoincash.cashaddr.BitcoinCashAddressType
|
||||
import com.tangem.blockchain.blockchains.bitcoincash.cashaddr.CashAddr
|
||||
import com.tangem.blockchain.common.AddressService
|
||||
import com.tangem.common.extensions.calculateRipemd160
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.toCompressedPublicKey
|
||||
|
||||
class BitcoinCashAddressService() : AddressService {
|
||||
override fun makeAddress(walletPublicKey: ByteArray): String {
|
||||
val publicKeyHash = walletPublicKey.toCompressedPublicKey().calculateSha256().calculateRipemd160()
|
||||
return CashAddr.toCashAddress(BitcoinCashAddressType.P2PKH, publicKeyHash)
|
||||
}
|
||||
|
||||
override fun validate(address: String): Boolean {
|
||||
return CashAddr.isValidCashAddress(address)
|
||||
}
|
||||
|
||||
fun getPublicKeyHash(address: String): ByteArray {
|
||||
return CashAddr.decodeCashAddress(address).hash
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoincash
|
||||
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinAddressResponse
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinFee
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinProvider
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.blockchain.network.API_BLOCKCHAIR
|
||||
import com.tangem.blockchain.network.blockchair.BlockchairApi
|
||||
import com.tangem.blockchain.network.blockchair.BlockchairProvider
|
||||
import com.tangem.blockchain.network.createRetrofitInstance
|
||||
|
||||
class BitcoinCashNetworkManager : BitcoinProvider {
|
||||
private val blockchain: Blockchain = Blockchain.BitcoinCash
|
||||
|
||||
private val blockchairProvider by lazy {
|
||||
val api = createRetrofitInstance(API_BLOCKCHAIR).create(BlockchairApi::class.java)
|
||||
BlockchairProvider(api, blockchain)
|
||||
}
|
||||
|
||||
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
|
||||
return blockchairProvider.getInfo(address)
|
||||
}
|
||||
|
||||
override suspend fun getFee(): Result<BitcoinFee> {
|
||||
return blockchairProvider.getFee()
|
||||
}
|
||||
|
||||
override suspend fun sendTransaction(transaction: String): SimpleResult {
|
||||
return blockchairProvider.sendTransaction(transaction)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoincash;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Sha256Hash;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionInput;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.core.UnsafeByteArrayOutputStream;
|
||||
import org.bitcoinj.core.VarInt;
|
||||
import org.bitcoinj.crypto.TransactionSignature;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
import static org.bitcoinj.core.Utils.uint32ToByteStreamLE;
|
||||
import static org.bitcoinj.core.Utils.uint64ToByteStreamLE;
|
||||
|
||||
// logic from https://github.com/pokkst/bitcoincashj
|
||||
public class BitcoinCashTransaction extends Transaction {
|
||||
// private ArrayList<TransactionInput> inputs;
|
||||
// private ArrayList<TransactionOutput> outputs;
|
||||
|
||||
// private long version;
|
||||
// private long lockTime;
|
||||
|
||||
public final byte SIGHASH_FORK_ID = 0x40;
|
||||
|
||||
public BitcoinCashTransaction(NetworkParameters params) {
|
||||
super(params);
|
||||
}
|
||||
|
||||
public synchronized Sha256Hash hashForSignatureWitness(
|
||||
int inputIndex,
|
||||
byte[] connectedScript,
|
||||
Coin prevValue,
|
||||
SigHash type,
|
||||
boolean anyoneCanPay)
|
||||
{
|
||||
byte sigHashType = (byte) TransactionSignature.calcSigHashValue(type, anyoneCanPay);
|
||||
sigHashType |= SIGHASH_FORK_ID;
|
||||
|
||||
ByteArrayOutputStream bos = new UnsafeByteArrayOutputStream(length == UNKNOWN_LENGTH ? 256 : length + 4);
|
||||
try {
|
||||
byte[] hashPrevouts = new byte[32];
|
||||
byte[] hashSequence = new byte[32];
|
||||
byte[] hashOutputs = new byte[32];
|
||||
anyoneCanPay = (sigHashType & SIGHASH_ANYONECANPAY_VALUE) == SIGHASH_ANYONECANPAY_VALUE;
|
||||
List<TransactionInput> inputs = getInputs();
|
||||
List<TransactionOutput> outputs = getOutputs();
|
||||
|
||||
if (!anyoneCanPay) {
|
||||
ByteArrayOutputStream bosHashPrevouts = new UnsafeByteArrayOutputStream(256);
|
||||
for (int i = 0; i < inputs.size(); ++i) {
|
||||
bosHashPrevouts.write(inputs.get(i).getOutpoint().getHash().getReversedBytes());
|
||||
uint32ToByteStreamLE(inputs.get(i).getOutpoint().getIndex(), bosHashPrevouts);
|
||||
}
|
||||
hashPrevouts = Sha256Hash.hashTwice(bosHashPrevouts.toByteArray());
|
||||
}
|
||||
|
||||
if (!anyoneCanPay && type != SigHash.SINGLE && type != SigHash.NONE) {
|
||||
ByteArrayOutputStream bosSequence = new UnsafeByteArrayOutputStream(256);
|
||||
for (int i = 0; i < inputs.size(); ++i) {
|
||||
uint32ToByteStreamLE(inputs.get(i).getSequenceNumber(), bosSequence);
|
||||
}
|
||||
hashSequence = Sha256Hash.hashTwice(bosSequence.toByteArray());
|
||||
}
|
||||
|
||||
if (type != SigHash.SINGLE && type != SigHash.NONE) {
|
||||
ByteArrayOutputStream bosHashOutputs = new UnsafeByteArrayOutputStream(256);
|
||||
for (int i = 0; i < outputs.size(); ++i) {
|
||||
uint64ToByteStreamLE(
|
||||
BigInteger.valueOf(outputs.get(i).getValue().getValue()),
|
||||
bosHashOutputs
|
||||
);
|
||||
bosHashOutputs.write(new VarInt(outputs.get(i).getScriptBytes().length).encode());
|
||||
bosHashOutputs.write(outputs.get(i).getScriptBytes());
|
||||
}
|
||||
hashOutputs = Sha256Hash.hashTwice(bosHashOutputs.toByteArray());
|
||||
} else if (type == SigHash.SINGLE && inputIndex < outputs.size()) {
|
||||
ByteArrayOutputStream bosHashOutputs = new UnsafeByteArrayOutputStream(256);
|
||||
uint64ToByteStreamLE(
|
||||
BigInteger.valueOf(outputs.get(inputIndex).getValue().getValue()),
|
||||
bosHashOutputs
|
||||
);
|
||||
bosHashOutputs.write(new VarInt(outputs.get(inputIndex).getScriptBytes().length).encode());
|
||||
bosHashOutputs.write(outputs.get(inputIndex).getScriptBytes());
|
||||
hashOutputs = Sha256Hash.hashTwice(bosHashOutputs.toByteArray());
|
||||
}
|
||||
uint32ToByteStreamLE(getVersion(), bos);
|
||||
bos.write(hashPrevouts);
|
||||
bos.write(hashSequence);
|
||||
bos.write(inputs.get(inputIndex).getOutpoint().getHash().getReversedBytes());
|
||||
uint32ToByteStreamLE(inputs.get(inputIndex).getOutpoint().getIndex(), bos);
|
||||
bos.write(new VarInt(connectedScript.length).encode());
|
||||
bos.write(connectedScript);
|
||||
uint64ToByteStreamLE(BigInteger.valueOf(prevValue.getValue()), bos);
|
||||
uint32ToByteStreamLE(inputs.get(inputIndex).getSequenceNumber(), bos);
|
||||
bos.write(hashOutputs);
|
||||
uint32ToByteStreamLE(getLockTime(), bos);
|
||||
uint32ToByteStreamLE(0x000000ff & sigHashType, bos);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e); // Cannot happen.
|
||||
}
|
||||
|
||||
return Sha256Hash.twiceOf(bos.toByteArray());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoincash
|
||||
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinTransactionBuilder
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.common.extensions.isZero
|
||||
import org.bitcoinj.core.*
|
||||
import org.bitcoinj.core.LegacyAddress.fromPubKeyHash
|
||||
import org.bitcoinj.crypto.TransactionSignature
|
||||
import org.bitcoinj.script.Script
|
||||
import org.bitcoinj.script.ScriptBuilder
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
class BitcoinCashTransactionBuilder(private val walletPublicKey: ByteArray)
|
||||
: BitcoinTransactionBuilder(walletPublicKey) {
|
||||
|
||||
private lateinit var transaction: BitcoinCashTransaction
|
||||
|
||||
override fun buildToSign(
|
||||
transactionData: TransactionData): Result<List<ByteArray>> {
|
||||
|
||||
if (unspentOutputs == null) return Result.Failure(Exception("Currently there's an unconfirmed transaction"))
|
||||
|
||||
val change: BigDecimal = calculateChange(transactionData, unspentOutputs!!)
|
||||
|
||||
networkParameters = NetworkParameters.fromID(NetworkParameters.ID_MAINNET)
|
||||
transaction = transactionData.toBitcoinCashTransaction(networkParameters, unspentOutputs!!, change)
|
||||
|
||||
val hashesForSign: MutableList<ByteArray> = MutableList(transaction.inputs.size) { byteArrayOf() }
|
||||
for (input in transaction.inputs) {
|
||||
val index = input.index
|
||||
val value = Coin.parseCoin(unspentOutputs!![index].amount.toString())
|
||||
hashesForSign[index] = transaction.hashForSignatureWitness(index, input.scriptBytes, value, Transaction.SigHash.ALL, false).bytes
|
||||
}
|
||||
return Result.Success(hashesForSign)
|
||||
}
|
||||
|
||||
override fun buildToSend(signedTransaction: ByteArray): ByteArray {
|
||||
for (index in transaction.inputs.indices) {
|
||||
transaction.inputs[index].scriptSig = createScript(index, signedTransaction, walletPublicKey)
|
||||
}
|
||||
return transaction.bitcoinSerialize()
|
||||
}
|
||||
|
||||
override fun createScript(index: Int, signedTransaction: ByteArray, publicKey: ByteArray): Script {
|
||||
val r = BigInteger(1, signedTransaction.copyOfRange(index * 64, 32 + index * 64))
|
||||
val s = BigInteger(1, signedTransaction.copyOfRange(32 + index * 64, 64 + index * 64))
|
||||
val canonicalS = ECKey.ECDSASignature(r, s).toCanonicalised().s
|
||||
|
||||
val sigHash = 0x41
|
||||
val signature = TransactionSignature(r, canonicalS, sigHash)
|
||||
return ScriptBuilder.createInputScript(signature, ECKey.fromPublicOnly(publicKey))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun TransactionData.toBitcoinCashTransaction(networkParameters: NetworkParameters?,
|
||||
unspentOutputs: List<BitcoinUnspentOutput>,
|
||||
change: BigDecimal): BitcoinCashTransaction {
|
||||
val transaction = BitcoinCashTransaction(networkParameters)
|
||||
for (utxo in unspentOutputs) {
|
||||
transaction.addInput(Sha256Hash.wrap(utxo.transactionHash), utxo.outputIndex, Script(utxo.outputScript))
|
||||
}
|
||||
val addressService = BitcoinCashAddressService()
|
||||
val sourceLegacyAddress =
|
||||
fromPubKeyHash(networkParameters, addressService.getPublicKeyHash(this.sourceAddress))
|
||||
val destinationLegacyAddress =
|
||||
fromPubKeyHash(networkParameters, addressService.getPublicKeyHash(this.destinationAddress))
|
||||
|
||||
transaction.addOutput(
|
||||
Coin.parseCoin(this.amount.value!!.toPlainString()),
|
||||
destinationLegacyAddress
|
||||
)
|
||||
if (!change.isZero()) {
|
||||
transaction.addOutput(
|
||||
Coin.parseCoin(change.toPlainString()),
|
||||
sourceLegacyAddress
|
||||
)
|
||||
}
|
||||
return transaction
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoincash
|
||||
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.TransactionSender
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import java.math.BigDecimal
|
||||
|
||||
class BitcoinCashWalletManager(
|
||||
cardId: String,
|
||||
wallet: Wallet,
|
||||
private val transactionBuilder: BitcoinCashTransactionBuilder,
|
||||
private val networkManager: BitcoinCashNetworkManager
|
||||
) : BitcoinWalletManager(cardId, wallet, transactionBuilder, networkManager), TransactionSender {
|
||||
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {
|
||||
val minimalFee = BigDecimal("0.00001")
|
||||
when (val result = super.getFee(amount, destination)) {
|
||||
is Result.Success -> {
|
||||
for (fee in result.data) {
|
||||
if (fee.value!! < minimalFee) fee.value = minimalFee
|
||||
}
|
||||
return result
|
||||
}
|
||||
is Result.Failure -> return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoincash.cashaddr;
|
||||
|
||||
// Helper class for CashAddr
|
||||
|
||||
public class BitcoinCashAddressDecodedParts {
|
||||
|
||||
String prefix;
|
||||
|
||||
BitcoinCashAddressType addressType;
|
||||
|
||||
byte[] hash;
|
||||
|
||||
public String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public void setPrefix(String prefix) {
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
public BitcoinCashAddressType getAddressType() {
|
||||
return addressType;
|
||||
}
|
||||
|
||||
public void setAddressType(BitcoinCashAddressType addressType) {
|
||||
this.addressType = addressType;
|
||||
}
|
||||
|
||||
public byte[] getHash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
public void setHash(byte[] hash) {
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoincash.cashaddr;
|
||||
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018 Tobias Brandt
|
||||
*
|
||||
* Distributed under the MIT software license, see the accompanying file LICENSE
|
||||
* or http://www.opensource.org/licenses/mit-license.php.
|
||||
*/
|
||||
public enum BitcoinCashAddressType {
|
||||
|
||||
P2PKH((byte) 0), P2SH((byte) 8);
|
||||
|
||||
private final byte versionByte;
|
||||
|
||||
BitcoinCashAddressType(byte versionByte) {
|
||||
this.versionByte = versionByte;
|
||||
}
|
||||
|
||||
public byte getVersionByte() {
|
||||
return versionByte;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoincash.cashaddr;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018 Tobias Brandt
|
||||
*
|
||||
* Distributed under the MIT software license, see the accompanying file LICENSE
|
||||
* or http://www.opensource.org/licenses/mit-license.php.
|
||||
*/
|
||||
public class BitcoinCashBase32 {
|
||||
|
||||
public static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
|
||||
private static final char[] CHARS = CHARSET.toCharArray();
|
||||
|
||||
private static Map<Character, Integer> charPositionMap;
|
||||
static {
|
||||
charPositionMap = new HashMap<>();
|
||||
for (int i = 0; i < CHARS.length; i++) {
|
||||
charPositionMap.put(CHARS[i], i);
|
||||
}
|
||||
if (charPositionMap.size() != 32) {
|
||||
throw new RuntimeException("The charset must contain 32 unique characters.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a byte array as base32 string. This method assumes that all bytes
|
||||
* are only from 0-31
|
||||
*
|
||||
* @param byteArray
|
||||
* @return
|
||||
*/
|
||||
public static String encode(byte[] byteArray) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
|
||||
for (int i = 0; i < byteArray.length; i++) {
|
||||
int val = (int) byteArray[i];
|
||||
|
||||
if (val < 0 || val > 31) {
|
||||
throw new RuntimeException("This method assumes that all bytes are only from 0-31. Was: " + val);
|
||||
}
|
||||
|
||||
sb.append(CHARS[val]);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a base32 string back to the byte array representation
|
||||
*
|
||||
* @param base32String
|
||||
* @return
|
||||
*/
|
||||
public static byte[] decode(String base32String) {
|
||||
byte[] bytes = new byte[base32String.length()];
|
||||
|
||||
char[] charArray = base32String.toCharArray();
|
||||
for (int i = 0; i < charArray.length; i++) {
|
||||
Integer position = charPositionMap.get(charArray[i]);
|
||||
if (position == null) {
|
||||
throw new RuntimeException("There seems to be an invalid char: " + charArray[i]);
|
||||
}
|
||||
bytes[i] = (byte) ((int) position);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoincash.cashaddr;
|
||||
/**
|
||||
* Copyright (c) 2018 Tobias Brandt
|
||||
*
|
||||
* Copyright (c) 2017 Pieter Wuille
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
public class BitcoinCashBitArrayConverter {
|
||||
|
||||
public static byte[] convertBits(byte[] bytes8Bits, int from, int to, boolean strictMode) {
|
||||
int length = (int) (strictMode ? Math.floor((double) bytes8Bits.length * from / to)
|
||||
: Math.ceil((double) bytes8Bits.length * from / to));
|
||||
int mask = ((1 << to) - 1) & 0xff;
|
||||
byte[] result = new byte[length];
|
||||
int index = 0;
|
||||
int accumulator = 0;
|
||||
int bits = 0;
|
||||
for (int i = 0; i < bytes8Bits.length; i++) {
|
||||
byte value = bytes8Bits[i];
|
||||
accumulator = (((accumulator & 0xff) << from) | (value & 0xff));
|
||||
bits += from;
|
||||
while (bits >= to) {
|
||||
bits -= to;
|
||||
result[index] = (byte) ((accumulator >> bits) & mask);
|
||||
++index;
|
||||
}
|
||||
}
|
||||
if (!strictMode) {
|
||||
if (bits > 0) {
|
||||
result[index] = (byte) ((accumulator << (to - bits)) & mask);
|
||||
++index;
|
||||
}
|
||||
} else {
|
||||
if (!(bits < from && ((accumulator << (to - bits)) & mask) == 0)) {
|
||||
throw new RuntimeException("Strict mode was used but input couldn't be converted without padding");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoincash.cashaddr;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
/**
|
||||
* Copyright (c) 2018 Tobias Brandt
|
||||
*
|
||||
* Distributed under the MIT software license, see the accompanying file LICENSE
|
||||
* or http://www.opensource.org/licenses/mit-license.php.
|
||||
*/
|
||||
|
||||
|
||||
public class CashAddr {
|
||||
|
||||
public static final String CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
|
||||
private static final char[] CHARS = CHARSET.toCharArray();
|
||||
|
||||
|
||||
public static final String SEPARATOR = ":";
|
||||
|
||||
public static final String MAIN_NET_PREFIX = "bitcoincash";
|
||||
|
||||
public static final String TEST_NET_PREFIX = "bchtest";
|
||||
|
||||
public static final String ASSUMED_DEFAULT_PREFIX = MAIN_NET_PREFIX;
|
||||
|
||||
private static final BigInteger[] POLYMOD_GENERATORS = new BigInteger[] { new BigInteger("98f2bc8e61", 16),
|
||||
new BigInteger("79b76d99e2", 16), new BigInteger("f33e5fb3c4", 16), new BigInteger("ae2eabe2a8", 16),
|
||||
new BigInteger("1e4f43e470", 16) };
|
||||
|
||||
private static final BigInteger POLYMOD_AND_CONSTANT = new BigInteger("07ffffffff", 16);
|
||||
|
||||
public static String toCashAddress(BitcoinCashAddressType addressType, byte[] hash) {
|
||||
String prefixString = MAIN_NET_PREFIX;
|
||||
byte[] prefixBytes = getPrefixBytes(prefixString);
|
||||
byte[] payloadBytes = concatenateByteArrays(new byte[] { addressType.getVersionByte() }, hash);
|
||||
payloadBytes = convertBits(payloadBytes, 8, 5, false);
|
||||
byte[] allChecksumInput = concatenateByteArrays(
|
||||
concatenateByteArrays(concatenateByteArrays(prefixBytes, new byte[] { 0 }), payloadBytes),
|
||||
new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 });
|
||||
byte[] checksumBytes = calculateChecksumBytesPolymod(allChecksumInput);
|
||||
checksumBytes = convertBits(checksumBytes, 8, 5, true);
|
||||
String cashAddress = BitcoinCashBase32.encode(concatenateByteArrays(payloadBytes, checksumBytes));
|
||||
return prefixString + SEPARATOR + cashAddress;
|
||||
}
|
||||
|
||||
public static BitcoinCashAddressDecodedParts decodeCashAddress(String bitcoinCashAddress) {
|
||||
if (!isValidCashAddress(bitcoinCashAddress)) {
|
||||
throw new RuntimeException("Address wasn't valid: " + bitcoinCashAddress);
|
||||
}
|
||||
|
||||
BitcoinCashAddressDecodedParts decoded = new BitcoinCashAddressDecodedParts();
|
||||
String[] addressParts = bitcoinCashAddress.split(SEPARATOR);
|
||||
if (addressParts.length == 2) {
|
||||
decoded.setPrefix(addressParts[0]);
|
||||
} else {
|
||||
decoded.setPrefix(MAIN_NET_PREFIX);
|
||||
}
|
||||
|
||||
byte[] addressData = BitcoinCashBase32.decode(addressParts[addressParts.length - 1]);
|
||||
addressData = Arrays.copyOfRange(addressData, 0, addressData.length - 8);
|
||||
addressData = BitcoinCashBitArrayConverter.convertBits(addressData, 5, 8, true);
|
||||
byte versionByte = addressData[0];
|
||||
byte[] hash = Arrays.copyOfRange(addressData, 1, addressData.length);
|
||||
|
||||
decoded.setAddressType(getAddressTypeFromVersionByte(versionByte));
|
||||
decoded.setHash(hash);
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
private static BitcoinCashAddressType getAddressTypeFromVersionByte(byte versionByte) {
|
||||
for (BitcoinCashAddressType addressType : BitcoinCashAddressType.values()) {
|
||||
if (addressType.getVersionByte() == versionByte) {
|
||||
return addressType;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException("Unknown version byte: " + versionByte);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static boolean isValidCashAddress(String bitcoinCashAddress ) {
|
||||
try {
|
||||
if (!isSingleCase(bitcoinCashAddress))
|
||||
return false;
|
||||
|
||||
bitcoinCashAddress = bitcoinCashAddress.toLowerCase();
|
||||
String prefix;
|
||||
|
||||
if (bitcoinCashAddress.contains(SEPARATOR)) {
|
||||
String[] split = bitcoinCashAddress.split(SEPARATOR);
|
||||
prefix = split[0];
|
||||
if (!prefix.equals(MAIN_NET_PREFIX)) {return false;} //for now we use main net only
|
||||
bitcoinCashAddress = split[1];
|
||||
} else {
|
||||
prefix = MAIN_NET_PREFIX;
|
||||
}
|
||||
if (!bitcoinCashAddress.startsWith("q")) {return false;} //for now we use P2PKH addresses only
|
||||
|
||||
byte[] checksumData = concatenateByteArrays(
|
||||
concatenateByteArrays(getPrefixBytes(prefix ), new byte[] { 0x00 }),
|
||||
BitcoinCashBase32.decode(bitcoinCashAddress));
|
||||
|
||||
byte[] calculateChecksumBytesPolymod = calculateChecksumBytesPolymod(checksumData);
|
||||
return new BigInteger(calculateChecksumBytesPolymod).compareTo(BigInteger.ZERO) == 0;
|
||||
} catch (RuntimeException re) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static boolean isSingleCase(String bitcoinCashAddress) {
|
||||
if (bitcoinCashAddress.equals(bitcoinCashAddress.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
if (bitcoinCashAddress.equals(bitcoinCashAddress.toUpperCase())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param checksumInput
|
||||
* @return Returns a 40 bits checksum in form of 5 8-bit arrays. This still has
|
||||
* to me mapped to 5-bit array representation
|
||||
*/
|
||||
private static byte[] calculateChecksumBytesPolymod(byte[] checksumInput) {
|
||||
BigInteger c = BigInteger.ONE;
|
||||
|
||||
for (int i = 0; i < checksumInput.length; i++) {
|
||||
byte c0 = c.shiftRight(35).byteValue();
|
||||
c = c.and(POLYMOD_AND_CONSTANT).shiftLeft(5)
|
||||
.xor(new BigInteger(String.format("%02x", checksumInput[i]), 16));
|
||||
|
||||
if ((c0 & 0x01) != 0)
|
||||
c = c.xor(POLYMOD_GENERATORS[0]);
|
||||
if ((c0 & 0x02) != 0)
|
||||
c = c.xor(POLYMOD_GENERATORS[1]);
|
||||
if ((c0 & 0x04) != 0)
|
||||
c = c.xor(POLYMOD_GENERATORS[2]);
|
||||
if ((c0 & 0x08) != 0)
|
||||
c = c.xor(POLYMOD_GENERATORS[3]);
|
||||
if ((c0 & 0x10) != 0)
|
||||
c = c.xor(POLYMOD_GENERATORS[4]);
|
||||
}
|
||||
|
||||
byte[] checksum = c.xor(BigInteger.ONE).toByteArray();
|
||||
if (checksum.length == 5) {
|
||||
return checksum;
|
||||
} else {
|
||||
byte[] newChecksumArray = new byte[5];
|
||||
|
||||
System.arraycopy(checksum, Math.max(0, checksum.length - 5), newChecksumArray,
|
||||
Math.max(0, 5 - checksum.length), Math.min(5, checksum.length));
|
||||
|
||||
return newChecksumArray;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static byte[] getPrefixBytes(String prefixString ) {
|
||||
byte[] prefixBytes = new byte[prefixString.length()];
|
||||
|
||||
char[] charArray = prefixString.toCharArray();
|
||||
for (int i = 0; i < charArray.length; i++) {
|
||||
prefixBytes[i] = (byte) (charArray[i] & 0x1f);
|
||||
}
|
||||
|
||||
return prefixBytes;
|
||||
}
|
||||
|
||||
private static byte[] concatenateByteArrays(byte[] first, byte[] second) {
|
||||
byte[] concatenatedBytes = new byte[first.length + second.length];
|
||||
|
||||
System.arraycopy(first, 0, concatenatedBytes, 0, first.length);
|
||||
System.arraycopy(second, 0, concatenatedBytes, first.length, second.length);
|
||||
|
||||
return concatenatedBytes;
|
||||
}
|
||||
|
||||
private static byte[] convertBits(byte[] bytes8Bits, int from, int to, boolean strictMode) {
|
||||
//Copyright (c) 2017 Pieter Wuille
|
||||
|
||||
int length = (int) (strictMode ? Math.floor((double) bytes8Bits.length * from / to)
|
||||
: Math.ceil((double) bytes8Bits.length * from / to));
|
||||
int mask = ((1 << to) - 1) & 0xff;
|
||||
byte[] result = new byte[length];
|
||||
int index = 0;
|
||||
int accumulator = 0;
|
||||
int bits = 0;
|
||||
for (int i = 0; i < bytes8Bits.length; i++) {
|
||||
byte value = bytes8Bits[i];
|
||||
accumulator = (((accumulator & 0xff) << from) | (value & 0xff));
|
||||
bits += from;
|
||||
while (bits >= to) {
|
||||
bits -= to;
|
||||
result[index] = (byte) ((accumulator >> bits) & mask);
|
||||
++index;
|
||||
}
|
||||
}
|
||||
if (!strictMode) {
|
||||
if (bits > 0) {
|
||||
result[index] = (byte) ((accumulator << (to - bits)) & mask);
|
||||
++index;
|
||||
}
|
||||
} else {
|
||||
if (!(bits < from && ((accumulator << (to - bits)) & mask) == 0)) {
|
||||
throw new RuntimeException("Strict mode was used but input couldn't be converted without padding");
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package com.tangem.blockchain.common
|
|||
|
||||
import com.tangem.blockchain.blockchains.binance.BinanceAddressService
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressService
|
||||
import com.tangem.blockchain.blockchains.bitcoincash.BitcoinCashAddressService
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoAddressService
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumAddressService
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarAddressService
|
||||
|
|
@ -17,6 +18,7 @@ enum class Blockchain(
|
|||
Unknown("", "", ""),
|
||||
Bitcoin("BTC", "BTC", "Bitcoin"),
|
||||
BitcoinTestnet("BTC/test", "BTCt", "Bitcoin Testnet"),
|
||||
BitcoinCash("BCH", "BCH", "Bitcoin Cash"),
|
||||
Ethereum("ETH", "ETH", "Ethereum"),
|
||||
Rootstock("", "", ""),
|
||||
Cardano("CARDANO", "ADA", "Cardano"),
|
||||
|
|
@ -32,7 +34,7 @@ enum class Blockchain(
|
|||
}
|
||||
|
||||
fun decimals(): Int = when (this) {
|
||||
Bitcoin, BitcoinTestnet, Binance, BinanceTestnet -> 8
|
||||
Bitcoin, BitcoinTestnet, BitcoinCash, Binance, BinanceTestnet -> 8
|
||||
Cardano, XRP -> 6
|
||||
Ethereum, Rootstock -> 18
|
||||
Stellar -> 7
|
||||
|
|
@ -51,6 +53,7 @@ enum class Blockchain(
|
|||
Unknown -> throw Exception("unsupported blockchain")
|
||||
Bitcoin -> BitcoinAddressService()
|
||||
BitcoinTestnet -> BitcoinAddressService(true)
|
||||
BitcoinCash -> BitcoinCashAddressService()
|
||||
Ethereum -> EthereumAddressService()
|
||||
Rootstock -> throw Exception("unsupported blockchain")
|
||||
Cardano -> CardanoAddressService()
|
||||
|
|
@ -70,6 +73,8 @@ enum class Blockchain(
|
|||
fun getExploreUrl(address: String, token: Token? = null): String = when (this) {
|
||||
Binance -> "https://explorer.binance.org/address/$address"
|
||||
Bitcoin -> "https://blockchain.info/address/$address"
|
||||
BitcoinTestnet -> "https://live.blockcypher.com/btc-testnet/address/$address"
|
||||
BitcoinCash -> "https://blockchair.com/bitcoin-cash/address/$address"
|
||||
Cardano -> "https://cardanoexplorer.com/address/$address"
|
||||
Ethereum -> if (token == null) {
|
||||
"https://etherscan.io/address/"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import com.tangem.blockchain.blockchains.binance.network.BinanceNetworkManager
|
|||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinTransactionBuilder
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinNetworkManager
|
||||
import com.tangem.blockchain.blockchains.bitcoincash.BitcoinCashNetworkManager
|
||||
import com.tangem.blockchain.blockchains.bitcoincash.BitcoinCashTransactionBuilder
|
||||
import com.tangem.blockchain.blockchains.bitcoincash.BitcoinCashWalletManager
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoTransactionBuilder
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoWalletManager
|
||||
import com.tangem.blockchain.blockchains.cardano.network.CardanoNetworkManager
|
||||
|
|
@ -21,6 +24,7 @@ import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
|
|||
import com.tangem.blockchain.blockchains.xrp.XrpWalletManager
|
||||
import com.tangem.blockchain.blockchains.xrp.network.XrpNetworkManager
|
||||
import com.tangem.commands.Card
|
||||
import com.tangem.common.extensions.toCompressedPublicKey
|
||||
|
||||
object WalletManagerFactory {
|
||||
|
||||
|
|
@ -50,6 +54,13 @@ object WalletManagerFactory {
|
|||
BitcoinNetworkManager(true)
|
||||
)
|
||||
}
|
||||
Blockchain.BitcoinCash -> {
|
||||
return BitcoinCashWalletManager(
|
||||
card.cardId, wallet,
|
||||
BitcoinCashTransactionBuilder(walletPublicKey.toCompressedPublicKey()),
|
||||
BitcoinCashNetworkManager()
|
||||
)
|
||||
}
|
||||
Blockchain.Ethereum -> {
|
||||
val chain = Chain.Mainnet
|
||||
return EthereumWalletManager(
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ const val API_STELLAR = "https://horizon.stellar.org/"
|
|||
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://explorer3.adalite.io"
|
||||
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"
|
||||
const val API_ADALITE = "https://explorer3.adalite.io/"
|
||||
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/"
|
||||
const val API_BLOCKCHAIR = "https://api.blockchair.com/"
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.tangem.blockchain.network.blockchair
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import retrofit2.http.*
|
||||
|
||||
interface BlockchairApi {
|
||||
@GET("{blockchain}/dashboards/address/{address}")
|
||||
suspend fun getAddressData(
|
||||
@Path("address") address: String,
|
||||
@Path("blockchain") blockchain: String,
|
||||
@Query("key") key: String
|
||||
): BlockchairAddress
|
||||
|
||||
@GET("{blockchain}/dashboards/transaction/{transaction}")
|
||||
suspend fun getTransaction(
|
||||
@Path("transaction") transactionHash: String,
|
||||
@Path("blockchain") blockchain: String,
|
||||
@Query("key") key: String
|
||||
): BlockchairTransaction
|
||||
|
||||
@GET("{blockchain}/stats")
|
||||
suspend fun getBlockchainStats(
|
||||
@Path("blockchain") blockchain: String,
|
||||
@Query("key") key: String
|
||||
): BlockchairStats
|
||||
|
||||
@POST("{blockchain}/push/transaction")
|
||||
suspend fun sendTransaction(
|
||||
@Body sendBody: BlockchairBody,
|
||||
@Path("blockchain") blockchain: String,
|
||||
@Query("key") key: String
|
||||
)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class BlockchairBody(val data: String)
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package com.tangem.blockchain.network.blockchair
|
||||
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinAddressResponse
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinFee
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinProvider
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.blockchain.extensions.retryIO
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
class BlockchairProvider(private val api: BlockchairApi, blockchain: Blockchain) : BitcoinProvider {
|
||||
private val blockchainPath = when (blockchain) {
|
||||
Blockchain.BitcoinCash -> "bitcoin-cash"
|
||||
else -> throw Exception("${blockchain.fullName} blockchain is not supported by BlockchairProvider")
|
||||
}
|
||||
private val decimals = blockchain.decimals()
|
||||
|
||||
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
|
||||
return try {
|
||||
val blockchairAddress = retryIO { api.getAddressData(address, blockchainPath, API_KEY) }
|
||||
|
||||
val addressData = blockchairAddress.data!!.getValue(address)
|
||||
val addressInfo = addressData.addressInfo!!
|
||||
val script = addressInfo.script!!.hexToBytes()
|
||||
|
||||
val hasUnconfirmed = checkHasUnconfirmed(addressData)
|
||||
|
||||
val unspentTransactions = addressData.unspentOutputs!!.map {
|
||||
BitcoinUnspentOutput(
|
||||
amount = it.amount!!.toBigDecimal().movePointLeft(decimals),
|
||||
outputIndex = it.index!!.toLong(),
|
||||
transactionHash = it.transactionHash!!.hexToBytes(),
|
||||
outputScript = script
|
||||
)
|
||||
}
|
||||
|
||||
Result.Success(BitcoinAddressResponse(
|
||||
balance = addressInfo.balance!!.toBigDecimal().movePointLeft(decimals),
|
||||
hasUnconfirmed = hasUnconfirmed,
|
||||
unspentOutputs = unspentTransactions
|
||||
))
|
||||
} catch (error: Exception) {
|
||||
Result.Failure(error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private suspend fun checkHasUnconfirmed(addressData: BlockchairAddressData): Boolean {
|
||||
for (utxo in addressData.unspentOutputs!!) { //check utxos first
|
||||
if (utxo.block == -1) return true
|
||||
}
|
||||
|
||||
return if (addressData.addressInfo!!.balance != 0L) { // if balance is not zero, unconfirmed tx should have unconfirmed utxo
|
||||
false
|
||||
} else {
|
||||
if (addressData.transactions!!.isEmpty()) { // no transactions from this address ever
|
||||
false
|
||||
} else { // check last transaction in case it spent all funds
|
||||
val lastTransactionHash = addressData.transactions[0]
|
||||
val blockchairTransaction = retryIO {
|
||||
api.getTransaction(lastTransactionHash, blockchainPath, API_KEY)
|
||||
}
|
||||
blockchairTransaction.data!!.getValue(lastTransactionHash).transaction!!.block == -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getFee(): Result<BitcoinFee> {
|
||||
return try {
|
||||
val stats = retryIO { api.getBlockchainStats(blockchainPath, API_KEY) }
|
||||
val feePerKb = (stats.data!!.feePerByte!! * 1024).toBigDecimal().movePointLeft(decimals)
|
||||
Result.Success(BitcoinFee(
|
||||
minimalPerKb = (feePerKb * BigDecimal.valueOf(0.8)).setScale(decimals, RoundingMode.DOWN),
|
||||
normalPerKb = feePerKb.setScale(decimals, RoundingMode.DOWN),
|
||||
priorityPerKb = (feePerKb * BigDecimal.valueOf(1.2)).setScale(decimals, RoundingMode.DOWN)
|
||||
))
|
||||
} catch (error: Exception) {
|
||||
Result.Failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendTransaction(transaction: String): SimpleResult {
|
||||
return try {
|
||||
retryIO { api.sendTransaction(BlockchairBody(transaction), blockchainPath, API_KEY) }
|
||||
SimpleResult.Success
|
||||
} catch (error: Exception) {
|
||||
SimpleResult.Failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val API_KEY = "A___0Shpsu4KagE7oSabrw20DfXAqWlT"
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package com.tangem.blockchain.network.blockchair
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class BlockchairAddress(
|
||||
@Json(name = "data")
|
||||
val data: Map<String, BlockchairAddressData>? = null
|
||||
)
|
||||
|
||||
data class BlockchairAddressData(
|
||||
@Json(name = "address")
|
||||
val addressInfo: BlockchairAddressInfo? = null,
|
||||
|
||||
@Json(name = "utxo")
|
||||
val unspentOutputs: List<BlockchairUnspentOutput>? = null,
|
||||
|
||||
@Json(name = "transactions")
|
||||
val transactions: List<String>? = null
|
||||
)
|
||||
|
||||
data class BlockchairAddressInfo(
|
||||
@Json(name = "balance")
|
||||
val balance: Long? = null,
|
||||
|
||||
@Json(name = "script_hex")
|
||||
val script: String? = null,
|
||||
|
||||
@Json(name = "output_count")
|
||||
val outputCount: Int? = null,
|
||||
|
||||
@Json(name = "unspent_output_count")
|
||||
val unspentOutputCount: Int? = null
|
||||
)
|
||||
|
||||
data class BlockchairUnspentOutput(
|
||||
@Json(name = "block_id")
|
||||
val block: Int? = null,
|
||||
|
||||
@Json(name = "transaction_hash")
|
||||
val transactionHash: String? = null,
|
||||
|
||||
@Json(name = "index")
|
||||
val index: Int? = null,
|
||||
|
||||
@Json(name = "value")
|
||||
val amount: Long? = null
|
||||
)
|
||||
|
||||
data class BlockchairTransaction(
|
||||
@Json(name = "data")
|
||||
val data: Map<String, BlockchairTransactionData>? = null
|
||||
)
|
||||
|
||||
data class BlockchairTransactionData(
|
||||
@Json(name = "transaction")
|
||||
val transaction: BlockchairTransactionInfo? = null
|
||||
)
|
||||
|
||||
data class BlockchairTransactionInfo(
|
||||
@Json(name = "block_id")
|
||||
val block: Int? = null
|
||||
)
|
||||
|
||||
data class BlockchairStats(
|
||||
@Json(name = "data")
|
||||
val data: BlockchairStatsData? = null
|
||||
)
|
||||
|
||||
data class BlockchairStatsData(
|
||||
@Json(name = "suggested_transaction_fee_per_byte_sat")
|
||||
val feePerByte: Int? = null
|
||||
)
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoincash
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toCompressedPublicKey
|
||||
import org.junit.Test
|
||||
|
||||
class BitcoinCashAddressTest {
|
||||
|
||||
private val addressService = BitcoinCashAddressService()
|
||||
|
||||
@Test
|
||||
fun makeAddressFromCorrectPublicKey() {
|
||||
val walletPublicKey = "04BE37CD5251C8999EDBBFC759D800EB41E4DCB718289601EB15819404E1B2F2ED90FE50C2A481D06EC790D1EF6184974EB655ABAE4BE56A6D1C9E1A17B1EFDF02".hexToBytes()
|
||||
val expected = "bitcoincash:qp7atyzvetwq8a0x02y2snvnns5jfwnzacf9vfa4x3"
|
||||
|
||||
Truth.assertThat(addressService.makeAddress(walletPublicKey.toCompressedPublicKey()))
|
||||
.isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateCorrectAddress() {
|
||||
val address = "bitcoincash:qp7atyzvetwq8a0x02y2snvnns5jfwnzacf9vfa4x3"
|
||||
Truth.assertThat(addressService.validate(address))
|
||||
.isTrue()
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.google.common.truth.Truth
|
|||
import com.tangem.SessionEnvironment
|
||||
import com.tangem.blockchain.blockchains.binance.BinanceWalletManager
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager
|
||||
import com.tangem.blockchain.blockchains.bitcoincash.BitcoinCashWalletManager
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoWalletManager
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarWalletManager
|
||||
|
|
@ -80,4 +81,15 @@ internal class WalletManagerFactoryTest {
|
|||
Truth.assertThat(walletManager)
|
||||
.isInstanceOf(BinanceWalletManager::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createBitcoinCashWalletManager() {
|
||||
val data = "0108BB00000000000049200754414E47454D00020102800A322E3432642053444B00034104766A1586D164B436E5D420AED01FDAB41B2AE7EDF0C865D7AF1DA995D70AB297E5B94B761CFBB405084C21BC97C02B4A1EA9ED4F515576EAB4D83AD3A0DFAA8A0A04041E76310C618102FFFF8A0101820407E4041B830B54414E47454D2053444B00840342434886408058F0F628C2466B09ECEB13F2A8EFDD4558F5D2DBDA9BD0628EE8C8CC99A778FF0F1AECD35704B9F3518486EA5C1D20F9DFCBAA66184F4CCCD9282E2632882C3041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A0070100604104BE37CD5251C8999EDBBFC759D800EB41E4DCB718289601EB15819404E1B2F2ED90FE50C2A481D06EC790D1EF6184974EB655ABAE4BE56A6D1C9E1A17B1EFDF0262040001869A6304000000060F01009000"
|
||||
val responseApdu = ResponseApdu(data.hexToBytes())
|
||||
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
|
||||
|
||||
Truth.assertThat(walletManager)
|
||||
.isInstanceOf(BitcoinCashWalletManager::class.java)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue