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