Updated on 2026-08-14

This commit is contained in:
Tangem 2020-04-28 14:30:34 +03:00
parent 894e601444
commit 1c0dfe48cd
23 changed files with 1004 additions and 47 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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