From a31c66d60f27547b8ec377f4ea224ddb91709761 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Mar 2020 12:21:18 +0300 Subject: [PATCH 1/4] Updated on 2026-08-14 --- .../tangem/data/network/BlockchairApi.java | 27 ++ .../java/com/tangem/data/network/Server.java | 11 + .../data/network/ServerApiBlockchair.java | 73 ++++ .../com/tangem/data/network/ServerURL.java | 1 + .../data/network/model/BlockchairResponse.kt | 69 ++++ .../network/model/BlockchairSendBody.java | 9 + .../java/com/tangem/di/NetworkComponent.kt | 4 +- .../main/java/com/tangem/di/NetworkModule.kt | 13 + .../main/java/com/tangem/wallet/CoinData.java | 4 + .../com/tangem/wallet/bch/BtcCashEngine.java | 383 ++++++------------ 10 files changed, 328 insertions(+), 266 deletions(-) create mode 100644 app/src/main/java/com/tangem/data/network/BlockchairApi.java create mode 100644 app/src/main/java/com/tangem/data/network/ServerApiBlockchair.java create mode 100644 app/src/main/java/com/tangem/data/network/model/BlockchairResponse.kt create mode 100644 app/src/main/java/com/tangem/data/network/model/BlockchairSendBody.java diff --git a/app/src/main/java/com/tangem/data/network/BlockchairApi.java b/app/src/main/java/com/tangem/data/network/BlockchairApi.java new file mode 100644 index 0000000000..ae6f01624b --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/BlockchairApi.java @@ -0,0 +1,27 @@ +package com.tangem.data.network; + +import com.tangem.data.network.model.BlockchairAddressResponse; +import com.tangem.data.network.model.BlockchairSendBody; +import com.tangem.data.network.model.BlockchairStatsResponse; +import com.tangem.data.network.model.BlockchairTransactionResponse; + +import io.reactivex.Completable; +import io.reactivex.Single; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Path; + +public interface BlockchairApi { + @GET(Server.ApiBlockchair.Method.ADDRESS) + Single getAddress(@Path("blockchain") String blockchain, @Path("address") String address); + + @GET(Server.ApiBlockchair.Method.TRANSACTION) + Single getTransaction(@Path("blockchain") String blockchain, @Path("transaction") String transaction); + + @GET(Server.ApiBlockchair.Method.STATS) + Single getStats(@Path("blockchain") String blockchain); + + @POST(Server.ApiBlockchair.Method.PUSH) + Completable sendTransaction(@Path("blockchain") String blockchain, @Body BlockchairSendBody body); +} diff --git a/app/src/main/java/com/tangem/data/network/Server.java b/app/src/main/java/com/tangem/data/network/Server.java index 29f69aba32..42b1a07512 100644 --- a/app/src/main/java/com/tangem/data/network/Server.java +++ b/app/src/main/java/com/tangem/data/network/Server.java @@ -139,4 +139,15 @@ public class Server { static final String SEND = URL_DUCATUS + "tx/send"; } } + + public static class ApiBlockchair { + public static final String URL_BLOCKCHAIR = ServerURL.API_BLOCKCHAIR + "{blockchain}/"; + + public static class Method { + static final String ADDRESS = URL_BLOCKCHAIR + "dashboards/address/{address}"; + static final String TRANSACTION = URL_BLOCKCHAIR + "dashboards/transaction/{transaction}"; + static final String STATS = URL_BLOCKCHAIR + "stats"; + static final String PUSH = URL_BLOCKCHAIR + "push/transaction"; + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/ServerApiBlockchair.java b/app/src/main/java/com/tangem/data/network/ServerApiBlockchair.java new file mode 100644 index 0000000000..f77bb83449 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/ServerApiBlockchair.java @@ -0,0 +1,73 @@ +package com.tangem.data.network; + +import com.tangem.App; +import com.tangem.data.Blockchain; +import com.tangem.data.network.model.BlockchairAddressResponse; +import com.tangem.data.network.model.BlockchairSendBody; +import com.tangem.data.network.model.BlockchairStatsResponse; +import com.tangem.data.network.model.BlockchairTransactionResponse; +import com.tangem.tangem_card.util.Log; + +import io.reactivex.Completable; +import io.reactivex.CompletableObserver; +import io.reactivex.Single; +import io.reactivex.SingleObserver; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; + +public class ServerApiBlockchair { + private static String TAG = ServerApiBlockchair.class.getSimpleName(); + private String blockchain; + + public ServerApiBlockchair(Blockchain blockchain) { + if (blockchain == Blockchain.BitcoinCash) this.blockchain = "bitcoin-cash"; + } + + public void getAddress(String wallet, SingleObserver addressObserver) { + Log.i(TAG, "new getAddress request"); + BlockchairApi api = App.Companion.getNetworkComponent().getRetrofitBlockchair().create(BlockchairApi.class); + + Single addressSingle = api.getAddress(blockchain, wallet) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + + addressSingle.subscribe(addressObserver); + } + + public void getTransaction(String transaction, SingleObserver transactionObserver) { + Log.i(TAG, "new getAddress request"); + BlockchairApi api = App.Companion.getNetworkComponent().getRetrofitBlockchair().create(BlockchairApi.class); + + Single transactionSingle = api.getTransaction(blockchain, transaction) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + + transactionSingle.subscribe(transactionObserver); + } + + public void getStats(SingleObserver statsObserver) { + Log.i(TAG, "new getStats request"); + BlockchairApi api = App.Companion.getNetworkComponent().getRetrofitBlockchair().create(BlockchairApi.class); + + Single statsSingle = api.getStats(blockchain) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + + statsSingle.subscribe(statsObserver); + } + + public void sendTransaction(String tx, CompletableObserver sendObserver) { + Log.i(TAG, "new getAddress request"); + BlockchairApi api = App.Companion.getNetworkComponent().getRetrofitDucatus().create(BlockchairApi.class); + + Completable sendCompletable = api.sendTransaction(blockchain, new BlockchairSendBody(tx)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + + sendCompletable.subscribe(sendObserver); + } + + public String getUrl() { + return ServerURL.API_BLOCKCHAIR; + } +} diff --git a/app/src/main/java/com/tangem/data/network/ServerURL.java b/app/src/main/java/com/tangem/data/network/ServerURL.java index cab4e81153..8147ae4d02 100644 --- a/app/src/main/java/com/tangem/data/network/ServerURL.java +++ b/app/src/main/java/com/tangem/data/network/ServerURL.java @@ -17,4 +17,5 @@ class ServerURL { static final String API_STELLAR_TESTNET = "https://horizon-testnet.stellar.org/"; static final String API_BLOCKCHAIN_INFO = "https://blockchain.info/"; static final String API_DUCATUS = "https://ducapi.rocknblock.io/"; + static final String API_BLOCKCHAIR = "https://api.blockchair.com/"; } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/model/BlockchairResponse.kt b/app/src/main/java/com/tangem/data/network/model/BlockchairResponse.kt new file mode 100644 index 0000000000..8e8f65aae1 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/BlockchairResponse.kt @@ -0,0 +1,69 @@ +package com.tangem.data.network.model + +import com.google.gson.annotations.SerializedName + +data class BlockchairAddressResponse( + @SerializedName("data") + val data: Map? = null +) + +data class BlockchairAddressData( + @SerializedName("address") + val address: BlockchairAddressInfo? = null, + + @SerializedName("utxo") + val unspentOutputs: List? = null, + + @SerializedName("transactions") + val transactions: List +) + +data class BlockchairAddressInfo( + @SerializedName("balance") + val balance: Long? = null, + + @SerializedName("output_count") + val outputCount: Int? = null, + + @SerializedName("unspent_output_count") + val unspentOutputCount: Int? = null +) + +data class BlockchairUnspentOutput( + @SerializedName("block_id") + val block: Int? = null, + + @SerializedName("transaction_hash") + val transactionHash: String? = null, + + @SerializedName("index") + val index: Int? = null, + + @SerializedName("value") + val amount: Long? = null +) + +data class BlockchairTransactionResponse( + @SerializedName("data") + val data: Map? = null +) + +data class BlockchairTransactionData( + @SerializedName("transaction") + val transaction: BlockchairTransactionInfo? = null +) + +data class BlockchairTransactionInfo( + @SerializedName("block_id") + val block: Int? = null +) + +data class BlockchairStatsResponse( + @SerializedName("data") + val data: BlockchairStatsData? = null +) + +data class BlockchairStatsData( + @SerializedName("suggested_transaction_fee_per_byte_sat") + val feePerByte: Int? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/model/BlockchairSendBody.java b/app/src/main/java/com/tangem/data/network/model/BlockchairSendBody.java new file mode 100644 index 0000000000..3e6458c090 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/BlockchairSendBody.java @@ -0,0 +1,9 @@ +package com.tangem.data.network.model; + +public class BlockchairSendBody { + private String data; + + public BlockchairSendBody(String data) { + this.data = data; + } +} diff --git a/app/src/main/java/com/tangem/di/NetworkComponent.kt b/app/src/main/java/com/tangem/di/NetworkComponent.kt index fa9291e3cc..1a526e385b 100644 --- a/app/src/main/java/com/tangem/di/NetworkComponent.kt +++ b/app/src/main/java/com/tangem/di/NetworkComponent.kt @@ -41,7 +41,9 @@ interface NetworkComponent { @get:Named(Server.ApiDucatus.URL_DUCATUS) val retrofitDucatus: Retrofit + @get:Named(Server.ApiBlockchair.URL_BLOCKCHAIR) + val retrofitBlockchair: Retrofit + @get:Named("socket") val socket: Socket - } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/di/NetworkModule.kt b/app/src/main/java/com/tangem/di/NetworkModule.kt index 64279e871b..ea9331c1b8 100644 --- a/app/src/main/java/com/tangem/di/NetworkModule.kt +++ b/app/src/main/java/com/tangem/di/NetworkModule.kt @@ -147,6 +147,19 @@ internal class NetworkModule { return builder.build() } + @Singleton + @Provides + @Named(Server.ApiBlockchair.URL_BLOCKCHAIR) + fun provideRetrofitBlockchair(): Retrofit { + val builder = Retrofit.Builder() + .baseUrl(Server.ApiBlockchair.URL_BLOCKCHAIR) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + if (BuildConfig.DEBUG) + builder.client(createOkHttpClient()) + return builder.build() + } + private fun createOkHttpClient(): OkHttpClient { return OkHttpClient.Builder().addInterceptor(createHttpLoggingInterceptor()).build() } diff --git a/app/src/main/java/com/tangem/wallet/CoinData.java b/app/src/main/java/com/tangem/wallet/CoinData.java index cac10cdc60..2e46eacc86 100644 --- a/app/src/main/java/com/tangem/wallet/CoinData.java +++ b/app/src/main/java/com/tangem/wallet/CoinData.java @@ -205,6 +205,10 @@ public abstract class CoinData { return sentTransactionsCount; } + public void setSentTransactionsCount(int sentTransactionsCount) { + this.sentTransactionsCount = sentTransactionsCount; + } + public void incSentTransactionsCount() { sentTransactionsCount++; } diff --git a/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java b/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java index f5614c7d03..c56b97052e 100644 --- a/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java +++ b/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java @@ -5,8 +5,12 @@ import android.text.InputFilter; import android.util.Log; import com.tangem.App; -import com.tangem.data.network.ElectrumRequest; -import com.tangem.data.network.ServerApiElectrum; +import com.tangem.data.network.ServerApiBlockchair; +import com.tangem.data.network.model.BlockchairAddressData; +import com.tangem.data.network.model.BlockchairAddressResponse; +import com.tangem.data.network.model.BlockchairStatsResponse; +import com.tangem.data.network.model.BlockchairTransactionResponse; +import com.tangem.data.network.model.BlockchairUnspentOutput; import com.tangem.tangem_card.data.TangemCard; import com.tangem.tangem_card.reader.CardProtocol; import com.tangem.tangem_card.tasks.SignTask; @@ -26,10 +30,6 @@ import com.tangem.wallet.UnspentOutputInfo; import com.tangem.wallet.btc.BtcData; import com.tangem.wallet.btc.Unspents; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - import java.io.ByteArrayOutputStream; import java.math.BigDecimal; import java.math.BigInteger; @@ -41,6 +41,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import io.reactivex.CompletableObserver; +import io.reactivex.SingleObserver; +import io.reactivex.observers.DisposableCompletableObserver; +import io.reactivex.observers.DisposableSingleObserver; + public class BtcCashEngine extends CoinEngine { private static final String TAG = BtcCashEngine.class.getSimpleName(); @@ -59,7 +64,6 @@ public class BtcCashEngine extends CoinEngine { } public BtcCashEngine() { - } private static int getDecimals() { @@ -73,7 +77,7 @@ public class BtcCashEngine extends CoinEngine { @Override public boolean awaitingConfirmation() { if (coinData == null) return false; - return coinData.getBalanceUnconfirmed() != 0 || App.pendingTransactionsStorage.hasTransactions(ctx.getCard()); + return coinData.getBalanceUnconfirmed() != 0 || coinData.isHasUnconfirmed() || App.pendingTransactionsStorage.hasTransactions(ctx.getCard()); } @Override @@ -127,50 +131,6 @@ public class BtcCashEngine extends CoinEngine { @Override public boolean validateAddress(String address) { -// 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; - return CashAddr.isValidCashAddress(address); } @@ -241,15 +201,7 @@ public class BtcCashEngine extends CoinEngine { return false; } - // Workaround before new back-end -// if (card.getRemainingSignatures() == card.getMaxSignatures()) { -// firstLine = "Verified balance"; -// secondLine = "Balance confirmed in blockchain. "; -// secondLine += "Verified note identity. "; -// return; -// } - - if (coinData.getBalanceUnconfirmed() != 0) { + if (coinData.getBalanceUnconfirmed() != 0 || coinData.isHasUnconfirmed()) { balanceValidator.setScore(0); balanceValidator.setFirstLine(R.string.balance_validator_first_line_transaction_in_progress); balanceValidator.setSecondLine(R.string.balance_validator_second_line_wait_for_confirmation); @@ -265,37 +217,6 @@ public class BtcCashEngine extends CoinEngine { balanceValidator.setSecondLine(R.string.empty_string); } } - - // rule 4 TODO: need to check SignedHashed against number of outputs in blockchain -// if((card.getRemainingSignatures() != card.getMaxSignatures()) && card.getBalance() != 0) -// { -// score = 80; -// firstLine = "Unguaranteed balance"; -// secondLine = "Potential unsent transaction. Redeem immediately if accept. "; -// return; -// } - -// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) { -// balanceValidator.setScore(80); -// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline); -// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance); -// } - -// if(card.getFailedBalanceRequestCounter()!=0) { -// score -= 5 * card.getFailedBalanceRequestCounter(); -// secondLine += "Not all nodes have returned balance. Swipe down or tap again. "; -// if(score <= 0) -// return; -// } - - // -// if(card.isBalanceReceived() && !card.isBalanceEqual()) { -// score = 0; -// firstLine = "Disputed balance"; -// secondLine += " Cannot obtain trusted balance at the moment. Try to tap and check this banknote later."; -// return; -// } - return true; } @@ -441,20 +362,13 @@ public class BtcCashEngine extends CoinEngine { try { String wallet = calculateAddress(ctx.getCard().getWalletPublicKeyRar()); ctx.getCoinData().setWallet(wallet); - } - catch (Exception e) - { + } catch (Exception e) { ctx.getCoinData().setWallet("ERROR"); throw new CardProtocol.TangemException("Can't define wallet address"); } } -// @Override -// public String getAmountDescription(TangemCard mCard, String amount) throws Exception { -// return mCard.getAmountDescription(Double.parseDouble(amount)); -// } - @Override public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception { @@ -464,12 +378,10 @@ public class BtcCashEngine extends CoinEngine { String destLegacyAddress = convertToLegacyAddress(targetAddress); byte[] pbKey = ctx.getCard().getWalletPublicKeyRar(); //ALWAYS USING COMPRESS KEY - // Build script for our address - List rawTxList = coinData.getUnspentTransactions(); - byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(srcLegacyAddress).bytes; - - // Collect unspent - ArrayList unspentOutputs = BCHUtils.getOutputs(rawTxList, outputScriptWeAreAbleToSpend); + final ArrayList unspentOutputs = new ArrayList<>(); + for (BtcData.UnspentTransaction utxo : coinData.getUnspentTransactions()) { + unspentOutputs.add(new UnspentOutputInfo(BTCUtils.fromHex(utxo.txID), new Transaction.Script(BTCUtils.fromHex(utxo.script)), utxo.amount, utxo.outputN, -1, utxo.txID, null)); + } long fullAmount = 0; for (int i = 0; i < unspentOutputs.size(); ++i) { @@ -513,7 +425,8 @@ public class BtcCashEngine extends CoinEngine { @Override public byte[][] getHashesToSign() throws Exception { byte[][] dataForSign = new byte[unspentOutputs.size()][]; - if (txForSign.length > 10) throw new Exception("To much hashes in one transaction!"); + if (txForSign.length > 10) + throw new Exception("To much hashes in one transaction!"); for (int i = 0; i < unspentOutputs.size(); ++i) { dataForSign[i] = bodyDoubleHash[i]; } @@ -562,115 +475,95 @@ public class BtcCashEngine extends CoinEngine { @Override public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) throws Exception { - final ServerApiElectrum serverApiElectrum = new ServerApiElectrum(); + final ServerApiBlockchair serverApiBlockchair = new ServerApiBlockchair(ctx.getBlockchain()); - ServerApiElectrum.ResponseListener electrumBodyListener = new ServerApiElectrum.ResponseListener() { + SingleObserver addressObserver = new DisposableSingleObserver() { @Override - public void onSuccess(ElectrumRequest electrumRequest) { - if (electrumRequest.isMethod(ElectrumRequest.METHOD_GetBalance)) { - try { - Long confBalance = electrumRequest.getResult().getLong("confirmed"); - Long unconfirmedBalance = electrumRequest.getResult().getLong("unconfirmed"); - coinData.setBalanceReceived(true); - coinData.setBalanceConfirmed(confBalance); - coinData.setBalanceUnconfirmed(unconfirmedBalance); - coinData.setValidationNodeDescription(serverApiElectrum.getValidationNodeDescription()); - } catch (JSONException e) { - e.printStackTrace(); - Log.e(TAG, "FAIL METHOD_GetBalance JSONException"); - } catch (Exception e) { - e.printStackTrace(); - Log.e(TAG, "FAIL METHOD_GetBalance Exception"); - } - } + public void onSuccess(BlockchairAddressResponse addressResponse) { + try { + BlockchairAddressData addressData = addressResponse.getData().get(coinData.getWallet()); - if (electrumRequest.isMethod(ElectrumRequest.METHOD_ListUnspent)) { - try { - String walletAddress = electrumRequest.getParams().getString(0); - JSONArray jsUnspentArray = electrumRequest.getResultArray(); - try { - coinData.getUnspentTransactions().clear(); - for (int i = 0; i < jsUnspentArray.length(); i++) { - JSONObject jsUnspent = jsUnspentArray.getJSONObject(i); - BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction(); - trUnspent.txID = jsUnspent.getString("tx_hash"); - trUnspent.amount = jsUnspent.getLong("value"); - trUnspent.outputN = jsUnspent.getInt("height"); - coinData.getUnspentTransactions().add(trUnspent); - } - } catch (JSONException e) { - e.printStackTrace(); - Log.e(TAG, "FAIL METHOD_ListUnspent JSONException"); + coinData.setBalanceConfirmed(addressData.getAddress().getBalance()); + coinData.setBalanceUnconfirmed(0L); + coinData.setBalanceReceived(true); + coinData.setValidationNodeDescription(serverApiBlockchair.getUrl()); + coinData.setSentTransactionsCount(addressData.getAddress().getOutputCount() - addressData.getAddress().getUnspentOutputCount()); + + for (BlockchairUnspentOutput utxo : addressData.getUnspentOutputs()) { + if (utxo.getBlock() != -1) { + BtcData.UnspentTransaction unspentTx = new BtcData.UnspentTransaction(); + unspentTx.txID = utxo.getTransactionHash(); + unspentTx.amount = utxo.getAmount(); + unspentTx.outputN = utxo.getIndex(); + coinData.getUnspentTransactions().add(unspentTx); + } else { + coinData.setHasUnconfirmed(true); } - - for (int i = 0; i < jsUnspentArray.length(); i++) { - JSONObject jsUnspent = jsUnspentArray.getJSONObject(i); - Integer height = jsUnspent.getInt("height"); - String hash = jsUnspent.getString("tx_hash"); - if (height != -1) { - if (blockchainRequestsCallbacks.allowAdvance()) { - serverApiElectrum.requestData(ctx, ElectrumRequest.getTransaction(walletAddress, hash)); - } else { - ctx.setError("Terminated by user"); - } - } - } - } catch (JSONException e) { - e.printStackTrace(); } - } - if (electrumRequest.isMethod(ElectrumRequest.METHOD_GetTransaction)) { - try { - String txHash = electrumRequest.txHash; - String raw = electrumRequest.getResultString(); - for (BtcData.UnspentTransaction tx : coinData.getUnspentTransactions()) { - if (tx.txID.equals(txHash)) - tx.script = raw; - } - } catch (JSONException e) { - e.printStackTrace(); + if (addressData.getAddress().getBalance() != 0) { + blockchainRequestsCallbacks.onComplete(true); + } else { + requestIsTransactionConfirmed(addressData.getTransactions().get(0), blockchainRequestsCallbacks); } - } - - if (serverApiElectrum.isRequestsSequenceCompleted()) { - blockchainRequestsCallbacks.onComplete(!ctx.hasError()); - }else{ - blockchainRequestsCallbacks.onProgress(); + } catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, "FAIL getAddress Exception"); + ctx.setError(e.getMessage()); + blockchainRequestsCallbacks.onComplete(false); } } @Override - public void onFail(ElectrumRequest electrumRequest) { - Log.i(TAG, "onFail: "+electrumRequest.getMethod()+" "+electrumRequest.getError()); - ctx.setError(electrumRequest.getError()); - if (serverApiElectrum.isRequestsSequenceCompleted()) { - blockchainRequestsCallbacks.onComplete(false);//serverApiElectrum.isErrorOccurred(), serverApiElectrum.getError()); - }else{ - blockchainRequestsCallbacks.onProgress(); - } + public void onError(Throwable e) { + e.printStackTrace(); + Log.e(TAG, "FAIL getAddress Exception"); + ctx.setError(e.getMessage()); + blockchainRequestsCallbacks.onComplete(false); } }; - serverApiElectrum.setResponseListener(electrumBodyListener); + serverApiBlockchair.getAddress(coinData.getWallet(), addressObserver); + } - serverApiElectrum.requestData(ctx, ElectrumRequest.checkBalance(convertToLegacyAddress(coinData.getWallet()))); - serverApiElectrum.requestData(ctx, ElectrumRequest.listUnspent(convertToLegacyAddress(coinData.getWallet()))); + private void requestIsTransactionConfirmed(String transaction, BlockchainRequestsCallbacks blockchainRequestsCallbacks) { + final ServerApiBlockchair serverApiBlockchair = new ServerApiBlockchair(ctx.getBlockchain()); + + SingleObserver transactionObserver = new DisposableSingleObserver() { + @Override + public void onSuccess(BlockchairTransactionResponse transactionResponse) { + if (transactionResponse.getData().get(transaction).getTransaction().getBlock() == -1) { + coinData.setHasUnconfirmed(true); + } + blockchainRequestsCallbacks.onComplete(true); + } + + @Override + public void onError(Throwable e) { + e.printStackTrace(); + Log.e(TAG, "FAIL getTransaction Exception"); + ctx.setError(e.getMessage()); + blockchainRequestsCallbacks.onComplete(false); + } + }; + + serverApiBlockchair.getTransaction(transaction, transactionObserver); } private Integer calculateEstimatedTransactionSize(String outputAddress, String outAmount) { try { - SignTask.TransactionToSign ps= constructTransaction(new Amount(outAmount, getBalanceCurrency()),new Amount("0.00",getFeeCurrency()), true, outputAddress ); + SignTask.TransactionToSign ps = constructTransaction(new Amount(outAmount, getBalanceCurrency()), new Amount("0.00", getFeeCurrency()), true, outputAddress); OnNeedSendTransaction onNeedSendTransactionBackup = onNeedSendTransaction; - onNeedSendTransaction =(tx)->{}; // empty function to bypass exception + onNeedSendTransaction = (tx) -> { + }; // empty function to bypass exception - byte[][] hashesToSign=ps.getHashesToSign(); + byte[][] hashesToSign = ps.getHashesToSign(); byte[] signFromCard = new byte[64 * hashesToSign.length]; Arrays.fill(signFromCard, (byte) 0x01); - byte[] txForSend=ps.onSignCompleted(signFromCard); + byte[] txForSend = ps.onSignCompleted(signFromCard); onNeedSendTransaction = onNeedSendTransactionBackup; - Log.e(TAG,"txForSend.length="+String.valueOf(txForSend.length)); - return txForSend.length +1; + Log.e(TAG, "txForSend.length=" + String.valueOf(txForSend.length)); + return txForSend.length + 1; } catch (Exception e) { e.printStackTrace(); Log.e(TAG, "Can't calculate transaction size -> use default!"); @@ -684,104 +577,64 @@ public class BtcCashEngine extends CoinEngine { public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) throws Exception { final int calcSize = calculateEstimatedTransactionSize(targetAddress, amount.toValueString()); Log.e(TAG, String.format("Estimated tx size %d", calcSize)); - coinData.minFee=null; - coinData.maxFee=null; - coinData.normalFee=null; + coinData.minFee = null; + coinData.maxFee = null; + coinData.normalFee = null; - final ServerApiElectrum serverApiElectrum = new ServerApiElectrum(); + final ServerApiBlockchair serverApiBlockchair = new ServerApiBlockchair(ctx.getBlockchain()); - final ServerApiElectrum.ResponseListener electrumListener = new ServerApiElectrum.ResponseListener() { + SingleObserver statsObserver = new DisposableSingleObserver() { @Override - public void onSuccess(ElectrumRequest electrumRequest) { - BigDecimal fee; - if (electrumRequest.isMethod(ElectrumRequest.METHOD_GetFee)) { - try { - fee = new BigDecimal(electrumRequest.getResultString()); //fee per KB - - if (fee.equals(BigDecimal.ZERO)) { - serverApiElectrum.requestData(ctx, ElectrumRequest.getFee()); - } - -// if (calcSize != 0) { - fee = fee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); // (per KB -> per byte)*size -// } else { -// serverApiElectrum.requestData(ctx, ElectrumRequest.getFee()); -// } - - //compare fee to usual relay fee - if (fee.compareTo(relayFee) < 0) { - fee = relayFee; - } - fee = fee.setScale(8, RoundingMode.DOWN); - - CoinEngine.Amount feeAmount = new CoinEngine.Amount(fee, ctx.getBlockchain().getCurrency()); - coinData.minFee = feeAmount; - coinData.normalFee = feeAmount; - coinData.maxFee = feeAmount; -// if (coinData.minFee != null && coinData.normalFee != null && coinData.maxFee != null) { - blockchainRequestsCallbacks.onComplete(true); -// } else { -// blockchainRequestsCallbacks.onProgress(); -// } - - } catch (JSONException e) { - e.printStackTrace(); + public void onSuccess(BlockchairStatsResponse blockchairStatsResponse) { + try { + int feeSatoshi = blockchairStatsResponse.getData().getFeePerByte() * calcSize; + BigDecimal fee = BigDecimal.valueOf(feeSatoshi).movePointLeft(getDecimals()); + if (fee.compareTo(relayFee) < 0) { + fee = relayFee; } + Amount feeAmount = new Amount(fee.setScale(getDecimals(), RoundingMode.DOWN), getFeeCurrency()); + coinData.minFee = feeAmount; + coinData.normalFee = feeAmount; + coinData.maxFee = feeAmount; + blockchainRequestsCallbacks.onComplete(true); + } catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, "FAIL getStats Exception"); + ctx.setError(e.getMessage()); + blockchainRequestsCallbacks.onComplete(false); } } @Override - public void onFail(ElectrumRequest electrumRequest) { - ctx.setError(electrumRequest.getError()); + public void onError(Throwable e) { + e.printStackTrace(); + Log.e(TAG, "FAIL getStats Exception"); + ctx.setError(e.getMessage()); blockchainRequestsCallbacks.onComplete(false); } }; - serverApiElectrum.setResponseListener(electrumListener); - serverApiElectrum.requestData(ctx, ElectrumRequest.getFee()); + serverApiBlockchair.getStats(statsObserver); } @Override public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) throws Exception { - final ServerApiElectrum serverApiElectrum = new ServerApiElectrum(); - final String txStr = BTCUtils.toHex(txForSend); + final ServerApiBlockchair serverApiBlockchair = new ServerApiBlockchair(ctx.getBlockchain()); - ServerApiElectrum.ResponseListener electrumBodyListener = new ServerApiElectrum.ResponseListener() { + CompletableObserver sendObserver = new DisposableCompletableObserver() { @Override - public void onSuccess(ElectrumRequest electrumRequest) { - if (electrumRequest.isMethod(ElectrumRequest.METHOD_SendTransaction)) { - try { - String resultString = electrumRequest.getResultString(); - if (resultString == null || resultString.isEmpty()) { - ctx.setError("Rejected by node: " + electrumRequest.getError()); - blockchainRequestsCallbacks.onComplete(false); - }else { - ctx.setError(null); - blockchainRequestsCallbacks.onComplete(true); - } - } catch (Exception e) { - if (e.getMessage() != null) { - ctx.setError(e.getMessage()); - blockchainRequestsCallbacks.onComplete(false); - } else { - ctx.setError(e.getClass().getName()); - blockchainRequestsCallbacks.onComplete(false); - } - } - } + public void onComplete() { + blockchainRequestsCallbacks.onComplete(true); } @Override - public void onFail(ElectrumRequest electrumRequest) { - ctx.setError(electrumRequest.getError()); + public void onError(Throwable e) { + ctx.setError(e.getMessage()); blockchainRequestsCallbacks.onComplete(false); } }; - serverApiElectrum.setResponseListener(electrumBodyListener); - - - serverApiElectrum.requestData(ctx, ElectrumRequest.broadcast(ctx.getCoinData().getWallet(), txStr)); + serverApiBlockchair.sendTransaction(Util.byteArrayToHexString(txForSend), sendObserver); } @Override From 7257712977fe78e855a500a82e16b7c0b747a2f0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Mar 2020 13:35:29 +0300 Subject: [PATCH 2/4] Updated on 2026-08-14 --- app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java b/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java index c56b97052e..48791ebffd 100644 --- a/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java +++ b/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java @@ -141,7 +141,7 @@ public class BtcCashEngine extends CoinEngine { @Override public Uri getWalletExplorerUri() { - return Uri.parse("https://bch.btc.com/" + ctx.getCoinData().getWallet()); + return Uri.parse("https://explorer.bitcoin.com/bch/address/" + ctx.getCoinData().getWallet()); } @Override From 560301be14a39e21f85377102f21f1e50cb09d4d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Mar 2020 14:47:59 +0300 Subject: [PATCH 3/4] Updated on 2026-08-14 --- .../java/com/tangem/commands/ReadCommand.kt | 18 ++++++++++------- .../java/com/tangem/common/tlv/TlvEncoder.kt | 6 +++--- .../java/com/tangem/common/tlv/TlvMapper.kt | 7 +------ .../main/java/com/tangem/tasks/ScanTask.kt | 3 +-- .../com/tangem/common/tlv/TlvMapperTest.kt | 20 +++++++++++++------ 5 files changed, 30 insertions(+), 24 deletions(-) diff --git a/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt b/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt index e6e2558685..0a161f392c 100644 --- a/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/ReadCommand.kt @@ -4,7 +4,6 @@ import com.tangem.common.CardEnvironment import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu -import com.tangem.common.extensions.calculateSha256 import com.tangem.common.tlv.Tlv import com.tangem.common.tlv.TlvBuilder import com.tangem.common.tlv.TlvMapper @@ -64,14 +63,19 @@ enum class CardStatus(val code: Int) { } } -enum class ProductMask(val code: Byte) { - Note(0x01), - Tag(0x02), - Card(0x04); +/** + * Mask of products enabled on card + * @property rawValue Products mask values, + * while flags definitions and values are in [ProductMask.Companion] as constants. + */ +data class ProductMask(val rawValue: Int) { + + fun contains(value: Int): Boolean = (rawValue and value) != 0 companion object { - private val values = values() - fun byCode(code: Byte): ProductMask? = values.find { it.code == code } + const val note = 0x01 + const val tag = 0x02 + const val idCard = 0x04 } } diff --git a/tangem-core/src/main/java/com/tangem/common/tlv/TlvEncoder.kt b/tangem-core/src/main/java/com/tangem/common/tlv/TlvEncoder.kt index e2dce7d743..1186dceeb3 100644 --- a/tangem-core/src/main/java/com/tangem/common/tlv/TlvEncoder.kt +++ b/tangem-core/src/main/java/com/tangem/common/tlv/TlvEncoder.kt @@ -74,12 +74,12 @@ class TlvEncoder { TlvValueType.ProductMask -> { typeCheck(tag) byteArrayOf( - (value as ProductMask).code + (value as ProductMask).rawValue.toByte() ) } TlvValueType.SettingsMask -> { typeCheck(tag) - (value as SettingsMask).rawValue.toByteArray() + (value as SettingsMask).rawValue.toByteArray(2) } TlvValueType.CardStatus -> { typeCheck(tag) @@ -97,7 +97,7 @@ class TlvEncoder { } private inline fun typeCheck(tag: TlvTag) { - if (T::class != ExpectedT::class){ + if (T::class != ExpectedT::class) { Log.e(this::class.simpleName!!, "Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}") throw TaskError.WrongType() diff --git a/tangem-core/src/main/java/com/tangem/common/tlv/TlvMapper.kt b/tangem-core/src/main/java/com/tangem/common/tlv/TlvMapper.kt index ead8085ebe..184f2fb49d 100644 --- a/tangem-core/src/main/java/com/tangem/common/tlv/TlvMapper.kt +++ b/tangem-core/src/main/java/com/tangem/common/tlv/TlvMapper.kt @@ -101,12 +101,7 @@ class TlvMapper(val tlvList: List) { } TlvValueType.ProductMask -> { typeCheck(tag) - try { - ProductMask.byCode(tlvValue.first()) as T - } catch (exception: Exception) { - logException(tag, tlvValue.first().toString(), exception) - throw TaskError.ConvertError() - } + ProductMask(tlvValue.toInt()) as T } TlvValueType.SettingsMask -> { typeCheck(tag) diff --git a/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt b/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt index 90a8629be0..f0160c48b3 100644 --- a/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt +++ b/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt @@ -3,7 +3,6 @@ package com.tangem.tasks import com.tangem.commands.* import com.tangem.common.CardEnvironment import com.tangem.common.CompletionResult -import com.tangem.crypto.CryptoUtils /** * Events that [ScanTask] returns on completion of its commands. @@ -38,7 +37,7 @@ internal class ScanTask : Task() { completeNfcSession(TaskError.MissingPreflightRead()) callback(TaskEvent.Completion(TaskError.MissingPreflightRead())) - } else if (currentCard.cardData?.productMask == ProductMask.Tag) { + } else if (currentCard.cardData?.productMask?.contains(ProductMask.tag) != false) { completeNfcSession() callback(TaskEvent.Completion()) diff --git a/tangem-core/src/test/java/com/tangem/common/tlv/TlvMapperTest.kt b/tangem-core/src/test/java/com/tangem/common/tlv/TlvMapperTest.kt index bbe41dee8a..b1ebcf143e 100644 --- a/tangem-core/src/test/java/com/tangem/common/tlv/TlvMapperTest.kt +++ b/tangem-core/src/test/java/com/tangem/common/tlv/TlvMapperTest.kt @@ -116,18 +116,26 @@ class TlvMapperTest { } @Test - fun `map ProductMask returns correct value`() { - val localMapper = TlvMapper(listOf(Tlv(TlvTag.ProductMask, byteArrayOf(4)))) + fun `map ProductMask with raw value 5 returns correct value`() { + val localMapper = TlvMapper(listOf(Tlv(TlvTag.ProductMask, byteArrayOf(5)))) val productMask: ProductMask = localMapper.map(TlvTag.ProductMask) - assertThat(productMask) - .isEqualTo(ProductMask.Card) + assertThat(productMask.contains(ProductMask.note) && productMask.contains(ProductMask.idCard)) + .isTrue() + } + + @Test + fun `map ProductMask with raw value 1 returns correct value`() { + val localMapper = TlvMapper(listOf(Tlv(TlvTag.ProductMask, byteArrayOf(1)))) + val productMask: ProductMask = localMapper.map(TlvTag.ProductMask) + assertThat(productMask.contains(ProductMask.note)) + .isTrue() } @Test fun `map Enum with unknown code throws ConversionException error`() { - val localMapper = TlvMapper(listOf(Tlv(TlvTag.ProductMask, byteArrayOf(5)))) + val localMapper = TlvMapper(listOf(Tlv(TlvTag.CurveId, "test".toByteArray()))) assertThrows { - localMapper.map(TlvTag.ProductMask) + localMapper.map(TlvTag.CurveId) } } From 35e1f44a3ea2255370abbdcc53a0b0bd2023e8d4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Mar 2020 16:25:33 +0300 Subject: [PATCH 4/4] Updated on 2026-08-14 --- app/src/main/java/com/tangem/wallet/bch/CashAddr.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/com/tangem/wallet/bch/CashAddr.java b/app/src/main/java/com/tangem/wallet/bch/CashAddr.java index 5dbf111c97..25c6851c16 100644 --- a/app/src/main/java/com/tangem/wallet/bch/CashAddr.java +++ b/app/src/main/java/com/tangem/wallet/bch/CashAddr.java @@ -44,8 +44,7 @@ public class CashAddr { byte[] checksumBytes = calculateChecksumBytesPolymod(allChecksumInput); checksumBytes = convertBits(checksumBytes, 8, 5, true); String cashAddress = BitcoinCashBase32.encode(concatenateByteArrays(payloadBytes, checksumBytes)); - //return prefixString + SEPARATOR + cashAddress; - return cashAddress; + return prefixString + SEPARATOR + cashAddress; } public static BitcoinCashAddressDecodedParts decodeCashAddress(String bitcoinCashAddress) {