diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt index 24f072b218..ef0cbe5500 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt @@ -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? = null + protected var networkParameters: NetworkParameters? = null + var unspentOutputs: List? = null - fun buildToSign( + open fun buildToSign( transactionData: TransactionData): Result> { 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 { val buildTransactionResult = buildToSign(transactionData) when (buildTransactionResult) { @@ -72,14 +58,28 @@ class BitcoinTransactionBuilder( } } } + + fun calculateChange(transactionData: TransactionData, unspentOutputs: List): 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, + unspentOutputs: List, 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 ) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinWalletManager.kt index 8d38566e23..0670f32ad4 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinWalletManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinWalletManager.kt @@ -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> { - 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), diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BitcoinNetworkManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BitcoinNetworkManager.kt index 5810ede23f..8483883d82 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BitcoinNetworkManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BitcoinNetworkManager.kt @@ -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? + val unspentOutputs: List? ) data class BitcoinFee( diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BlockchainInfoProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BlockchainInfoProvider.kt index b26b05debc..cb9dc12e6a 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BlockchainInfoProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BlockchainInfoProvider.kt @@ -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(), diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BlockcypherProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BlockcypherProvider.kt index 17da8cced0..58b6bec6a4 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BlockcypherProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BlockcypherProvider.kt @@ -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(), diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashAddressService.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashAddressService.kt new file mode 100644 index 0000000000..20e41a0b49 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashAddressService.kt @@ -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 + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashNetworkManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashNetworkManager.kt new file mode 100644 index 0000000000..5a594aa312 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashNetworkManager.kt @@ -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 { + return blockchairProvider.getInfo(address) + } + + override suspend fun getFee(): Result { + return blockchairProvider.getFee() + } + + override suspend fun sendTransaction(transaction: String): SimpleResult { + return blockchairProvider.sendTransaction(transaction) + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashTransaction.java b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashTransaction.java new file mode 100644 index 0000000000..ec339789c1 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashTransaction.java @@ -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 inputs; +// private ArrayList 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 inputs = getInputs(); + List 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()); + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashTransactionBuilder.kt new file mode 100644 index 0000000000..58bec1f6e3 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashTransactionBuilder.kt @@ -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> { + + 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 = 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, + 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 +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashWalletManager.kt new file mode 100644 index 0000000000..b0793c6cf9 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashWalletManager.kt @@ -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> { + 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 + } + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/cashaddr/BitcoinCashAddressDecodedParts.java b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/cashaddr/BitcoinCashAddressDecodedParts.java new file mode 100644 index 0000000000..b3675c1f52 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/cashaddr/BitcoinCashAddressDecodedParts.java @@ -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; + } + +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/cashaddr/BitcoinCashAddressType.java b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/cashaddr/BitcoinCashAddressType.java new file mode 100644 index 0000000000..8654b29a1c --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/cashaddr/BitcoinCashAddressType.java @@ -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; + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/cashaddr/BitcoinCashBase32.java b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/cashaddr/BitcoinCashBase32.java new file mode 100644 index 0000000000..04f3fef9f4 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/cashaddr/BitcoinCashBase32.java @@ -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 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; + } +} + + \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/cashaddr/BitcoinCashBitArrayConverter.java b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/cashaddr/BitcoinCashBitArrayConverter.java new file mode 100644 index 0000000000..ec0f980198 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/cashaddr/BitcoinCashBitArrayConverter.java @@ -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; + } + +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/cashaddr/CashAddr.java b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/cashaddr/CashAddr.java new file mode 100644 index 0000000000..8c2cd1bfab --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/cashaddr/CashAddr.java @@ -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; + } + + + + +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt index f2be55b6f0..34920ecaca 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt @@ -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/" diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt index b5bf3032f2..b0a50afe28 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt @@ -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( diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/RetrofitBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/network/RetrofitBuilder.kt index a3ecaaac41..bdd4eca666 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/RetrofitBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/RetrofitBuilder.kt @@ -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" \ No newline at end of file +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/" \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairApi.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairApi.kt new file mode 100644 index 0000000000..b078327847 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairApi.kt @@ -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) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairProvider.kt new file mode 100644 index 0000000000..a14c6b8561 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairProvider.kt @@ -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 { + 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 { + 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" \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairResponse.kt new file mode 100644 index 0000000000..8962ce7860 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairResponse.kt @@ -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? = null +) + +data class BlockchairAddressData( + @Json(name = "address") + val addressInfo: BlockchairAddressInfo? = null, + + @Json(name = "utxo") + val unspentOutputs: List? = null, + + @Json(name = "transactions") + val transactions: List? = 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? = 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 +) \ No newline at end of file diff --git a/blockchain/src/test/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashAddressTest.kt b/blockchain/src/test/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashAddressTest.kt new file mode 100644 index 0000000000..ff0d1a7f95 --- /dev/null +++ b/blockchain/src/test/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashAddressTest.kt @@ -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() + } +} \ No newline at end of file diff --git a/blockchain/src/test/java/com/tangem/blockchain/common/WalletManagerFactoryTest.kt b/blockchain/src/test/java/com/tangem/blockchain/common/WalletManagerFactoryTest.kt index 9c15645ed2..ff9edbcdd1 100644 --- a/blockchain/src/test/java/com/tangem/blockchain/common/WalletManagerFactoryTest.kt +++ b/blockchain/src/test/java/com/tangem/blockchain/common/WalletManagerFactoryTest.kt @@ -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) + } } \ No newline at end of file