From d36c77d0ac2e30f7d34c11613fc1bcb6f27f5c77 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 14 Nov 2018 09:48:25 +0300 Subject: [PATCH] Updated on 2026-08-14 --- app/build.gradle | 2 +- .../data/network/ServerApiHelperElectrum.java | 13 +- .../{ => BitcoinCash}/BtcCashEngine.java | 166 ++++++++----- .../domain/wallet/BitcoinCash/CashAddr.java | 221 +++++++++++++++++ .../com/tangem/domain/wallet/Blockchain.java | 3 +- .../com/tangem/domain/wallet/BtcEngine.java | 2 +- .../domain/wallet/CoinEngineFactory.java | 5 +- .../com/tangem/domain/wallet/LocalStorage.kt | 30 +-- .../activity/ConfirmPaymentActivity.kt | 77 +++--- ...pareCryptonitOtherAPIWithdrawalActivity.kt | 2 +- .../activity/SendTransactionActivity.kt | 2 +- .../presentation/fragment/LoadedWallet.kt | 10 +- .../main/java/com/tangem/util/BTCUtils.java | 230 ------------------ 13 files changed, 397 insertions(+), 366 deletions(-) rename app/src/main/java/com/tangem/domain/wallet/{ => BitcoinCash}/BtcCashEngine.java (79%) create mode 100644 app/src/main/java/com/tangem/domain/wallet/BitcoinCash/CashAddr.java diff --git a/app/build.gradle b/app/build.gradle index f18aa3510b..4dd5c06c92 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 { diff --git a/app/src/main/java/com/tangem/data/network/ServerApiHelperElectrum.java b/app/src/main/java/com/tangem/data/network/ServerApiHelperElectrum.java index abd6fe934d..1282b0cd5f 100644 --- a/app/src/main/java/com/tangem/data/network/ServerApiHelperElectrum.java +++ b/app/src/main/java/com/tangem/data/network/ServerApiHelperElectrum.java @@ -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 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 { diff --git a/app/src/main/java/com/tangem/domain/wallet/BtcCashEngine.java b/app/src/main/java/com/tangem/domain/wallet/BitcoinCash/BtcCashEngine.java similarity index 79% rename from app/src/main/java/com/tangem/domain/wallet/BtcCashEngine.java rename to app/src/main/java/com/tangem/domain/wallet/BitcoinCash/BtcCashEngine.java index ed37399ac2..7240084e2b 100644 --- a/app/src/main/java/com/tangem/domain/wallet/BtcCashEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/BitcoinCash/BtcCashEngine.java @@ -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 rawTxList = coinData.getUnspentTransactions(); - byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(myAddress).bytes; + byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(srcLegacyAddress).bytes; // Collect unspent ArrayList 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); } } diff --git a/app/src/main/java/com/tangem/domain/wallet/BitcoinCash/CashAddr.java b/app/src/main/java/com/tangem/domain/wallet/BitcoinCash/CashAddr.java new file mode 100644 index 0000000000..05fe7c2353 --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/BitcoinCash/CashAddr.java @@ -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; + } + + + + +} diff --git a/app/src/main/java/com/tangem/domain/wallet/Blockchain.java b/app/src/main/java/com/tangem/domain/wallet/Blockchain.java index 5e52880c0c..f7bf3c304b 100644 --- a/app/src/main/java/com/tangem/domain/wallet/Blockchain.java +++ b/app/src/main/java/com/tangem/domain/wallet/Blockchain.java @@ -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; diff --git a/app/src/main/java/com/tangem/domain/wallet/BtcEngine.java b/app/src/main/java/com/tangem/domain/wallet/BtcEngine.java index 80f413312b..97999a8774 100644 --- a/app/src/main/java/com/tangem/domain/wallet/BtcEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/BtcEngine.java @@ -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()); } diff --git a/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.java b/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.java index 35bbe4a891..ec23ffefbd 100644 --- a/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.java +++ b/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.java @@ -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); diff --git a/app/src/main/java/com/tangem/domain/wallet/LocalStorage.kt b/app/src/main/java/com/tangem/domain/wallet/LocalStorage.kt index 3c0312de22..82a26096d5 100644 --- a/app/src/main/java/com/tangem/domain/wallet/LocalStorage.kt +++ b/app/src/main/java/com/tangem/domain/wallet/LocalStorage.kt @@ -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 } diff --git a/app/src/main/java/com/tangem/presentation/activity/ConfirmPaymentActivity.kt b/app/src/main/java/com/tangem/presentation/activity/ConfirmPaymentActivity.kt index d4a8508f0c..7ad17be028 100644 --- a/app/src/main/java/com/tangem/presentation/activity/ConfirmPaymentActivity.kt +++ b/app/src/main/java/com/tangem/presentation/activity/ConfirmPaymentActivity.kt @@ -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() } diff --git a/app/src/main/java/com/tangem/presentation/activity/PrepareCryptonitOtherAPIWithdrawalActivity.kt b/app/src/main/java/com/tangem/presentation/activity/PrepareCryptonitOtherAPIWithdrawalActivity.kt index b54d8b7c1c..30a210bd8f 100644 --- a/app/src/main/java/com/tangem/presentation/activity/PrepareCryptonitOtherAPIWithdrawalActivity.kt +++ b/app/src/main/java/com/tangem/presentation/activity/PrepareCryptonitOtherAPIWithdrawalActivity.kt @@ -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 -> { diff --git a/app/src/main/java/com/tangem/presentation/activity/SendTransactionActivity.kt b/app/src/main/java/com/tangem/presentation/activity/SendTransactionActivity.kt index 08754389e5..7206af57be 100644 --- a/app/src/main/java/com/tangem/presentation/activity/SendTransactionActivity.kt +++ b/app/src/main/java/com/tangem/presentation/activity/SendTransactionActivity.kt @@ -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 diff --git a/app/src/main/java/com/tangem/presentation/fragment/LoadedWallet.kt b/app/src/main/java/com/tangem/presentation/fragment/LoadedWallet.kt index cbbc97f849..5a66ea71a2 100644 --- a/app/src/main/java/com/tangem/presentation/fragment/LoadedWallet.kt +++ b/app/src/main/java/com/tangem/presentation/fragment/LoadedWallet.kt @@ -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") } diff --git a/app/src/main/java/com/tangem/util/BTCUtils.java b/app/src/main/java/com/tangem/util/BTCUtils.java index 0adf324f4b..ce8a71c5df 100644 --- a/app/src/main/java/com/tangem/util/BTCUtils.java +++ b/app/src/main/java/com/tangem/util/BTCUtils.java @@ -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 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 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 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 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 getPrevTX(String hex) throws BitcoinException { - byte[] rawTxByte = fromHex(hex); - Transaction baseTx = new Transaction(rawTxByte); - ArrayList prevHashes = new ArrayList(); - 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 getOutputs(List rawTxList, byte[] outputScriptWeAreAbleToSpend) throws BitcoinException { ArrayList 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 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 outputsToSpend; - - public FeeChangeAndSelectedOutputs(long fee, long change, long amountForRecipient, ArrayList outputsToSpend) { - this.fee = fee; - this.change = change; - this.amountForRecipient = amountForRecipient; - this.outputsToSpend = outputsToSpend; - } - } - - public static FeeChangeAndSelectedOutputs calcFeeChangeAndSelectOutputsToSpend(List unspentOutputs, long amountToSend, long extraFee, final boolean isPublicKeyCompressed) throws BitcoinException { - long fee = 0;//calculated below - long change = 0; - long valueOfUnspentOutputs; - ArrayList 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); - - } }