Updated on 2026-08-14

This commit is contained in:
Tangem 2018-11-14 09:48:25 +03:00
parent 59aba143c8
commit d36c77d0ac
13 changed files with 397 additions and 366 deletions

View file

@ -16,7 +16,7 @@ android {
minSdkVersion 21
targetSdkVersion 28
versionCode 98
versionName "0.810.1." + generateVersionName()
versionName "0.811.1." + generateVersionName()
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
androidExtensions {

View file

@ -5,6 +5,7 @@ import android.util.Log;
import com.tangem.App;
import com.tangem.domain.BitcoinNode;
import com.tangem.domain.BitcoinNodeTestNet;
import com.tangem.domain.BitcoinCashNode;
import com.tangem.domain.wallet.Blockchain;
import com.tangem.domain.wallet.TangemCard;
@ -161,14 +162,17 @@ public class ServerApiHelperElectrum {
}
private List<ElectrumRequest> doElectrumRequest(TangemCard card, ElectrumRequest electrumRequest) {
BitcoinNode bitcoinNode = BitcoinNode.values()[new Random().nextInt(BitcoinNode.values().length)];
if (card.getBlockchain() == Blockchain.BitcoinTestNet || card.getBlockchain() == Blockchain.BitcoinCashTestNet) {
if (card.getBlockchain() == Blockchain.BitcoinTestNet) {
BitcoinNodeTestNet bitcoinNodeTestNet = BitcoinNodeTestNet.values()[new Random().nextInt(BitcoinNodeTestNet.values().length)];
this.host = bitcoinNodeTestNet.getHost();
this.port = bitcoinNodeTestNet.getPort();
} else {
} else if (card.getBlockchain() == Blockchain.BitcoinCash) {
BitcoinCashNode bitcoinCashNode = BitcoinCashNode.values()[new Random().nextInt(BitcoinCashNode.values().length)];
this.host = bitcoinCashNode.getHost();
this.port = bitcoinCashNode.getPort();
} else if (card.getBlockchain() == Blockchain.Bitcoin) {
BitcoinNode bitcoinNode = BitcoinNode.values()[new Random().nextInt(BitcoinNode.values().length)];
this.host = bitcoinNode.getHost();
this.port = bitcoinNode.getPort();
}
@ -178,6 +182,7 @@ public class ServerApiHelperElectrum {
try {
Socket socket = App.getNetworkComponent().getSocket();
socket.setSoTimeout(3000);
socket.connect(new InetSocketAddress(InetAddress.getByName(host), port));
Log.i(TAG, host + " " + port);
try {

View file

@ -1,10 +1,22 @@
package com.tangem.domain.wallet;
package com.tangem.domain.wallet.BitcoinCash;
import android.net.Uri;
import android.text.InputFilter;
import com.tangem.domain.cardReader.CardProtocol;
import com.tangem.domain.cardReader.TLV;
import com.tangem.domain.wallet.BalanceValidator;
import com.tangem.domain.wallet.Base58;
import com.tangem.domain.wallet.Blockchain;
import com.tangem.domain.wallet.BtcData;
import com.tangem.domain.wallet.CoinData;
import com.tangem.domain.wallet.CoinEngine;
import com.tangem.domain.wallet.CoinEngineFactory;
import com.tangem.domain.wallet.PINStorage;
import com.tangem.domain.wallet.TangemCard;
import com.tangem.domain.wallet.TangemContext;
import com.tangem.domain.wallet.Transaction;
import com.tangem.domain.wallet.UnspentOutputInfo;
import com.tangem.util.BTCUtils;
import com.tangem.util.CryptoUtil;
import com.tangem.util.DecimalDigitsInputFilter;
@ -113,54 +125,58 @@ public class BtcCashEngine extends CoinEngine {
@Override
public String getFeeCurrency() {
return "mBCH";
return "BCH";
}
@Override
public boolean validateAddress(String address) {
if (address == null || address.isEmpty()) {
return false;
}
// if (address == null || address.isEmpty()) {
// return false;
// }
//
// if (address.length() < 25) {
// return false;
// }
//
// if (address.length() > 35) {
// return false;
// }
//
// if (!address.startsWith("1") && !address.startsWith("2") && !address.startsWith("3") && !address.startsWith("n") && !address.startsWith("m")) {
// return false;
// }
//
// byte[] decAddress = Base58.decodeBase58(address);
//
// if (decAddress == null || decAddress.length == 0) {
// return false;
// }
//
// byte[] rip = new byte[21];
// for (int i = 0; i < 21; ++i) {
// rip[i] = decAddress[i];
// }
//
// byte[] kcv = CryptoUtil.doubleSha256(rip);
//
// for (int i = 0; i < 4; ++i) {
// if (kcv[i] != decAddress[21 + i])
// return false;
// }
//
// if (ctx.getBlockchain() != Blockchain.BitcoinTestNet && ctx.getBlockchain() != Blockchain.Bitcoin) {
// return false;
// }
//
// if (ctx.getBlockchain() == Blockchain.BitcoinTestNet && (address.startsWith("1") || address.startsWith("3"))) {
// return false;
// }
//
// return true;
if (address.length() < 25) {
return false;
}
if (address.length() > 35) {
return false;
}
if (!address.startsWith("1") && !address.startsWith("2") && !address.startsWith("3") && !address.startsWith("n") && !address.startsWith("m")) {
return false;
}
byte[] decAddress = Base58.decodeBase58(address);
if (decAddress == null || decAddress.length == 0) {
return false;
}
byte[] rip = new byte[21];
for (int i = 0; i < 21; ++i) {
rip[i] = decAddress[i];
}
byte[] kcv = CryptoUtil.doubleSha256(rip);
for (int i = 0; i < 4; ++i) {
if (kcv[i] != decAddress[21 + i])
return false;
}
if (ctx.getBlockchain() != Blockchain.BitcoinCashTestNet && ctx.getBlockchain() != Blockchain.BitcoinCash) {
return false;
}
if (ctx.getBlockchain() == Blockchain.BitcoinCashTestNet && (address.startsWith("1") || address.startsWith("3"))) {
return false;
}
return true;
if(CashAddr.isValidCashAddress(address))
return true;
return false;
}
@Override
@ -175,7 +191,7 @@ public class BtcCashEngine extends CoinEngine {
@Override
public Uri getShareWalletUri() {
return Uri.parse("bitcoincash:" + ctx.getCard().getWallet());
return Uri.parse(ctx.getCard().getWallet());
}
@Override
@ -315,15 +331,17 @@ public class BtcCashEngine extends CoinEngine {
@Override
public String calculateAddress(byte[] pkUncompressed) throws NoSuchProviderException, NoSuchAlgorithmException {
byte netSelectionByte;
switch (ctx.getBlockchain()) {
case BitcoinCash:
netSelectionByte = (byte) 0x00; //0 - MainNet 0x6f - TestNet
break;
default:
netSelectionByte = (byte) 0x6f; //0 - MainNet 0x6f - TestNet
break;
}
// CashAddr format
byte hash1[] = Util.calculateSHA256(pkUncompressed);
byte hash2[] = Util.calculateRIPEMD160(hash1);
return CashAddr.toCashAddress(BitcoinCashAddressType.P2PKH, hash2);
}
public String calculateLegacyAddress(byte[] pkUncompressed) throws NoSuchProviderException, NoSuchAlgorithmException {
// Legacy format (BTC)
byte netSelectionByte = (byte) 0x00;
byte hash1[] = Util.calculateSHA256(pkUncompressed);
byte hash2[] = Util.calculateRIPEMD160(hash1);
@ -337,7 +355,32 @@ public class BtcCashEngine extends CoinEngine {
byte hash4[] = Util.calculateSHA256(hash3);
BB = ByteBuffer.allocate(hash2.length + 5);
BB.put(netSelectionByte); //BB.put((byte) 0x6f);
BB.put(netSelectionByte);
BB.put(hash2);
BB.put(hash4[0]);
BB.put(hash4[1]);
BB.put(hash4[2]);
BB.put(hash4[3]);
return org.bitcoinj.core.Base58.encode(BB.array());
}
public String convertToLegacyAddress(String cashAddr) throws NoSuchProviderException, NoSuchAlgorithmException {
BitcoinCashAddressDecodedParts bcadp = CashAddr.decodeCashAddress(cashAddr);
byte netSelectionByte = (byte) 0x00;
byte hash2[] = bcadp.hash;
ByteBuffer BB = ByteBuffer.allocate(hash2.length + 1);
BB.put(netSelectionByte);
BB.put(hash2);
byte hash3[] = Util.calculateSHA256(BB.array());
byte hash4[] = Util.calculateSHA256(hash3);
BB = ByteBuffer.allocate(hash2.length + 5);
BB.put(netSelectionByte);
BB.put(hash2);
BB.put(hash4[0]);
BB.put(hash4[1]);
@ -396,16 +439,19 @@ public class BtcCashEngine extends CoinEngine {
// }
@Override
public byte[] sign(Amount feeValue, Amount amountValue, boolean IncFee, String targetAddress, CardProtocol protocol) throws Exception {
public byte[] sign(Amount feeValue, Amount amountValue, boolean IncFee, String destAddress, CardProtocol protocol) throws Exception {
checkBlockchainDataExists();
String myAddress = ctx.getCard().getWallet();
CoinEngine engine = CoinEngineFactory.create(ctx);
String srcLegacyAddress = ((BtcCashEngine)engine).convertToLegacyAddress(ctx.getCard().getWallet());
String destLegacyAddress = ((BtcCashEngine)engine).convertToLegacyAddress(destAddress);
byte[] pbKey = ctx.getCard().getWalletPublicKeyRar(); //ALWAYS USING COMPRESS KEY
// Build script for our address
List<BtcData.UnspentTransaction> rawTxList = coinData.getUnspentTransactions();
byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(myAddress).bytes;
byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(srcLegacyAddress).bytes;
// Collect unspent
ArrayList<UnspentOutputInfo> unspentOutputs = BTCUtils.getOutputs(rawTxList, outputScriptWeAreAbleToSpend);
@ -432,7 +478,7 @@ public class BtcCashEngine extends CoinEngine {
byte[][] dataForSign = new byte[unspentOutputs.size()][];
for (int i = 0; i < unspentOutputs.size(); ++i) {
byte[] newTX = BTCUtils.buildTXForSign(myAddress, targetAddress, myAddress, unspentOutputs, i, amount, change);
byte[] newTX = BTCUtils.buildTXForSign(srcLegacyAddress, destLegacyAddress, srcLegacyAddress, unspentOutputs, i, amount, change);
byte[] hashData = Util.calculateSHA256(newTX);
byte[] doubleHashData = Util.calculateSHA256(hashData);
@ -471,6 +517,6 @@ public class BtcCashEngine extends CoinEngine {
unspentOutputs.get(i).scriptForBuild = DerEncodingUtil.packSignDerBitcoinCash(r, s, pbKey);
}
return BTCUtils.buildTXForSend(targetAddress, myAddress, unspentOutputs, amount, change);
return BTCUtils.buildTXForSend(destLegacyAddress, srcLegacyAddress, unspentOutputs, amount, change);
}
}

View file

@ -0,0 +1,221 @@
package com.tangem.domain.wallet.BitcoinCash;
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]);
}
byte[] addressData = BitcoinCashBase32.decode(addressParts[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 {
String prefix;
if (bitcoinCashAddress.contains(SEPARATOR)) {
String[] split = bitcoinCashAddress.split(SEPARATOR);
prefix = split[0];
bitcoinCashAddress = split[1];
} else {
prefix =MAIN_NET_PREFIX;
}
if (!isSingleCase(bitcoinCashAddress))
return false;
bitcoinCashAddress = bitcoinCashAddress.toLowerCase();
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

@ -13,8 +13,7 @@ public enum Blockchain {
Ethereum("ETH", "ETH", 1.0, R.drawable.ic_logo_ethereum, "Ethereum"),
EthereumTestNet("ETH/test", "ETH", 1.0, R.drawable.ic_logo_ethereum_testnet, "Ethereum Testnet"),
Token("Token", "ERC20", 1.0, R.drawable.ic_logo_bat_token, "Ethereum"),
BitcoinCash("BCH", "BCH", 100000000.0, R.drawable.ic_logo_bitcoin_cash, "Bitcoin Cash"),
BitcoinCashTestNet("BCH/test", "BTC", 100000000.0, R.drawable.ic_logo_bitcoin_cash, "Bitcoin Cash Testnet");
BitcoinCash("BCH", "BCH", 100000000.0, R.drawable.ic_logo_bitcoin_cash, "Bitcoin Cash");
Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) {
mID = ID;

View file

@ -175,7 +175,7 @@ public class BtcEngine extends CoinEngine {
@Override
public Uri getShareWalletUri() {
if (ctx.getCard().getDenomination() != null) {
return Uri.parse("bitcoin:" + ctx.getCard().getWallet() + "?amount=" + BTCUtils.satoshiToBtc(ctx.getCard().getDenomination()));
return Uri.parse("bitcoin:" + ctx.getCard().getWallet() + "?amount=" + convertToAmount(convertToInternalAmount(ctx.getCard().getDenomination())).toValueString(8));
} else {
return Uri.parse("bitcoin:" + ctx.getCard().getWallet());
}

View file

@ -2,6 +2,8 @@ package com.tangem.domain.wallet;
import android.util.Log;
import com.tangem.domain.wallet.BitcoinCash.BtcCashEngine;
/**
* Created by Ilia on 15.02.2018.
*/
@ -13,7 +15,6 @@ public class CoinEngineFactory {
case BitcoinTestNet:
return new BtcEngine();
case BitcoinCash:
case BitcoinCashTestNet:
return new BtcCashEngine();
case Ethereum:
case EthereumTestNet:
@ -28,7 +29,7 @@ public class CoinEngineFactory {
public static CoinEngine create(TangemContext context) {
CoinEngine result;
try {
if (Blockchain.BitcoinCash == context.getBlockchain() || Blockchain.BitcoinCashTestNet == context.getBlockchain()) {
if (Blockchain.BitcoinCash == context.getBlockchain()) {
result = new BtcCashEngine(context);
} else if (Blockchain.Bitcoin == context.getBlockchain() || Blockchain.BitcoinTestNet == context.getBlockchain()) {
result = new BtcEngine(context);

View file

@ -59,6 +59,10 @@ data class LocalStorage(
putResourceArtworkToCatalog(R.drawable.card_ru021, false)
putResourceArtworkToCatalog(R.drawable.card_ru022, false)
putResourceArtworkToCatalog(R.drawable.card_ru023, true)
putResourceArtworkToCatalog(R.drawable.card_ru024, true)
putResourceArtworkToCatalog(R.drawable.card_ru028, true)
putResourceArtworkToCatalog(R.drawable.card_ru029, true)
putResourceArtworkToCatalog(R.drawable.card_ru030, true)
}
if (batchesFile.exists()) {
try {
@ -70,28 +74,6 @@ data class LocalStorage(
} else {
batches = HashMap()
}
// if (batches.count() == 0) {
// putBatchToCatalog("0004", R.drawable.card_ru006, false)
// putBatchToCatalog("0006", R.drawable.card_ru006, false)
// putBatchToCatalog("0010", R.drawable.card_ru006, false)
//
// putBatchToCatalog("0005", R.drawable.card_ru007, false)
// putBatchToCatalog("0007", R.drawable.card_ru007, false)
// putBatchToCatalog("0011", R.drawable.card_ru007, false)
//
// putBatchToCatalog("0012", R.drawable.card_ru011, false)
// putBatchToCatalog("0013", R.drawable.card_ru012, false)
// putBatchToCatalog("0014", R.drawable.card_ru006, false)
// putBatchToCatalog("0015", R.drawable.card_ru020, false)
// putBatchToCatalog("0016", R.drawable.card_ru021, false)
// putBatchToCatalog("0017", R.drawable.card_ru013, false)
// putBatchToCatalog("0019", R.drawable.card_ru016, false)
// putBatchToCatalog("001A", R.drawable.card_ru014, false)
// putBatchToCatalog("001B", R.drawable.card_ru015, false)
// putBatchToCatalog("001C", R.drawable.card_ru023, false)
// putBatchToCatalog("001D", R.drawable.card_ru022, true)
// }
}
private fun getArtworkFile(artworkId: String): File {
@ -236,6 +218,10 @@ data class LocalStorage(
card.batch == "001B" -> R.drawable.card_ru015
card.batch == "001C" -> R.drawable.card_ru023
card.batch == "001D" -> R.drawable.card_ru022
card.batch == "001E" -> R.drawable.card_ru024
card.batch == "001F" -> R.drawable.card_ru028
card.batch == "0018" -> R.drawable.card_ru029
card.batch == "0020" -> R.drawable.card_ru030
else -> null
}

View file

@ -44,13 +44,13 @@ class ConfirmPaymentActivity : AppCompatActivity(), NfcAdapter.ReaderCallback {
private lateinit var amount: CoinEngine.Amount
private var feeRequestSuccess = false
private var balanceRequestSuccess = false
// private var balanceRequestSuccess = false
private var minFee: CoinEngine.Amount? = null
private var maxFee: CoinEngine.Amount? = null
private var normalFee: CoinEngine.Amount? = null
private var isIncludeFee: Boolean = true
private var requestPIN2Count = 0
private var nodeCheck = false
private var nodeCheck = true
private var dtVerified: Date? = null
private var calcSize: Int = 0
@ -92,7 +92,7 @@ class ConfirmPaymentActivity : AppCompatActivity(), NfcAdapter.ReaderCallback {
btnSend.visibility = View.INVISIBLE
feeRequestSuccess = false
balanceRequestSuccess = false
// balanceRequestSuccess = false
if (ctx.blockchain == Blockchain.Ethereum || ctx.blockchain == Blockchain.EthereumTestNet || ctx.blockchain == Blockchain.Token) {
rgFee.isEnabled = false
@ -102,7 +102,7 @@ class ConfirmPaymentActivity : AppCompatActivity(), NfcAdapter.ReaderCallback {
} else {
rgFee.isEnabled = true
requestElectrum(ctx.card, ElectrumRequest.checkBalance(ctx.card!!.wallet))
// requestElectrum(ctx.card, ElectrumRequest.checkBalance(ctx.card!!.wallet))
calcSize = 256
try {
@ -194,38 +194,38 @@ class ConfirmPaymentActivity : AppCompatActivity(), NfcAdapter.ReaderCallback {
}
// request electrum listener
val electrumBodyListener: ServerApiHelperElectrum.ElectrumRequestDataListener = object : ServerApiHelperElectrum.ElectrumRequestDataListener {
override fun onSuccess(electrumRequest: ElectrumRequest?) {
if (electrumRequest!!.isMethod(ElectrumRequest.METHOD_GetBalance)) {
try {
if (etFee.text.toString().isEmpty()) etFee.setText(getString(R.string.empty))
val engine = CoinEngineFactory.create(ctx)
val balance = engine.convertToAmount(CoinEngine.InternalAmount(electrumRequest.result.getLong("confirmed") + electrumRequest.result.getLong("unconfirmed"), "Satoshi"))
val amount = CoinEngine.Amount(etAmount.text.toString(), ctx.blockchain.currency)
if (balance < amount) {
etFee.error = getString(R.string.not_enough_funds)
} else {
etFee.error = null
balanceRequestSuccess = true
if (feeRequestSuccess && balanceRequestSuccess) {
btnSend.visibility = View.VISIBLE
}
dtVerified = Date()
nodeCheck = true
}
} catch (e: JSONException) {
e.printStackTrace()
requestElectrum(ctx.card!!, ElectrumRequest.checkBalance(ctx.card!!.wallet))
}
}
}
override fun onFail(message: String?) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.cannot_check_balance_no_connection_with_blockchain_nodes))
}
}
serverApiHelperElectrum.setElectrumRequestData(electrumBodyListener)
// val electrumBodyListener: ServerApiHelperElectrum.ElectrumRequestDataListener = object : ServerApiHelperElectrum.ElectrumRequestDataListener {
// override fun onSuccess(electrumRequest: ElectrumRequest?) {
// if (electrumRequest!!.isMethod(ElectrumRequest.METHOD_GetBalance)) {
// try {
// if (etFee.text.toString().isEmpty()) etFee.setText(getString(R.string.empty))
// val engine = CoinEngineFactory.create(ctx)
// val balance = engine.convertToAmount(CoinEngine.InternalAmount(electrumRequest.result.getLong("confirmed") + electrumRequest.result.getLong("unconfirmed"), "Satoshi"))
// val amount = CoinEngine.Amount(etAmount.text.toString(), ctx.blockchain.currency)
// if (balance < amount) {
// etFee.error = getString(R.string.not_enough_funds)
// } else {
// etFee.error = null
// balanceRequestSuccess = true
// if (feeRequestSuccess && balanceRequestSuccess) {
// btnSend.visibility = View.VISIBLE
// }
// dtVerified = Date()
// nodeCheck = true
// }
// } catch (e: JSONException) {
// e.printStackTrace()
//// requestElectrum(ctx.card!!, ElectrumRequest.checkBalance(ctx.card!!.wallet))
// }
// }
// }
//
// override fun onFail(message: String?) {
// finishWithError(Activity.RESULT_CANCELED, getString(R.string.cannot_check_balance_no_connection_with_blockchain_nodes))
// }
//
// }
// serverApiHelperElectrum.setElectrumRequestData(electrumBodyListener)
// request infura eth gasPrice listener
val infuraBodyListener: ServerApiHelper.InfuraBodyListener = object : ServerApiHelper.InfuraBodyListener {
@ -252,7 +252,7 @@ class ConfirmPaymentActivity : AppCompatActivity(), NfcAdapter.ReaderCallback {
etFee.error = null
btnSend.visibility = View.VISIBLE
feeRequestSuccess = true
balanceRequestSuccess = true
// balanceRequestSuccess = true
dtVerified = Date()
}
}
@ -308,7 +308,8 @@ class ConfirmPaymentActivity : AppCompatActivity(), NfcAdapter.ReaderCallback {
etFee.error = null
feeRequestSuccess = true
if (feeRequestSuccess && balanceRequestSuccess)
if (feeRequestSuccess)
// if (feeRequestSuccess && balanceRequestSuccess)
btnSend.visibility = View.VISIBLE
dtVerified = Date()
}

View file

@ -104,7 +104,7 @@ class PrepareCryptonitOtherAPIWithdrawalActivity : AppCompatActivity(), NfcAdapt
Blockchain.Ethereum, Blockchain.EthereumTestNet -> {
tvBalance.text = response.eth_available
}
Blockchain.Bitcoin, Blockchain.BitcoinTestNet, Blockchain.BitcoinCash, Blockchain.BitcoinCashTestNet -> {
Blockchain.Bitcoin, Blockchain.BitcoinTestNet, Blockchain.BitcoinCash -> {
tvBalance.text = response.btc_available
}
else -> {

View file

@ -49,7 +49,7 @@ class SendTransactionActivity : AppCompatActivity(), NfcAdapter.ReaderCallback {
requestInfura(ServerApiHelper.INFURA_ETH_SEND_RAW_TRANSACTION, "")
else if (ctx.blockchain == Blockchain.Bitcoin || ctx.blockchain == Blockchain.BitcoinTestNet)
requestElectrum(ctx.card!!, ElectrumRequest.broadcast(ctx.card!!.wallet, tx))
else if (ctx.blockchain == Blockchain.BitcoinCash || ctx.blockchain == Blockchain.BitcoinCashTestNet)
else if (ctx.blockchain == Blockchain.BitcoinCash)
requestElectrum(ctx.card!!, ElectrumRequest.broadcast(ctx.card!!.wallet, tx))
// request electrum listener

View file

@ -28,6 +28,7 @@ import com.tangem.data.nfc.VerifyCardTask
import com.tangem.domain.cardReader.CardProtocol
import com.tangem.domain.cardReader.NfcManager
import com.tangem.domain.wallet.*
import com.tangem.domain.wallet.BitcoinCash.BtcCashEngine
import com.tangem.presentation.activity.*
import com.tangem.presentation.dialog.NoExtendedLengthSupportDialog
import com.tangem.presentation.dialog.PINSwapWarningDialog
@ -811,7 +812,7 @@ class LoadedWallet : Fragment(), NfcAdapter.ReaderCallback, CardProtocol.Notific
requestVerifyAndGetInfo()
// Bitcoin
if (ctx.blockchain == Blockchain.Bitcoin || ctx.blockchain == Blockchain.BitcoinTestNet) {
if (ctx.blockchain == Blockchain.Bitcoin) {
ctx.coinData.setIsBalanceEqual(true)
requestElectrum(ElectrumRequest.checkBalance(ctx.card!!.wallet))
@ -820,11 +821,12 @@ class LoadedWallet : Fragment(), NfcAdapter.ReaderCallback, CardProtocol.Notific
}
// BitcoinCash
else if (ctx.blockchain == Blockchain.BitcoinCash || ctx.blockchain == Blockchain.BitcoinCashTestNet) {
else if (ctx.blockchain == Blockchain.BitcoinCash) {
ctx.coinData.setIsBalanceEqual(true)
val engine = CoinEngineFactory.create(ctx)
requestElectrum(ElectrumRequest.checkBalance(ctx.card!!.wallet))
requestElectrum(ElectrumRequest.listUnspent(ctx.card!!.wallet))
requestElectrum(ElectrumRequest.checkBalance((engine as BtcCashEngine).convertToLegacyAddress(ctx.card!!.wallet)))
requestElectrum(ElectrumRequest.listUnspent((engine as BtcCashEngine).convertToLegacyAddress(ctx.card!!.wallet)))
requestRateInfo("bitcoin-cash")
}

View file

@ -37,57 +37,6 @@ public final class BTCUtils {
public static final int MAX_TX_LEN_FOR_NO_FEE = 10000;
public static final float EXPECTED_BLOCKS_PER_DAY = 144.0f;//(expected confirmations per day)
public static long calcMinimumFee(int txLen, Collection<UnspentOutputInfo> unspentOutputInfos, long minOutput) {
if (isZeroFeeAllowed(txLen, unspentOutputInfos, minOutput)) {
return 0;
}
return MIN_FEE_PER_KB * (1 + txLen / 1000);
}
public static boolean isZeroFeeAllowed(int txLen, Collection<UnspentOutputInfo> unspentOutputInfos, long minOutput) {
if (txLen < MAX_TX_LEN_FOR_NO_FEE && minOutput > MIN_MIN_OUTPUT_VALUE_FOR_NO_FEE) {
long priority = 0;
for (UnspentOutputInfo output : unspentOutputInfos) {
if (output.confirmations > 0) {
priority += output.confirmations * output.value;
}
}
priority /= txLen;
if (priority > MIN_PRIORITY_FOR_NO_FEE) {
return true;
}
}
return false;
}
public static int getMaximumTxSize(Collection<UnspentOutputInfo> unspentOutputInfos, int outputsCount, boolean compressedPublicKey) throws BitcoinException {
if (unspentOutputInfos == null || unspentOutputInfos.isEmpty()) {
throw new BitcoinException(BitcoinException.ERR_NO_INPUT, "No information about tx inputs provided");
}
int maxInputScriptLen = 73 + (compressedPublicKey ? 33 : 65);
return 9 + unspentOutputInfos.size() * (41 + maxInputScriptLen) + outputsCount * 33;
}
public static String publicKeyToAddress(byte[] publicKey) {
return publicKeyToAddress(false, publicKey);
}
public static String publicKeyToAddress(boolean testNet, byte[] publicKey) {
try {
byte[] hashedPublicKey = CryptoUtil.sha256ripemd160(publicKey);
byte[] addressBytes = new byte[1 + hashedPublicKey.length + 4];
addressBytes[0] = (byte) (testNet ? 111 : 0);
System.arraycopy(hashedPublicKey, 0, addressBytes, 1, hashedPublicKey.length);
MessageDigest digestSha = MessageDigest.getInstance("SHA-256");
digestSha.update(addressBytes, 0, addressBytes.length - 4);
byte[] check = digestSha.digest(digestSha.digest());
System.arraycopy(check, 0, addressBytes, hashedPublicKey.length + 1, 4);
return Base58.encodeBase58(addressBytes);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static String toHex(byte[] bytes) {
if (bytes == null) {
return "";
@ -103,21 +52,6 @@ public final class BTCUtils {
return new String(hexChars);
}
public static String satoshiToBtc(byte[] satoshi)
{
satoshi = reverse(satoshi);
BigInteger num = new BigInteger(1, satoshi);
BigDecimal dec = new BigDecimal(num);
dec = dec.setScale(8, RoundingMode.DOWN);
dec = dec.divide(new BigDecimal("100000000"));
String pattern = "#0.00000000";
DecimalFormat myFormatter = new DecimalFormat(pattern);
String output = myFormatter.format(dec).replace(",",".");
return output;
}
public static byte[] buildTXForSign(String myAddress, String outputAddress, String changeAddress, ArrayList<UnspentOutputInfo> unspentOutputs, int currentInputPos, long amount, long change) throws BitcoinException, IOException {
byte[] myScript = Transaction.Script.buildOutput(myAddress).bytes;
unspentOutputs.get(currentInputPos).scriptForBuild = myScript;
@ -207,40 +141,6 @@ public final class BTCUtils {
return rawData;
}
int calculateSize(int inputCount, int outputCount)
{
int size = 0;
size += 4; // header
size += 1; //inputCount
//hex str hash prev btc
for(int i = 0; i < inputCount; ++i)
{
size += 32; //prevtx
size += 4; //outputIndex;
size += 1; //scriptLength
// size+=script;
size += 4; //ffffffff
}
size+=1; //outputCount
size+=8; //amount
size+=1;
// size+=script;
if(outputCount > 1)
{
size+=8;
size+=1;
//scriptLen;
}
size+=4;
return size;
}
public static byte[] buildBodyTX(String outputAddress, String changeAddress, int outputIndex, String prevID, long amount, long change, byte[] script) throws BitcoinException, IOException {
//0200000000
@ -297,34 +197,6 @@ public final class BTCUtils {
return rawData;
}
public static ArrayList<byte[]> getPrevTX(String hex) throws BitcoinException {
byte[] rawTxByte = fromHex(hex);
Transaction baseTx = new Transaction(rawTxByte);
ArrayList<byte[]> prevHashes = new ArrayList<byte[]>();
for(int i =0; i < baseTx.inputs.length; ++i)
{
Transaction.Input input = baseTx.inputs[i];
prevHashes.add(input.outPoint.hash);
}
return prevHashes;
}
public static boolean isInput(String myAddress, String hex) throws BitcoinException {
byte[] rawTxByte = fromHex(hex);
Transaction baseTx = new Transaction(rawTxByte);
byte[] myScript = Transaction.Script.buildOutput(myAddress).bytes;
for(int i =0; i < baseTx.inputs.length; ++i)
{
Transaction.Input input = baseTx.inputs[i];
byte[] script = input.script.bytes;
// find outputs
if (Arrays.equals(myScript, script)){
return true;
}
}
return false;
}
public static ArrayList<UnspentOutputInfo> getOutputs(List<BtcData.UnspentTransaction> rawTxList, byte[] outputScriptWeAreAbleToSpend) throws BitcoinException {
ArrayList<UnspentOutputInfo> unspentOutputs = new ArrayList<>();
@ -405,107 +277,5 @@ public final class BTCUtils {
return bytes;
}
public static int findSpendableOutput(Transaction tx, String forAddress, long minAmount) throws BitcoinException {
byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(forAddress).bytes;
int indexOfOutputToSpend = -1;
for (int indexOfOutput = 0; indexOfOutput < tx.outputs.length; indexOfOutput++) {
Transaction.Output output = tx.outputs[indexOfOutput];
if (Arrays.equals(outputScriptWeAreAbleToSpend, output.script.bytes)) {
indexOfOutputToSpend = indexOfOutput;
break;//only one input is supported for now
}
}
if (indexOfOutputToSpend == -1) {
throw new BitcoinException(BitcoinException.ERR_NO_SPENDABLE_OUTPUTS_FOR_THE_ADDRESS, "No spendable standard outputs for " + forAddress + " have found", forAddress);
}
final long spendableOutputValue = tx.outputs[indexOfOutputToSpend].value;
if (spendableOutputValue < minAmount) {
throw new BitcoinException(BitcoinException.ERR_INSUFFICIENT_FUNDS, "Unspent amount is too small: " + spendableOutputValue, spendableOutputValue);
}
return indexOfOutputToSpend;
}
public static void verify(Transaction.Script[] scripts, Transaction spendTx) throws Transaction.Script.ScriptInvalidException {
for (int i = 0; i < scripts.length; i++) {
Stack<byte[]> stack = new Stack<>();
spendTx.inputs[i].script.run(stack);//load signature+public key
scripts[i].run(i, spendTx, stack); //verify that this transaction able to spend that output
if (Transaction.Script.verifyFails(stack)) {
throw new Transaction.Script.ScriptInvalidException("Signature is invalid");
}
}
}
public static class FeeChangeAndSelectedOutputs {
public final long amountForRecipient, change, fee;
public final ArrayList<UnspentOutputInfo> outputsToSpend;
public FeeChangeAndSelectedOutputs(long fee, long change, long amountForRecipient, ArrayList<UnspentOutputInfo> outputsToSpend) {
this.fee = fee;
this.change = change;
this.amountForRecipient = amountForRecipient;
this.outputsToSpend = outputsToSpend;
}
}
public static FeeChangeAndSelectedOutputs calcFeeChangeAndSelectOutputsToSpend(List<UnspentOutputInfo> unspentOutputs, long amountToSend, long extraFee, final boolean isPublicKeyCompressed) throws BitcoinException {
long fee = 0;//calculated below
long change = 0;
long valueOfUnspentOutputs;
ArrayList<UnspentOutputInfo> outputsToSpend = new ArrayList<>();
if (amountToSend <= 0) {
//transfer all funds from these addresses to outputAddress
change = 0;
valueOfUnspentOutputs = 0;
for (UnspentOutputInfo outputInfo : unspentOutputs) {
outputsToSpend.add(outputInfo);
valueOfUnspentOutputs += outputInfo.value;
}
final int txLen = BTCUtils.getMaximumTxSize(unspentOutputs, 1, isPublicKeyCompressed);
fee = BTCUtils.calcMinimumFee(txLen, unspentOutputs, valueOfUnspentOutputs - MIN_FEE_PER_KB * (1 + txLen / 1000));
amountToSend = valueOfUnspentOutputs - fee - extraFee;
} else {
valueOfUnspentOutputs = 0;
for (UnspentOutputInfo outputInfo : unspentOutputs) {
outputsToSpend.add(outputInfo);
valueOfUnspentOutputs += outputInfo.value;
long updatedFee = MIN_FEE_PER_KB;
for (int i = 0; i < 3; i++) {
fee = updatedFee;
change = valueOfUnspentOutputs - fee - extraFee - amountToSend;
final int txLen = BTCUtils.getMaximumTxSize(unspentOutputs, change > 0 ? 2 : 1, isPublicKeyCompressed);
updatedFee = BTCUtils.calcMinimumFee(txLen, unspentOutputs, change > 0 ? Math.min(amountToSend, change) : amountToSend);
if (updatedFee == fee) {
break;
}
}
fee = updatedFee;
if (valueOfUnspentOutputs >= amountToSend + fee + extraFee) {
break;
}
}
}
if (amountToSend > valueOfUnspentOutputs - fee) {
throw new BitcoinException(BitcoinException.ERR_INSUFFICIENT_FUNDS, "Not enough funds", valueOfUnspentOutputs - fee);
}
if (outputsToSpend.isEmpty()) {
throw new BitcoinException(BitcoinException.ERR_NO_INPUT, "No outputs to spend");
}
if (fee + extraFee > MAX_ALLOWED_FEE) {
throw new BitcoinException(BitcoinException.ERR_FEE_IS_TOO_BIG, "Fee is too big", fee);
}
if (fee < 0 || extraFee < 0) {
throw new BitcoinException(BitcoinException.ERR_FEE_IS_LESS_THEN_ZERO, "Incorrect fee", fee);
}
if (change < 0) {
throw new BitcoinException(BitcoinException.ERR_CHANGE_IS_LESS_THEN_ZERO, "Incorrect change", change);
}
if (amountToSend < 0) {
throw new BitcoinException(BitcoinException.ERR_AMOUNT_TO_SEND_IS_LESS_THEN_ZERO, "Incorrect amount to send", amountToSend);
}
return new FeeChangeAndSelectedOutputs(fee + extraFee, change, amountToSend, outputsToSpend);
}
}