diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index 5ad40fedc9..81229b390b 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -13,7 +13,9 @@ public enum Blockchain { 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"), - Litecoin("LTC", "LTC", 100000000.0, R.drawable.ic_logo_bitcoin, "Litecoin"); + Litecoin("LTC", "LTC", 100000000.0, R.drawable.ic_logo_bitcoin, "Litecoin"), + Rootstock("RSK", "RBTC", 1.0, R.drawable.ic_logo_bitcoin, "Rootstock"), + RootstockToken("Token", "ERC20", 1.0, R.drawable.ic_logo_bat_token, "Rootstock"); Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) { mID = ID; diff --git a/app/src/main/java/com/tangem/data/network/InsightApi.java b/app/src/main/java/com/tangem/data/network/InsightApi.java new file mode 100644 index 0000000000..b75e92d06c --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/InsightApi.java @@ -0,0 +1,32 @@ +package com.tangem.data.network; + +import com.tangem.data.network.model.InsightBody; +import com.tangem.data.network.model.InsightResponse; + +import java.util.List; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.Headers; +import retrofit2.http.POST; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface InsightApi { + @GET(ServerApiInsight.INSIGHT_ADDRESS) + Call insightAddress(@Path("address") String address); + + @GET(ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS) + Call> insightUnspent(@Path("address") String address); + + @GET(ServerApiInsight.INSIGHT_TRANSACTION) + Call insightTransaction(@Path("transaction") String transaction); + + @GET(ServerApiInsight.INSIGHT_FEE) + Call insightFee(); + + @Headers("Content-Type: application/json") + @POST(ServerApiInsight.INSIGHT_SEND) + Call insightSend(@Body InsightBody body ); +} diff --git a/app/src/main/java/com/tangem/data/network/RootstockApi.java b/app/src/main/java/com/tangem/data/network/RootstockApi.java new file mode 100644 index 0000000000..7faadfb4c0 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/RootstockApi.java @@ -0,0 +1,15 @@ +package com.tangem.data.network; + +import com.tangem.data.network.model.InfuraBody; +import com.tangem.data.network.model.InfuraResponse; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.Headers; +import retrofit2.http.POST; + +public interface RootstockApi { + @Headers("Content-Type: application/json") + @POST(Server.ApiRootstock.Method.MAIN) + Call rootstock(@Body InfuraBody 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 7e8bc996aa..9be4f58449 100644 --- a/app/src/main/java/com/tangem/data/network/Server.java +++ b/app/src/main/java/com/tangem/data/network/Server.java @@ -28,7 +28,15 @@ public class Server { public static final String URL_INFURA = ServerURL.API_INFURA; public static class Method { - static final String MAIN = URL_INFURA + "613a0b14833145968b1f656240c7d245"; + static final String MAIN = URL_INFURA + "v3/613a0b14833145968b1f656240c7d245"; + } + } + + public static class ApiRootstock { + public static final String URL_ROOTSTOCK = ServerURL.API_ROOTSTOCK; + + public static class Method { + static final String MAIN = URL_ROOTSTOCK; } } @@ -44,5 +52,4 @@ public class Server { static final String N_6 = URL_ESTIMATEFEE + "n/6"; } } - } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/ServerApiInsight.java b/app/src/main/java/com/tangem/data/network/ServerApiInsight.java new file mode 100644 index 0000000000..fc7de6214c --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/ServerApiInsight.java @@ -0,0 +1,131 @@ +package com.tangem.data.network; + +import android.support.annotation.NonNull; +import android.util.Log; + +import com.tangem.data.network.model.InsightBody; +import com.tangem.data.network.model.InsightResponse; + +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class ServerApiInsight { + private static String TAG = ServerApiInsight.class.getSimpleName(); + + public static final String INSIGHT_ADDRESS = "/addr/{address}"; + public static final String INSIGHT_UNSPENT_OUTPUTS = "/addr/{address}/utxo"; + public static final String INSIGHT_TRANSACTION = "/rawtx/{transaction}"; + public static final String INSIGHT_FEE = "/utils/estimatefee?nbBlocks=2,3,6"; + public static final String INSIGHT_SEND = "/tx/send"; + + private int requestsCount=0; + + public static String lastNode; + + public boolean isRequestsSequenceCompleted() { + Log.i(TAG, String.format("isRequestsSequenceCompleted: %s (%d requests left)", String.valueOf(requestsCount <= 0), requestsCount)); + return requestsCount <= 0; + } + + private InsightBodyListener insightBodyListener; + + public interface InsightBodyListener { + void onSuccess(String method, InsightResponse insightResponse); + void onSuccess(String method, List utxoList); + void onFail(String method, String message); + } + + public void setInsightResponse(InsightBodyListener listener) { + insightBodyListener = listener; + } + + public void insight(String method, String wallet, String tx) { + requestsCount++; + String insightURL = "http://130.185.109.17:3001/insight-api"; //TODO: make random selection + this.lastNode = insightURL; //TODO: show node instead of URL + + Retrofit retrofitInsight = new Retrofit.Builder() //TODO: move to NetworkModule+NetworkComponent if possible + .baseUrl(insightURL) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + +// InsightApi insightApi = App.getNetworkComponent().getRetrofitInsight(insightURL).create(InsightApi.class); + InsightApi insightApi = retrofitInsight.create(InsightApi.class); + + if (method.equals(INSIGHT_UNSPENT_OUTPUTS)) { + Call> call = insightApi.insightUnspent(wallet); + call.enqueue(new Callback>() { + @Override + public void onResponse(@NonNull Call> call, @NonNull Response> response) { + if (response.code() == 200) { + requestsCount--; + insightBodyListener.onSuccess(method, response.body()); + Log.i(TAG, "insight " + method + " onResponse " + response.code()); + } else { + insightBodyListener.onFail(method, String.valueOf(response.code())); + Log.e(TAG, "insight " + method + " onResponse " + response.code()); + } + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + insightBodyListener.onFail(method, String.valueOf(t.getMessage())); + Log.e(TAG, "insight " + method + " onFailure " + t.getMessage()); + } + }); + + } else { + Call call = null; + + switch (method) { + case INSIGHT_ADDRESS: + call = insightApi.insightAddress(wallet); + break; + + case INSIGHT_UNSPENT_OUTPUTS: + + break; + + case INSIGHT_TRANSACTION: + call = insightApi.insightTransaction(tx); + break; + + case INSIGHT_FEE: + call = insightApi.insightFee(); + break; + + case INSIGHT_SEND: + call = insightApi.insightSend(new InsightBody(tx)); + break; + + default: + call = insightApi.insightAddress(wallet); + break; + } + call.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.code() == 200) { + requestsCount--; + insightBodyListener.onSuccess(method, response.body()); + Log.i(TAG, "insight " + method + " onResponse " + response.code()); + } else { + insightBodyListener.onFail(method, String.valueOf(response.code())); + Log.e(TAG, "insight " + method + " onResponse " + response.code()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + insightBodyListener.onFail(method, String.valueOf(t.getMessage())); + Log.e(TAG, "insight " + method + " onFailure " + t.getMessage()); + } + }); + } + } +} diff --git a/app/src/main/java/com/tangem/data/network/ServerApiRootstock.java b/app/src/main/java/com/tangem/data/network/ServerApiRootstock.java new file mode 100644 index 0000000000..988c383639 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/ServerApiRootstock.java @@ -0,0 +1,105 @@ +package com.tangem.data.network; + +import android.support.annotation.NonNull; +import android.util.Log; + +import com.tangem.App; +import com.tangem.data.network.model.InfuraBody; +import com.tangem.data.network.model.InfuraResponse; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class ServerApiRootstock { + private static String TAG = ServerApiRootstock.class.getSimpleName(); + + /** + * HTTP + * Infura + *

+ * eth_getBalance + * eth_getTransactionCount + * eth_call + * eth_sendRawTransaction + * eth_gasPrice + */ + public static final String ROOTSTOCK_ETH_GET_BALANCE = "eth_getBalance"; + public static final String ROOTSTOCK_ETH_GET_TRANSACTION_COUNT = "eth_getTransactionCount"; + public static final String ROOTSTOCK_ETH_GET_PENDING_COUNT = "eth_getPendingCount"; + public static final String ROOTSTOCK_ETH_CALL = "eth_call"; + public static final String ROOTSTOCK_ETH_SEND_RAW_TRANSACTION = "eth_sendRawTransaction"; + public static final String ROOTSTOCK_ETH_GAS_PRICE = "eth_gasPrice"; + + private int requestsCount=0; + + public boolean isRequestsSequenceCompleted() { + Log.i(TAG, String.format("isRequestsSequenceCompleted: %s (%d requests left)", String.valueOf(requestsCount <= 0), requestsCount)); + return requestsCount <= 0; + } + + private RootstockBodyListener rootstockBodyListener; + + public interface RootstockBodyListener { + void onSuccess(String method, InfuraResponse infuraResponse); + + void onFail(String method, String message); + } + + public void setRootstockResponse(RootstockBodyListener listener) { + rootstockBodyListener = listener; + } + + public void rootstock(String method, int id, String wallet, String contract, String tx) { + requestsCount++; + RootstockApi rootstockApi = App.getNetworkComponent().getRetrofitRootstock().create(RootstockApi.class); + + InfuraBody infuraBody; + switch (method) { + case ROOTSTOCK_ETH_GET_BALANCE: + case ROOTSTOCK_ETH_GET_TRANSACTION_COUNT: + infuraBody = new InfuraBody(method, new String[]{wallet, "latest"}, id); + break; + case ROOTSTOCK_ETH_GET_PENDING_COUNT: + infuraBody = new InfuraBody(ROOTSTOCK_ETH_GET_TRANSACTION_COUNT, new String[]{wallet, "pending"}, id); + break; + case ROOTSTOCK_ETH_CALL: + String address = wallet.substring(2); + infuraBody = new InfuraBody(method, new Object[]{new InfuraBody.EthCallParams("0x70a08231000000000000000000000000" + address, contract), "latest"}, id); + break; + + case ROOTSTOCK_ETH_SEND_RAW_TRANSACTION: + infuraBody = new InfuraBody(method, new String[]{tx}, id); + break; + + case ROOTSTOCK_ETH_GAS_PRICE: + infuraBody = new InfuraBody(method, id); + break; + + default: + infuraBody = new InfuraBody(); + } + + Call call = rootstockApi.rootstock(infuraBody); + call.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.code() == 200) { + requestsCount--; + rootstockBodyListener.onSuccess(method, response.body()); + Log.i(TAG, "rootstock " + method + " onResponse " + response.code()); + } else { + rootstockBodyListener.onFail(method, String.valueOf(response.code())); + Log.e(TAG, "rootstock " + method + " onResponse " + response.code()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + rootstockBodyListener.onFail(method, String.valueOf(t.getMessage())); + Log.e(TAG, "rootstock " + method + " onFailure " + t.getMessage()); + } + }); + } + +} \ No newline at end of file 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 297b3ee60c..b33d4c4ba4 100644 --- a/app/src/main/java/com/tangem/data/network/ServerURL.java +++ b/app/src/main/java/com/tangem/data/network/ServerURL.java @@ -3,7 +3,8 @@ package com.tangem.data.network; class ServerURL { static final String API_TANGEM = "https://verify.tangem.com/"; static final String API_COINMARKETCAP = "https://api.coinmarketcap.com/"; - static final String API_INFURA = "https://mainnet.infura.io/v3/"; + static final String API_INFURA = "https://mainnet.infura.io/"; static final String API_ESTIMATEFEE = "https://estimatefee.com/"; static final String API_UPDATE_VERSION = "https://raw.githubusercontent.com/"; + static final String API_ROOTSTOCK = "https://public-node.rsk.co/"; } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/model/InsightBody.java b/app/src/main/java/com/tangem/data/network/model/InsightBody.java new file mode 100644 index 0000000000..4d8951adfa --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/InsightBody.java @@ -0,0 +1,9 @@ +package com.tangem.data.network.model; + +public class InsightBody { + private String rawtx; + + public InsightBody(String rawtx){ + this.rawtx = rawtx; + } +} diff --git a/app/src/main/java/com/tangem/data/network/model/InsightResponse.kt b/app/src/main/java/com/tangem/data/network/model/InsightResponse.kt new file mode 100644 index 0000000000..4910b36695 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/InsightResponse.kt @@ -0,0 +1,38 @@ +package com.tangem.data.network.model + +import com.google.gson.annotations.SerializedName + +data class InsightResponse( + @SerializedName("balanceSat") + var balanceSat: Long? = null, + + @SerializedName("unconfirmedBalanceSat") + var unconfirmedBalanceSat: Long? = null, + + @SerializedName("addrStr") + var addrStr: String = "", + + @SerializedName("txid") + var txid: String = "", + + @SerializedName("satoshis") + var satoshis: Long? = null, + + @SerializedName("height") + var height: Int? = null, + + @SerializedName("2") + var fee2: String = "", + + @SerializedName("3") + var fee3: String = "", + + @SerializedName("6") + var fee6: String = "", + + @SerializedName("rawtx") + var rawtx: String = "", + + @SerializedName("error") + var error: String = "" +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/di/NetworkComponent.java b/app/src/main/java/com/tangem/di/NetworkComponent.java index 4de9763426..f7367bdeb0 100644 --- a/app/src/main/java/com/tangem/di/NetworkComponent.java +++ b/app/src/main/java/com/tangem/di/NetworkComponent.java @@ -26,6 +26,12 @@ public interface NetworkComponent { @Named(Server.ApiUpdateVersion.URL_UPDATE_VERSION) Retrofit getRetrofitGithubusercontent(); + @Named(Server.ApiRootstock.URL_ROOTSTOCK) + Retrofit getRetrofitRootstock(); + +// @Named("Insight") //TODO:check +// Retrofit getRetrofitInsight(String insightURL); + @Named("socket") Socket getSocket(); diff --git a/app/src/main/java/com/tangem/di/NetworkModule.java b/app/src/main/java/com/tangem/di/NetworkModule.java index 86e669389e..e4ce02ddc8 100644 --- a/app/src/main/java/com/tangem/di/NetworkModule.java +++ b/app/src/main/java/com/tangem/di/NetworkModule.java @@ -31,6 +31,26 @@ class NetworkModule { .build(); } + @Singleton + @Provides + @Named(Server.ApiRootstock.URL_ROOTSTOCK) + Retrofit provideRetrofitRootstock() { + return new Retrofit.Builder() + .baseUrl(Server.ApiRootstock.URL_ROOTSTOCK) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + } + +// //@Singleton // TODO:check +// @Provides +// @Named("Insight") +// Retrofit provideRetrofitInsight(String insightURL) { +// return new Retrofit.Builder() +// .baseUrl(insightURL) +// .addConverterFactory(GsonConverterFactory.create()) +// .build(); +// } + @Singleton @Provides @Named(Server.ApiEstimatefee.URL_ESTIMATEFEE) diff --git a/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.kt b/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.kt index 951b7eb457..adf693ade8 100644 --- a/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.kt +++ b/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.kt @@ -8,6 +8,8 @@ import com.tangem.domain.wallet.token.TokenEngine import com.tangem.domain.wallet.bch.BtcCashEngine import com.tangem.data.Blockchain import com.tangem.domain.wallet.ltc.LtcEngine +import com.tangem.domain.wallet.rsk.RskEngine +import com.tangem.domain.wallet.rsk.RskTokenEngine /** * Factory for create specific engine @@ -27,11 +29,14 @@ object CoinEngineFactory { Blockchain.Ethereum, Blockchain.EthereumTestNet -> EthEngine() Blockchain.Token -> TokenEngine() Blockchain.Litecoin -> LtcEngine() + Blockchain.Rootstock -> RskEngine() + Blockchain.RootstockToken -> RskTokenEngine() else -> null } } fun create(context: TangemContext): CoinEngine? { + var result: CoinEngine? try { result = if (Blockchain.BitcoinCash == context.blockchain) @@ -44,6 +49,10 @@ object CoinEngineFactory { TokenEngine(context) else if (Blockchain.Litecoin == context.blockchain) LtcEngine(context) + else if (Blockchain.Rootstock == context.blockchain) + RskEngine(context) + else if (Blockchain.RootstockToken == context.blockchain) + RskTokenEngine(context) else return null } catch (e: Exception) { diff --git a/app/src/main/java/com/tangem/domain/wallet/TangemContext.java b/app/src/main/java/com/tangem/domain/wallet/TangemContext.java index 5802887489..8a837bb611 100644 --- a/app/src/main/java/com/tangem/domain/wallet/TangemContext.java +++ b/app/src/main/java/com/tangem/domain/wallet/TangemContext.java @@ -33,6 +33,10 @@ public class TangemContext { { return Blockchain.Token; } + if( (blockchain==Blockchain.Rootstock)&& card.isToken() ) + { + return Blockchain.RootstockToken; + } return blockchain; } @@ -45,9 +49,9 @@ public class TangemContext { public String getBlockchainName() { Blockchain blockchain=getBlockchain(); - if( (blockchain==Blockchain.Ethereum || blockchain==Blockchain.EthereumTestNet)&& card.isToken() ) { + if( blockchain==Blockchain.Token || blockchain==Blockchain.RootstockToken ) { String token = card.getTokenSymbol(); - return token + "
" + getBlockchain().getOfficialName() + " ERC20 token"; + return token + "
" + getBlockchain().getOfficialName() + " smart contract token"; }else { return blockchain.getOfficialName(); } diff --git a/app/src/main/java/com/tangem/domain/wallet/bch/BitcoinCashNode.kt b/app/src/main/java/com/tangem/domain/wallet/bch/BitcoinCashNode.kt index e08f74eacf..40a32ea6e1 100644 --- a/app/src/main/java/com/tangem/domain/wallet/bch/BitcoinCashNode.kt +++ b/app/src/main/java/com/tangem/domain/wallet/bch/BitcoinCashNode.kt @@ -4,21 +4,17 @@ enum class BitcoinCashNode(val host: String, val port: Int, val proto: String) { N_001("electrumx.hillsideinternet.com", 50002, "ssl"), N_002("bch0.kister.net", 50002, "ssl"), N_003("abc1.hsmiths.com", 60002, "ssl"), - N_004("bch.curalle.ovh", 50002, "ssl"), - N_005("207.180.215.112", 52002, "ssl"), - N_006("bch.imaginary.cash", 50002, "ssl"), - N_007("dedi.jochen-hoenicke.de", 51002, "ssl"), - N_008("crypto.mldlabs.com", 50002, "ssl"), - N_009("bch.electrumx.cash", 50002, "ssl"), - N_010("electroncash.cascharia.com", 50002, "ssl"), - N_011("bch.crypto.mldlabs.com", 50002, "ssl"), - N_012("electron-cash.dragon.zone", 50002, "ssl"), - N_013("electron.coinucopia.io", 50002, "ssl"), - N_014("blackie.c3-soft.com", 50002, "ssl"), - N_015("electroncash.ueo.ch", 51002, "ssl"), - N_016("electrum.imaginary.cash", 50002, "ssl"), - N_017("35.157.238.5", 51002, "ssl"), - N_018("bitcoincash.quangld.com", 50002, "ssl"), - N_019("bch.stitthappens.com", 50002, "ssl"), - N_020("electroncash.dk", 50002, "ssl"), + N_004("bch.imaginary.cash", 50002, "ssl"), + N_005("dedi.jochen-hoenicke.de", 51002, "ssl"), + N_006("crypto.mldlabs.com", 50002, "ssl"), + N_007("electroncash.cascharia.com", 50002, "ssl"), + N_008("bch.crypto.mldlabs.com", 50002, "ssl"), + N_009("electron-cash.dragon.zone", 50002, "ssl"), + N_010("electron.coinucopia.io", 50002, "ssl"), + N_011("blackie.c3-soft.com", 50002, "ssl"), + N_012("electroncash.ueo.ch", 51002, "ssl"), + N_013("electrum.imaginary.cash", 50002, "ssl"), + N_014("bitcoincash.quangld.com", 50002, "ssl"), + N_015("bch.stitthappens.com", 50002, "ssl"), + N_016("electroncash.dk", 50002, "ssl"), } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/domain/wallet/bch/BtcCashEngine.java b/app/src/main/java/com/tangem/domain/wallet/bch/BtcCashEngine.java index 61c55ba38f..bca4b4cb3d 100644 --- a/app/src/main/java/com/tangem/domain/wallet/bch/BtcCashEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/bch/BtcCashEngine.java @@ -5,13 +5,11 @@ import android.text.InputFilter; import android.util.Log; import com.tangem.data.network.ElectrumRequest; -import com.tangem.data.network.ServerApiCommon; import com.tangem.data.network.ServerApiElectrum; import com.tangem.domain.wallet.BCHUtils; import com.tangem.domain.wallet.BTCUtils; import com.tangem.tangemcard.reader.CardProtocol; import com.tangem.domain.wallet.BalanceValidator; -import com.tangem.data.Blockchain; import com.tangem.domain.wallet.btc.BtcData; import com.tangem.domain.wallet.CoinData; import com.tangem.domain.wallet.CoinEngine; @@ -24,7 +22,6 @@ import com.tangem.util.CryptoUtil; import com.tangem.util.DecimalDigitsInputFilter; import com.tangem.util.DerEncodingUtil; import com.tangem.tangemcard.util.Util; -import com.tangem.util.FormatUtil; import com.tangem.wallet.R; import org.json.JSONArray; @@ -594,7 +591,7 @@ public class BtcCashEngine extends CoinEngine { JSONObject jsUnspent = jsUnspentArray.getJSONObject(i); BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction(); trUnspent.txID = jsUnspent.getString("tx_hash"); - trUnspent.Amount = jsUnspent.getInt("value"); + trUnspent.Amount = jsUnspent.getLong("value"); trUnspent.Height = jsUnspent.getInt("height"); coinData.getUnspentTransactions().add(trUnspent); } diff --git a/app/src/main/java/com/tangem/domain/wallet/btc/BitcoinNode.kt b/app/src/main/java/com/tangem/domain/wallet/btc/BitcoinNode.kt index 0d5d7cb997..a8bfc5b3ee 100644 --- a/app/src/main/java/com/tangem/domain/wallet/btc/BitcoinNode.kt +++ b/app/src/main/java/com/tangem/domain/wallet/btc/BitcoinNode.kt @@ -7,79 +7,58 @@ enum class BitcoinNode(val host: String, val port: Int, val proto: String) { N_004("ip239.ip-54-36-234.eu", 50001, "tcp"), N_005("electrum-server.ninja", 50001, "tcp"), N_006("174.138.11.174", 50001, "tcp"), - N_007("ndnd.selfhost.eu", 50001, "tcp"), - N_008("btc.cihar.com", 50001, "tcp"), - N_009("vps.hsmiths.com", 8080, "tcp"), - N_010("electrum.hsmiths.com", 8080, "tcp"), - N_011("ip120.ip-54-37-91.eu", 50001, "tcp"), - N_012("vps.hsmiths.com", 50001, "tcp"), - N_013("orannis.com", 50001, "tcp"), - N_014("ip101.ip-54-37-91.eu", 50001, "tcp"), - N_015("e-x.not.fyi", 50001, "tcp"), - N_016("electrum.hsmiths.com", 50001, "tcp"), - N_017("electrum.vom-stausee.de", 50001, "tcp"), - N_018("bitcoin.corgi.party", 50001, "tcp"), - N_019("electrum2.eff.ro", 50001, "tcp"), - N_020("electrum.coinucopia.io", 50001, "tcp"), - N_021("electrum.eff.ro", 50001, "tcp"), - N_022("btc.xskyx.net", 50001, "tcp"), - N_023("kirsche.emzy.de", 50001, "tcp"), - N_024("electrum.petrkr.net", 50001, "tcp"), - N_025("btc.knas.systems", 50001, "tcp"), - N_026("b.ooze.cc", 50002, "ssl"), - N_027("electrum.nute.net", 50002, "ssl"), - N_028("ndnd.selfhost.eu", 50002, "ssl"), - N_029("electrum.coinop.cc", 50002, "ssl"), - N_030("orannis.com", 50002, "ssl"), - N_031("electrum.vom-stausee.de", 50002, "ssl"), - N_032("ip119.ip-54-37-91.eu", 50002, "ssl"), - N_033("ip101.ip-54-37-91.eu", 50002, "ssl"), - N_034("electrum2.villocq.com", 50002, "ssl"), - N_035("dedi.jochen-hoenicke.de", 50002, "ssl"), - N_036("174.138.11.174", 50002, "ssl"), - N_037("tomscryptos.com", 50002, "ssl"), - N_038("elec.luggs.co", 443, "ssl"), - N_039("ip239.ip-54-36-234.eu", 50002, "ssl"), - N_040("bitcoins.sk", 50002, "ssl"), - N_041("btc.cihar.com", 50002, "ssl"), - N_042("e-x.not.fyi", 50002, "ssl"), - N_043("ip120.ip-54-37-91.eu", 50002, "ssl"), - N_044("electrum.villocq.com", 50002, "ssl"), - N_045("electrum.anduck.net", 50012, "ssl"), - N_046("technetium.network", 50002, "ssl"), - N_047("electrum.coinucopia.io", 50002, "ssl"), - N_048("currentlane.lovebitco.in", 50002, "ssl"), - N_049("dimon.trimon.de", 50002, "ssl"), - N_050("rbx.curalle.ovh", 50002, "ssl"), - N_051("btc.gravitech.net", 50002, "ssl"), - N_052("hetzner01.fischl-online.de", 50002, "ssl"), - N_053("fn.48.org", 50002, "ssl"), - N_054("185.64.116.15", 50002, "ssl"), - N_055("kirsche.emzy.de", 50002, "ssl"), - N_056("109.192.105.174", 50002, "ssl"), - N_057("fedaykin.goip.de", 50002, "ssl"), - N_058("vps.hsmiths.com", 50002, "ssl"), - N_059("104.250.141.242", 50002, "ssl"), - N_060("electrum.qtornado.com", 50002, "ssl"), - N_061("electrum-server.ninja", 50002, "ssl"), - N_062("electrum2.eff.ro", 50002, "ssl"), - N_063("electrum.hsmiths.com", 995, "ssl"), - N_064("electrum.hsmiths.com", 50002, "ssl"), - N_065("139.162.14.142", 50002, "ssl"), - N_066("electrum.eff.ro", 50002, "ssl"), - N_067("electrum.taborsky.cz", 50002, "ssl"), - N_068("electrum.festivaldelhumor.org", 50002, "ssl"), - N_069("electrum.petrkr.net", 50002, "ssl"), - N_070("us.electrum.be", 50002, "ssl"), - N_071("bitcoin-node.org", 50002, "ssl"), - N_072("vmd27610.contaboserver.net", 50002, "ssl"), - N_073("electrumx.soon.it", 50002, "ssl"), - N_074("vmd30612.contaboserver.net", 50002, "ssl"), - N_075("enode.duckdns.org", 50002, "ssl"), - N_076("81-7-13-84.blue.kundencontroller.de", 50002, "ssl"), - N_077("electrum.scumm.it", 50002, "ssl"), - N_078("helicarrier.bauerj.eu", 50002, "ssl"), - N_079("tardis.bauerj.eu", 50002, "ssl"), - N_080("such.ninja", 50002, "ssl"), - N_081("electrum.be", 50002, "ssl"), + N_007("vps.hsmiths.com", 8080, "tcp"), + N_008("ip120.ip-54-37-91.eu", 50001, "tcp"), + N_009("ip101.ip-54-37-91.eu", 50001, "tcp"), + N_010("e-x.not.fyi", 50001, "tcp"), + N_011("electrum.vom-stausee.de", 50001, "tcp"), + N_012("electrum2.eff.ro", 50001, "tcp"), + N_013("electrum.coinucopia.io", 50001, "tcp"), + N_014("kirsche.emzy.de", 50001, "tcp"), + N_015("electrum.petrkr.net", 50001, "tcp"), + N_016("btc.knas.systems", 50001, "tcp"), + N_017("b.ooze.cc", 50002, "ssl"), + N_018("electrum.nute.net", 50002, "ssl"), + N_019("electrum.coinop.cc", 50002, "ssl"), + N_020("electrum.vom-stausee.de", 50002, "ssl"), + N_021("ip119.ip-54-37-91.eu", 50002, "ssl"), + N_022("ip101.ip-54-37-91.eu", 50002, "ssl"), + N_023("dedi.jochen-hoenicke.de", 50002, "ssl"), + N_024("174.138.11.174", 50002, "ssl"), + N_025("ip239.ip-54-36-234.eu", 50002, "ssl"), + N_026("bitcoins.sk", 50002, "ssl"), + N_027("e-x.not.fyi", 50002, "ssl"), + N_028("ip120.ip-54-37-91.eu", 50002, "ssl"), + N_029("electrum.villocq.com", 50002, "ssl"), + N_030("electrum.anduck.net", 50012, "ssl"), + N_031("technetium.network", 50002, "ssl"), + N_032("electrum.coinucopia.io", 50002, "ssl"), + N_033("dimon.trimon.de", 50002, "ssl"), + N_034("rbx.curalle.ovh", 50002, "ssl"), + N_035("btc.gravitech.net", 50002, "ssl"), + N_036("hetzner01.fischl-online.de", 50002, "ssl"), + N_037("fn.48.org", 50002, "ssl"), + N_038("185.64.116.15", 50002, "ssl"), + N_039("kirsche.emzy.de", 50002, "ssl"), + N_040("109.192.105.174", 50002, "ssl"), + N_041("fedaykin.goip.de", 50002, "ssl"), + N_042("vps.hsmiths.com", 50002, "ssl"), + N_043("104.250.141.242", 50002, "ssl"), + N_044("electrum.qtornado.com", 50002, "ssl"), + N_045("electrum-server.ninja", 50002, "ssl"), + N_046("electrum2.eff.ro", 50002, "ssl"), + N_047("electrum.hsmiths.com", 995, "ssl"), + N_048("electrum.hsmiths.com", 50002, "ssl"), + N_049("139.162.14.142", 50002, "ssl"), + N_050("electrum.petrkr.net", 50002, "ssl"), + N_051("us.electrum.be", 50002, "ssl"), + N_052("bitcoin-node.org", 50002, "ssl"), + N_053("electrumx.soon.it", 50002, "ssl"), + N_054("vmd30612.contaboserver.net", 50002, "ssl"), + N_055("81-7-13-84.blue.kundencontroller.de", 50002, "ssl"), + N_056("electrum.scumm.it", 50002, "ssl"), + N_057("helicarrier.bauerj.eu", 50002, "ssl"), + N_058("tardis.bauerj.eu", 50002, "ssl"), + N_059("such.ninja", 50002, "ssl"), + N_060("electrum.be", 50002, "ssl"), } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/domain/wallet/btc/BtcData.java b/app/src/main/java/com/tangem/domain/wallet/btc/BtcData.java index f036a85aa0..5a40f1ae6a 100644 --- a/app/src/main/java/com/tangem/domain/wallet/btc/BtcData.java +++ b/app/src/main/java/com/tangem/domain/wallet/btc/BtcData.java @@ -35,14 +35,14 @@ public class BtcData extends CoinData { public static class UnspentTransaction { public String txID; - public Integer Amount; + public Long Amount; public Integer Height; public String Raw = ""; public Bundle getAsBundle() { Bundle B = new Bundle(); B.putString("txID", txID); - B.putInt("Amount", Amount); + B.putLong("Amount", Amount); B.putInt("Height", Height); B.putString("Raw", Raw); return B; @@ -50,7 +50,7 @@ public class BtcData extends CoinData { public void loadFromBundle(Bundle B) { txID = B.getString("txID"); - Amount = B.getInt("Amount"); + Amount = B.getLong("Amount"); Height = B.getInt("Height"); Raw = B.getString("Raw"); } diff --git a/app/src/main/java/com/tangem/domain/wallet/btc/BtcEngine.java b/app/src/main/java/com/tangem/domain/wallet/btc/BtcEngine.java index 80f0e11d56..42b0073218 100644 --- a/app/src/main/java/com/tangem/domain/wallet/btc/BtcEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/btc/BtcEngine.java @@ -561,7 +561,7 @@ public class BtcEngine extends CoinEngine { JSONObject jsUnspent = jsUnspentArray.getJSONObject(i); BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction(); trUnspent.txID = jsUnspent.getString("tx_hash"); - trUnspent.Amount = jsUnspent.getInt("value"); + trUnspent.Amount = jsUnspent.getLong("value"); trUnspent.Height = jsUnspent.getInt("height"); coinData.getUnspentTransactions().add(trUnspent); } diff --git a/app/src/main/java/com/tangem/domain/wallet/ducatus/DucatusEngine.java b/app/src/main/java/com/tangem/domain/wallet/ducatus/DucatusEngine.java new file mode 100644 index 0000000000..682d62ac33 --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/ducatus/DucatusEngine.java @@ -0,0 +1,707 @@ +package com.tangem.domain.wallet.ducatus; + +import android.net.Uri; +import android.text.InputFilter; +import android.util.Log; + +import com.tangem.data.network.ElectrumRequest; +import com.tangem.data.network.InsightApi; +import com.tangem.data.network.ServerApiInsight; +import com.tangem.data.network.model.InsightResponse; +import com.tangem.domain.wallet.BTCUtils; +import com.tangem.domain.wallet.BalanceValidator; +import com.tangem.domain.wallet.Base58; +import com.tangem.domain.wallet.CoinData; +import com.tangem.domain.wallet.TangemContext; +import com.tangem.domain.wallet.Transaction; +import com.tangem.domain.wallet.UnspentOutputInfo; +import com.tangem.domain.wallet.btc.BtcData; +import com.tangem.domain.wallet.btc.BtcEngine; +import com.tangem.tangemcard.data.TangemCard; +import com.tangem.tangemcard.reader.CardProtocol; +import com.tangem.tangemcard.tasks.SignTask; +import com.tangem.tangemcard.util.Util; +import com.tangem.util.CryptoUtil; +import com.tangem.util.DecimalDigitsInputFilter; +import com.tangem.util.DerEncodingUtil; +import com.tangem.wallet.R; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class DucatusEngine extends BtcEngine { + private static final String TAG = DucatusEngine.class.getSimpleName(); + public BtcData coinData = null; + + public DucatusEngine(TangemContext context) throws Exception { + super(context); + if (context.getCoinData() == null) { + coinData = new BtcData(); + context.setCoinData(coinData); + } else if (context.getCoinData() instanceof BtcData) { + coinData = (BtcData) context.getCoinData(); + } else { + throw new Exception("Invalid type of Blockchain data for LtcEngine"); + } + } + + public DucatusEngine() { + super(); + } + + private static int getDecimals() { + return 8; + } + + + private void checkBlockchainDataExists() throws Exception { + if (coinData == null) throw new Exception("No blockchain data"); + } + + @Override + public boolean awaitingConfirmation() { + if (coinData == null) return false; + return coinData.getBalanceUnconfirmed() != 0; + } + + @Override + public String getBalanceHTML() { + Amount balance = getBalance(); + if (balance != null) { + return balance.toDescriptionString(getDecimals()); + } else { + return ""; + } + } + + @Override + public String getBalanceCurrency() { + return "LTC"; + } + + @Override + public String getOfflineBalanceHTML() { + InternalAmount offlineInternalAmount = convertToInternalAmount(ctx.getCard().getOfflineBalance()); + Amount offlineAmount = convertToAmount(offlineInternalAmount); + return offlineAmount.toDescriptionString(getDecimals()); + } + + @Override + public boolean isBalanceNotZero() { + if (coinData == null) return false; + if (coinData.getBalanceInInternalUnits() == null) return false; + return coinData.getBalanceInInternalUnits().notZero(); + } + + @Override + public boolean hasBalanceInfo() { + if (coinData == null) return false; + return coinData.hasBalanceInfo(); + } + + + @Override + public boolean isExtractPossible() { + if (!hasBalanceInfo()) { + ctx.setMessage(R.string.cannot_obtain_data_from_blockchain); + } else if (!isBalanceNotZero()) { + ctx.setMessage(R.string.wallet_empty); + } else if (awaitingConfirmation()) { + ctx.setMessage(R.string.please_wait_while_previous); + } else if (coinData.getUnspentTransactions().size() == 0) { + ctx.setMessage(R.string.please_wait_for_confirmation); + } else { + return true; + } + return false; + } + + @Override + public String getFeeCurrency() { + return "LTC"; + } + + @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("L") && !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; + } + + return true; + } + + + @Override + public boolean isNeedCheckNode() { + return true; + } + + @Override + public Uri getShareWalletUriExplorer() { + return Uri.parse("https://live.blockcypher.com/ltc/address/" + ctx.getCoinData().getWallet()); + } + + @Override + public Uri getShareWalletUri() { + if (ctx.getCard().getDenomination() != null) { + return Uri.parse("litecoin:" + ctx.getCoinData().getWallet() + "?amount=" + convertToAmount(convertToInternalAmount(ctx.getCard().getDenomination())).toValueString(8)); + } else { + return Uri.parse("litecoin:" + ctx.getCoinData().getWallet()); + } + } + + @Override + public InputFilter[] getAmountInputFilters() { + return new InputFilter[]{new DecimalDigitsInputFilter(getDecimals())}; + } + + @Override + public boolean checkNewTransactionAmount(Amount amount) { + if (coinData == null) return false; + if (amount.compareTo(convertToAmount(coinData.getBalanceInInternalUnits())) > 0) { + return false; + } + return true; + } + + @Override + public boolean checkNewTransactionAmountAndFee(Amount amountValue, Amount feeValue, Boolean isIncludeFee) { + InternalAmount fee; + InternalAmount amount; + + try { + checkBlockchainDataExists(); + amount = convertToInternalAmount(amountValue); + fee = convertToInternalAmount(feeValue); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + if (fee == null || amount == null) + return false; + + if (fee.isZero() || amount.isZero()) + return false; + + if (isIncludeFee && (amount.compareTo(coinData.getBalanceInInternalUnits()) > 0 || amount.compareTo(fee) < 0)) + return false; + + if (!isIncludeFee && amount.add(fee).compareTo(coinData.getBalanceInInternalUnits()) > 0) + return false; + + return true; + } + + @Override + public boolean validateBalance(BalanceValidator balanceValidator) { + if (((ctx.getCard().getOfflineBalance() == null) && !ctx.getCoinData().isBalanceReceived()) || (!ctx.getCoinData().isBalanceReceived() && (ctx.getCard().getRemainingSignatures() != ctx.getCard().getMaxSignatures()))) { + balanceValidator.setScore(0); + balanceValidator.setFirstLine("Unknown balance"); + balanceValidator.setSecondLine("Balance cannot be verified. Swipe down to refresh."); + 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) { + balanceValidator.setScore(0); + balanceValidator.setFirstLine("Transaction in progress"); + balanceValidator.setSecondLine("Wait for confirmation in blockchain"); + return false; + } + + if (coinData.isBalanceReceived() && coinData.isBalanceEqual()) { + balanceValidator.setScore(100); + balanceValidator.setFirstLine("Verified balance"); + balanceValidator.setSecondLine("Balance confirmed in blockchain"); + if (coinData.getBalanceInInternalUnits().isZero()) { + balanceValidator.setFirstLine("Empty wallet"); + balanceValidator.setSecondLine(""); + } + } + + // 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()) && coinData.getBalanceInInternalUnits().notZero()) { + balanceValidator.setScore(80); + balanceValidator.setFirstLine("Verified offline balance"); + balanceValidator.setSecondLine("Can't obtain balance from blockchain. Restore internet connection to be more confident. "); + } + +// 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; + } + + @Override + public Amount getBalance() { + if (!hasBalanceInfo()) return null; + return convertToAmount(coinData.getBalanceInInternalUnits()); + } + + @Override + public String evaluateFeeEquivalent(String fee) { + if (!coinData.getAmountEquivalentDescriptionAvailable()) return ""; + try { + Amount feeAmount = new Amount(fee, getFeeCurrency()); + return feeAmount.toEquivalentString(coinData.getRate()); + } catch (Exception e) { + return ""; + } + } + + @Override + public String getBalanceEquivalent() { + if (coinData == null || !coinData.getAmountEquivalentDescriptionAvailable()) return ""; + Amount balance = getBalance(); + if (balance == null) return ""; + return balance.toEquivalentString(coinData.getRate()); + } + + @Override + public String calculateAddress(byte[] pkUncompressed) throws NoSuchProviderException, NoSuchAlgorithmException { + byte netSelectionByte = (byte) 0x30; + + byte hash1[] = Util.calculateSHA256(pkUncompressed); + byte hash2[] = Util.calculateRIPEMD160(hash1); + + 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((byte) 0x6f); + 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()); + } + + @Override + public Amount convertToAmount(InternalAmount internalAmount) { + BigDecimal d = internalAmount.divide(new BigDecimal("100000000")); + return new Amount(d, getBalanceCurrency()); + } + + @Override + public Amount convertToAmount(String strAmount, String currency) { + return new Amount(strAmount, currency); + } + + @Override + public InternalAmount convertToInternalAmount(Amount amount) { + BigDecimal d = amount.multiply(new BigDecimal("100000000")); + return new InternalAmount(d, "Satoshi"); + } + + @Override + public InternalAmount convertToInternalAmount(byte[] bytes) { + if (bytes == null) return null; + byte[] reversed = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) reversed[i] = bytes[bytes.length - i - 1]; + return new InternalAmount(Util.byteArrayToLong(reversed), "Satoshi"); + } + + @Override + public byte[] convertToByteArray(InternalAmount internalAmount) { + byte[] bytes = Util.longToByteArray(internalAmount.longValueExact()); + byte[] reversed = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) reversed[i] = bytes[bytes.length - i - 1]; + return reversed; + } + + @Override + public CoinData createCoinData() { + return new BtcData(); + } + + @Override + public String getUnspentInputsDescription() { + return coinData.getUnspentInputsDescription(); + } + + @Override + public SignTask.PaymentToSign constructPayment(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception { + final ArrayList unspentOutputs; + checkBlockchainDataExists(); + + String myAddress = ctx.getCoinData().getWallet(); + byte[] pbKey = ctx.getCard().getWalletPublicKey(); + + // Build script for our address + List rawTxList = coinData.getUnspentTransactions(); + byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(myAddress).bytes; + + // Collect unspent + unspentOutputs = BTCUtils.getOutputs(rawTxList, outputScriptWeAreAbleToSpend); + + long fullAmount = 0; + for (int i = 0; i < unspentOutputs.size(); ++i) { + fullAmount += unspentOutputs.get(i).value; + } + + long fees = convertToInternalAmount(feeValue).longValueExact(); + long amount = convertToInternalAmount(amountValue).longValueExact(); + long change = fullAmount - amount; + if (IncFee) { + amount = amount - fees; + } else { + change = change - fees; + } + + final long amountFinal=amount; + final long changeFinal=change; + + if (amount + fees > fullAmount) { + throw new CardProtocol.TangemException_WrongAmount(String.format("Balance (%d) < change (%d) + amount (%d)", fullAmount, change, amount)); + } + + final byte[][] txForSign = new byte[unspentOutputs.size()][]; + final byte[][] bodyDoubleHash = new byte[unspentOutputs.size()][]; + final byte[][] bodyHash= new byte[unspentOutputs.size()][]; + + for (int i = 0; i < unspentOutputs.size(); ++i) { + txForSign[i] = BTCUtils.buildTXForSign(myAddress, targetAddress, myAddress, unspentOutputs, i, amount, change); + bodyHash[i] = Util.calculateSHA256(txForSign[i]); + bodyDoubleHash[i] = Util.calculateSHA256(bodyHash[i]); + } + + return new SignTask.PaymentToSign() { + + @Override + public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) { + return signingMethod==TangemCard.SigningMethod.Sign_Hash || signingMethod==TangemCard.SigningMethod.Sign_Raw; + } + + @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!"); + for (int i = 0; i < unspentOutputs.size(); ++i) { + dataForSign[i] = bodyDoubleHash[i]; + } + return dataForSign; + } + + @Override + public byte[] getRawDataToSign() throws Exception { + ByteArrayOutputStream bs = new ByteArrayOutputStream(); + for (int i = 0; i < txForSign.length; i++) { + if (i != 0 && txForSign[0].length != txForSign[i].length) + throw new Exception("Hashes length must be identical!"); + bs.write(txForSign[i]); + } + + return bs.toByteArray(); + } + + @Override + public String getHashAlgToSign() { + return "sha-256x2"; + } + + @Override + public byte[] getIssuerTransactionSignature(byte[] dataToSignByIssuer) throws Exception { + throw new Exception("Issuer validation not supported!"); + } + + @Override + public byte[] onSignCompleted(byte[] signFromCard) throws Exception { + for (int i = 0; i < unspentOutputs.size(); ++i) { + BigInteger r = new BigInteger(1, Arrays.copyOfRange(signFromCard, i * 64, 32 + i * 64)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(signFromCard, 32 + i * 64, 64 + i * 64)); + s = CryptoUtil.toCanonicalised(s); + + unspentOutputs.get(i).scriptForBuild = DerEncodingUtil.packSignDer(r, s, pbKey); + } + + byte[] txForSend=BTCUtils.buildTXForSend(targetAddress, myAddress, unspentOutputs, amountFinal, changeFinal); + notifyOnNeedSendPayment(txForSend); + return txForSend; + } + }; + } + + @Override + public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) { + final ServerApiInsight serverApiInsight = new ServerApiInsight(); + + ServerApiInsight.InsightBodyListener insightBodyListener = new ServerApiInsight.InsightBodyListener() { + @Override + public void onSuccess(String method, InsightResponse insightResponse) { + switch (method) { + case ServerApiInsight.INSIGHT_ADDRESS: { + try { + String walletAddress = insightResponse.getAddrStr(); + if (!walletAddress.equals(coinData.getWallet())) { + // todo - check + throw new Exception("Invalid wallet address in answer!"); + } + coinData.setBalanceReceived(true); + coinData.setBalanceConfirmed(insightResponse.getBalanceSat()); + coinData.setBalanceUnconfirmed(insightResponse.getUnconfirmedBalanceSat()); + coinData.setValidationNodeDescription(ServerApiInsight.lastNode); + } + catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, "FAIL INSIGHT_ADDRESS Exception"); + } + } + break; + + case ServerApiInsight.INSIGHT_TRANSACTION: { + try { + String raw = insightResponse.getRawtx(); + String txHash = new String(BTCUtils.reverse(CryptoUtil.doubleSha256(BTCUtils.fromHex(raw)))); //TODO: check + for (BtcData.UnspentTransaction tx : coinData.getUnspentTransactions()) { + if (tx.txID.equals(txHash)) + tx.Raw = raw; + } + } catch (Exception e) { + e.printStackTrace(); + } + } + break; + } + + if (serverApiInsight.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError()); + } else { + blockchainRequestsCallbacks.onProgress(); + } + } + + public void onSuccess(String method, List utxoList) { + // case ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS: TODO: check method + try { + coinData.getUnspentTransactions().clear(); + for (InsightResponse utxo : utxoList) { + BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction(); + trUnspent.txID = utxo.getTxid(); + trUnspent.Amount = utxo.getSatoshis(); + trUnspent.Height = utxo.getHeight(); + coinData.getUnspentTransactions().add(trUnspent); + } + + for (InsightResponse utxo : utxoList) { + //if (height != -1) { TODO: check + if (blockchainRequestsCallbacks.allowAdvance()) { + serverApiInsight.insight(ServerApiInsight.INSIGHT_TRANSACTION, "", utxo.getTxid()); + } else { + ctx.setError("Terminated by user"); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + + } + + @Override + public void onFail(String method, String message) { + if (!serverApiInsight.isRequestsSequenceCompleted()) { + ctx.setError(message); + blockchainRequestsCallbacks.onComplete(false); + } + } + }; + + serverApiInsight.setInsightResponse(insightBodyListener); + + serverApiInsight.insight(ServerApiInsight.INSIGHT_ADDRESS, coinData.getWallet(), ""); + serverApiInsight.insight(ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS, coinData.getWallet(), ""); + } + +// private final static BigDecimal relayFee = new BigDecimal(0.00001); + + @Override + 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; + + final ServerApiInsight serverApiInsight = new ServerApiInsight(); + + final ServerApiInsight.InsightBodyListener insightBodyListener = new ServerApiInsight.InsightBodyListener () { + @Override + public void onSuccess(String method, InsightResponse insightResponse) { + if ( method.equals(ServerApiInsight.INSIGHT_FEE)) { + try { + BigDecimal minFee = new BigDecimal(insightResponse.getFee2()); //fee per KB + BigDecimal normalFee = new BigDecimal(insightResponse.getFee3()); + BigDecimal maxFee = new BigDecimal(insightResponse.getFee6()); + + if (minFee.equals(BigDecimal.ZERO) || normalFee.equals(BigDecimal.ZERO) || maxFee.equals(BigDecimal.ZERO)) { + serverApiInsight.insight(ServerApiInsight.INSIGHT_FEE, "",""); + } + + minFee = minFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); // (per KB -> per byte)*size + normalFee = normalFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); + maxFee = maxFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); + +// //compare fee to usual relay fee TODO: check if needed after we get access to Ducatus network +// if (fee.compareTo(relayFee) < 0) { +// fee = relayFee; +// } + minFee = minFee.setScale(8, RoundingMode.DOWN); + normalFee = normalFee.setScale(8, RoundingMode.DOWN); + maxFee = maxFee.setScale(8, RoundingMode.DOWN); + + coinData.minFee = new Amount(minFee, ctx.getBlockchain().getCurrency()); + coinData.normalFee = new Amount(normalFee, ctx.getBlockchain().getCurrency()); + coinData.maxFee = new Amount(maxFee, ctx.getBlockchain().getCurrency()); + + blockchainRequestsCallbacks.onComplete(true); + + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + @Override + public void onSuccess (String method, List utxoList) { + Log.e(TAG, "Wrong response body, InsightResponse expected"); + } + + @Override + public void onFail(String method, String message) { + if (!serverApiInsight.isRequestsSequenceCompleted()) { + ctx.setError(message); + blockchainRequestsCallbacks.onComplete(false); + } + } + }; + serverApiInsight.setInsightResponse(insightBodyListener); + + serverApiInsight.insight(ServerApiInsight.INSIGHT_FEE, "", ""); + } + + @Override + public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) { + final ServerApiInsight serverApiInsight = new ServerApiInsight(); + final String txStr = BTCUtils.toHex(txForSend); + + final ServerApiInsight.InsightBodyListener insightBodyListener = new ServerApiInsight.InsightBodyListener () { + @Override + public void onSuccess(String method, InsightResponse insightResponse) { + if (method.equals(ServerApiInsight.INSIGHT_SEND)) { + String resultString = insightResponse.toString(); + try { + if (resultString.isEmpty()) { + ctx.setError("No response from node"); + blockchainRequestsCallbacks.onComplete(false); + }else { // TODO: Make check for a valid send response + 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); + + Log.e(TAG, resultString); + } + } + } + } + + @Override + public void onSuccess (String method, List utxoList) { + Log.e(TAG, "Wrong response body, InsightResponse expected"); + } + + @Override + public void onFail(String method, String message) { + if (!serverApiInsight.isRequestsSequenceCompleted()) { + ctx.setError(message); + blockchainRequestsCallbacks.onComplete(false); + } + } + }; + serverApiInsight.setInsightResponse(insightBodyListener); + + serverApiInsight.insight(ServerApiInsight.INSIGHT_SEND, "", txStr); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/domain/wallet/eth/EthEngine.java b/app/src/main/java/com/tangem/domain/wallet/eth/EthEngine.java index 876bc8bd17..c885e8fb85 100644 --- a/app/src/main/java/com/tangem/domain/wallet/eth/EthEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/eth/EthEngine.java @@ -57,6 +57,10 @@ public class EthEngine extends CoinEngine { return 18; } + protected int getChainId() { + return ctx.getBlockchain() == Blockchain.Ethereum ? EthTransaction.ChainEnum.Mainnet.getValue() : EthTransaction.ChainEnum.Rinkeby.getValue(); + } + @Override public boolean awaitingConfirmation() { return false; @@ -391,7 +395,6 @@ public class EthEngine extends CoinEngine { BigInteger gasPrice = weiFee.divide(BigInteger.valueOf(21000)); BigInteger gasLimit = BigInteger.valueOf(21000); - Integer chainId = ctx.getBlockchain() == Blockchain.Ethereum ? EthTransaction.ChainEnum.Mainnet.getValue() : EthTransaction.ChainEnum.Rinkeby.getValue(); String to = targetAddress; @@ -399,7 +402,7 @@ public class EthEngine extends CoinEngine { to = to.substring(2); } - final EthTransaction tx = EthTransaction.create(to, weiAmount, nonceValue, gasPrice, gasLimit, chainId); + final EthTransaction tx = EthTransaction.create(to, weiAmount, nonceValue, gasPrice, gasLimit, this.getChainId()); return new SignTask.PaymentToSign() { @Override diff --git a/app/src/main/java/com/tangem/domain/wallet/ltc/LitecoinNode.kt b/app/src/main/java/com/tangem/domain/wallet/ltc/LitecoinNode.kt index 0d7ff8c172..4ce198c857 100644 --- a/app/src/main/java/com/tangem/domain/wallet/ltc/LitecoinNode.kt +++ b/app/src/main/java/com/tangem/domain/wallet/ltc/LitecoinNode.kt @@ -10,8 +10,7 @@ enum class LitecoinNode(val host: String, val port: Int, val proto: String) { N_007("electrum-ltc.bysh.me", 50002, "ssl"), N_008("e-3.claudioboxx.com", 50004, "ssl"), N_009("electrum-ltc.wilv.in", 50002, "ssl"), - N_010("ltc.rentonisk.com", 50002, "ssl"), - N_011("e-1.claudioboxx.com", 50004, "ssl"), - N_012("electrum.ltc.xurious.com", 50002, "ssl"), - N_013("ltc01.knas.systems", 50004, "ssl"), + N_010("e-1.claudioboxx.com", 50004, "ssl"), + N_011("electrum.ltc.xurious.com", 50002, "ssl"), + N_012("ltc01.knas.systems", 50004, "ssl"), } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/domain/wallet/rsk/RskEngine.java b/app/src/main/java/com/tangem/domain/wallet/rsk/RskEngine.java new file mode 100644 index 0000000000..3d381f5b5f --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/rsk/RskEngine.java @@ -0,0 +1,304 @@ +package com.tangem.domain.wallet.rsk; + +import android.net.Uri; +import android.util.Log; + +import com.tangem.data.Blockchain; +import com.tangem.data.network.ServerApiRootstock; +import com.tangem.data.network.model.InfuraResponse; +import com.tangem.domain.wallet.BTCUtils; +import com.tangem.domain.wallet.CoinEngine; +import com.tangem.domain.wallet.ECDSASignatureETH; +import com.tangem.domain.wallet.EthTransaction; +import com.tangem.domain.wallet.TangemContext; +import com.tangem.domain.wallet.eth.EthData; +import com.tangem.domain.wallet.eth.EthEngine; +import com.tangem.tangemcard.data.TangemCard; +import com.tangem.tangemcard.tasks.SignTask; +import com.tangem.util.CryptoUtil; +import com.tangem.wallet.R; + +import org.bitcoinj.core.ECKey; + +import java.math.BigInteger; +import java.util.Arrays; + +public class RskEngine extends EthEngine { + + private static final String TAG = RskEngine.class.getSimpleName(); + + public RskEngine(TangemContext ctx) throws Exception { + this.ctx = ctx; + if (ctx.getCoinData() == null) { + coinData = new EthData(); + ctx.setCoinData(coinData); + } else if (ctx.getCoinData() instanceof EthData) { + coinData = (EthData) ctx.getCoinData(); + } else { + throw new Exception("Invalid type of Blockchain data for RskEngine"); + } + } + + public RskEngine() { + super(); + } + + private static int getDecimals() { + return 18; + } + + @Override + protected int getChainId() { + return EthTransaction.ChainEnum.Rootstock_mainnet.getValue(); + } + + @Override + public String getBalanceCurrency() { + return Blockchain.Rootstock.getCurrency(); + } + + @Override + public String getFeeCurrency() { + return Blockchain.Rootstock.getCurrency(); + } + + @Override + public Uri getShareWalletUri() { return Uri.parse(ctx.getCoinData().getWallet()); } + + @Override + public Uri getShareWalletUriExplorer() { return Uri.parse("https://explorer.rsk.co/address/" + ctx.getCoinData().getWallet()); } + + @Override + public SignTask.PaymentToSign constructPayment(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) { + + Log.e(TAG, "Construct payment " + amountValue.toString() + " with fee " + feeValue.toString() + (IncFee ? " including" : " excluding")); + + BigInteger nonceValue = coinData.getConfirmedTXCount(); + byte[] pbKey = ctx.getCard().getWalletPublicKey(); + + BigInteger weiFee = convertToInternalAmount(feeValue).toBigIntegerExact(); + BigInteger weiAmount = convertToInternalAmount(amountValue).toBigIntegerExact(); + + if (IncFee) { + weiAmount = weiAmount.subtract(weiFee); + } + + BigInteger gasPrice = weiFee.divide(BigInteger.valueOf(21000)); + BigInteger gasLimit = BigInteger.valueOf(21000); + Integer chainId = EthTransaction.ChainEnum.Rootstock_mainnet.getValue(); + + String to = targetAddress; + + if (to.startsWith("0x") || to.startsWith("0X")) { + to = to.substring(2); + } + + final EthTransaction tx = EthTransaction.create(to, weiAmount, nonceValue, gasPrice, gasLimit, chainId); + + return new SignTask.PaymentToSign() { + @Override + public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) { + return signingMethod == TangemCard.SigningMethod.Sign_Hash; + } + + @Override + public byte[][] getHashesToSign() { + byte[][] hashesForSign = new byte[1][]; + hashesForSign[0] = tx.getRawHash(); + return hashesForSign; + } + + @Override + public byte[] getRawDataToSign() throws Exception { + throw new Exception("Signing of raw transaction not supported for RSK"); + } + + @Override + public String getHashAlgToSign() throws Exception { + throw new Exception("Signing of raw transaction not supported for RSK"); + } + + @Override + public byte[] getIssuerTransactionSignature(byte[] dataToSignByIssuer) throws Exception { + throw new Exception("Transaction validation by issuer not supported in this version"); + } + + @Override + public byte[] onSignCompleted(byte[] signFromCard) throws Exception { + byte[] for_hash = tx.getRawHash(); + BigInteger r = new BigInteger(1, Arrays.copyOfRange(signFromCard, 0, 32)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(signFromCard, 32, 64)); + s = CryptoUtil.toCanonicalised(s); + + boolean f = ECKey.verify(for_hash, new ECKey.ECDSASignature(r, s), pbKey); + + if (!f) { + Log.e("RSK-CHECK", "sign Failed."); + } + + tx.signature = new ECDSASignatureETH(r, s); + int v = tx.BruteRecoveryID2(tx.signature, for_hash, pbKey); + if (v != 27 && v != 28) { + Log.e(TAG, "invalid v"); + throw new Exception("Error in RskEngine - invalid v"); + } + tx.signature.v = (byte) v; + Log.e(TAG, "RSK_v: " +String.valueOf(v)); + + byte[] txForSend = tx.getEncoded(); + notifyOnNeedSendPayment(txForSend); + return txForSend; + } + }; + } + + @Override + public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) { + final ServerApiRootstock serverApiRootstock = new ServerApiRootstock(); + // request rootstock listener + ServerApiRootstock.RootstockBodyListener rootstockBodyListener = new ServerApiRootstock.RootstockBodyListener() { + @Override + public void onSuccess(String method, InfuraResponse rootstockResponse) { + switch (method) { + case ServerApiRootstock.ROOTSTOCK_ETH_GET_BALANCE: { + String balanceCap = rootstockResponse.getResult(); + balanceCap = balanceCap.substring(2); + BigInteger l = new BigInteger(balanceCap, 16); + coinData.setBalanceReceived(true); + coinData.setBalanceInInternalUnits(new CoinEngine.InternalAmount(l, "wei")); + +// Log.i("$TAG eth_get_balance", balanceCap) + } + break; + + case ServerApiRootstock.ROOTSTOCK_ETH_GET_TRANSACTION_COUNT: { + String nonce = rootstockResponse.getResult(); + nonce = nonce.substring(2); + BigInteger count = new BigInteger(nonce, 16); + coinData.setConfirmedTXCount(count); + + +// Log.i("$TAG eth_getTransCount", nonce) + } + break; + + case ServerApiRootstock.ROOTSTOCK_ETH_GET_PENDING_COUNT: { + String pending = rootstockResponse.getResult(); + pending = pending.substring(2); + BigInteger count = new BigInteger(pending, 16); + coinData.setUnconfirmedTXCount(count); + +// Log.i("$TAG eth_getPendingTxCount", pending) + } + break; + } + + if (serverApiRootstock.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError()); + } else { + blockchainRequestsCallbacks.onProgress(); + } + } + + @Override + public void onFail(String method, String message) { + if (!serverApiRootstock.isRequestsSequenceCompleted()) { + ctx.setError(message); + blockchainRequestsCallbacks.onComplete(false); + } + } + }; + serverApiRootstock.setRootstockResponse(rootstockBodyListener); + + serverApiRootstock.rootstock(ServerApiRootstock.ROOTSTOCK_ETH_GET_BALANCE, 67, coinData.getWallet(), "", ""); + serverApiRootstock.rootstock(ServerApiRootstock.ROOTSTOCK_ETH_GET_TRANSACTION_COUNT, 67, coinData.getWallet(), "", ""); + serverApiRootstock.rootstock(ServerApiRootstock.ROOTSTOCK_ETH_GET_PENDING_COUNT, 67, coinData.getWallet(), "", ""); + } + + @Override + public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) { + ServerApiRootstock serverApiRootstock = new ServerApiRootstock(); + // request rootstock gasPrice listener + ServerApiRootstock.RootstockBodyListener rootstockBodyListener = new ServerApiRootstock.RootstockBodyListener() { + @Override + public void onSuccess(String method, InfuraResponse rootstockResponse) { + String gasPrice = rootstockResponse.getResult(); + gasPrice = gasPrice.substring(2); + // rounding gas price to integer gwei + BigInteger l = new BigInteger(gasPrice, 16);//.divide(BigInteger.valueOf(1000000000L)).multiply(BigInteger.valueOf(1000000000L)); + + Log.i(TAG, "Rootstock gas price: " + gasPrice + " (" + l.toString() + ")"); + BigInteger m = BigInteger.valueOf(21000); + + Log.e(TAG, "fee multiplier: " + m.toString()); + + CoinEngine.InternalAmount weiMinFee = new CoinEngine.InternalAmount(l.multiply(m), "wei"); + CoinEngine.InternalAmount weiNormalFee = new CoinEngine.InternalAmount(l.multiply(BigInteger.valueOf(12)).divide(BigInteger.valueOf(10)).multiply(m), "wei"); + CoinEngine.InternalAmount weiMaxFee = new CoinEngine.InternalAmount(l.multiply(BigInteger.valueOf(15)).divide(BigInteger.valueOf(10)).multiply(m), "wei"); + + Log.i(TAG, "min fee : " + weiMinFee.toValueString() + " wei"); + Log.i(TAG, "normal fee: " + weiNormalFee.toValueString() + " wei"); + Log.i(TAG, "max fee : " + weiMaxFee.toValueString() + " wei"); + + coinData.minFee = convertToAmount(weiMinFee); + coinData.normalFee = convertToAmount(weiNormalFee); + coinData.maxFee = convertToAmount(weiMaxFee); + + Log.i(TAG, "min fee : " + coinData.minFee.toString()); + Log.i(TAG, "normal fee: " + coinData.normalFee.toString()); + Log.i(TAG, "max fee : " + coinData.maxFee.toString()); + + blockchainRequestsCallbacks.onComplete(true); + } + + @Override + public void onFail(String method, String message) { + ctx.setError(ctx.getContext().getString(R.string.cannot_calculate_fee_wrong_data_received_from_node)); + blockchainRequestsCallbacks.onComplete(false); + } + }; + serverApiRootstock.setRootstockResponse(rootstockBodyListener); + + serverApiRootstock.rootstock(ServerApiRootstock.ROOTSTOCK_ETH_GAS_PRICE, 67, coinData.getWallet(), "", ""); + } + + @Override + public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) { + + String txStr = String.format("0x%s", BTCUtils.toHex(txForSend)); + + ServerApiRootstock serverApiRootstock = new ServerApiRootstock(); + ServerApiRootstock.RootstockBodyListener rootstockBodyListener = new ServerApiRootstock.RootstockBodyListener() { + @Override + public void onSuccess(String method, InfuraResponse rootstockResponse) { + if (method.equals(ServerApiRootstock.ROOTSTOCK_ETH_SEND_RAW_TRANSACTION)) { + if (rootstockResponse.getResult().isEmpty()) { + ctx.setError("Rejected by node: " + rootstockResponse.getError()); + blockchainRequestsCallbacks.onComplete(false); + } else { + BigInteger nonce = coinData.getConfirmedTXCount(); + nonce=nonce.add(BigInteger.valueOf(1)); + coinData.setConfirmedTXCount(nonce); + ctx.setError(null); + blockchainRequestsCallbacks.onComplete(true); + } + } + } + + @Override + public void onFail(String method, String message) { + if (method.equals(ServerApiRootstock.ROOTSTOCK_ETH_SEND_RAW_TRANSACTION)) { + ctx.setError(message); + blockchainRequestsCallbacks.onComplete(false); + } + } + }; + + serverApiRootstock.setRootstockResponse(rootstockBodyListener); + + serverApiRootstock.rootstock(ServerApiRootstock.ROOTSTOCK_ETH_SEND_RAW_TRANSACTION, 67, coinData.getWallet(), "", txStr); + + } + +} + diff --git a/app/src/main/java/com/tangem/domain/wallet/rsk/RskTokenEngine.java b/app/src/main/java/com/tangem/domain/wallet/rsk/RskTokenEngine.java new file mode 100644 index 0000000000..f571505f4b --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/rsk/RskTokenEngine.java @@ -0,0 +1,563 @@ +package com.tangem.domain.wallet.rsk; + +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import com.google.common.base.Strings; +import com.tangem.data.Blockchain; +import com.tangem.data.network.ServerApiRootstock; +import com.tangem.data.network.model.InfuraResponse; +import com.tangem.domain.wallet.BTCUtils; +import com.tangem.domain.wallet.CoinEngine; +import com.tangem.domain.wallet.ECDSASignatureETH; +import com.tangem.domain.wallet.EthTransaction; +import com.tangem.domain.wallet.TangemContext; +import com.tangem.domain.wallet.eth.EthData; +import com.tangem.domain.wallet.token.TokenData; +import com.tangem.domain.wallet.token.TokenEngine; +import com.tangem.tangemcard.data.TangemCard; +import com.tangem.tangemcard.tasks.SignTask; +import com.tangem.util.CryptoUtil; +import com.tangem.wallet.R; + +import org.bitcoinj.core.ECKey; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.util.Arrays; + +public class RskTokenEngine extends TokenEngine { + + private static final String TAG = RskTokenEngine.class.getSimpleName(); + + public RskTokenEngine(TangemContext ctx) throws Exception { + super(ctx); + if (ctx.getCoinData() == null) { + coinData = new TokenData(); + ctx.setCoinData(coinData); + } else if (ctx.getCoinData() instanceof TokenData) { + coinData = (TokenData) ctx.getCoinData(); + } else if (ctx.getCoinData() instanceof EthData) { + // special case with receive card data substitution from server at the moment + Bundle B=new Bundle(); + ctx.getCoinData().saveToBundle(B); + coinData = new TokenData(); + coinData.loadFromBundle(B); + ctx.setCoinData(coinData); + } else { + throw new Exception("Invalid type of Blockchain data for RskTokenEngine"); + } + } + + public RskTokenEngine() { + super(); + } + + @Override + public String getBalanceCurrency() { + String currency = ctx.getCard().getTokenSymbol(); + if (Strings.isNullOrEmpty(currency)) + return "NoN"; + if (hasBalanceInfo()) { + if (coinData.getBalanceInInternalUnits().notZero()) { + return currency; + } else { + return Blockchain.Rootstock.getCurrency(); + } + } else { + return currency; + } + } + + @Override + public String getFeeCurrency() { + return Blockchain.Rootstock.getCurrency(); + } + + @Override + public Amount convertToAmount(InternalAmount internalAmount) throws Exception { + if (internalAmount.getCurrency().equals("wei")) { + BigDecimal d = internalAmount.divide(new BigDecimal("1000000000000000000"), getEthDecimals(), RoundingMode.DOWN); + return new Amount(d, Blockchain.Rootstock.getCurrency()); + } else if (internalAmount.getCurrency().equals(ctx.getCard().getTokenSymbol())) { + BigDecimal p = new BigDecimal(10); + p = p.pow(getTokenDecimals()); + BigDecimal d = internalAmount.divide(p); + return new Amount(d, ctx.getCard().getTokenSymbol()); + } + throw new Exception(String.format("Can't convert '%s' to '%s'", internalAmount.getCurrency(), ctx.getCard().getTokenSymbol())); + } + + @Override + public InternalAmount convertToInternalAmount(Amount amount) throws Exception { + if (amount.getCurrency().equals(Blockchain.Rootstock.getCurrency())) { + BigDecimal d = amount.multiply(new BigDecimal("1000000000000000000")); + return new InternalAmount(d, "wei"); + } else if (amount.getCurrency().equals(ctx.getCard().getTokenSymbol())) { + BigDecimal p = new BigDecimal(10); + p = p.pow(getTokenDecimals()); + BigDecimal d = amount.multiply(p); + return new InternalAmount(d, ctx.getCard().getTokenSymbol()); + } + throw new Exception(String.format("Can't convert '%s' to '%s'", amount.getCurrency(), ctx.getCard().getTokenSymbol())); + } + + @Override + public Uri getShareWalletUriExplorer() { return Uri.parse("https://explorer.rsk.co/address/" + ctx.getCoinData().getWallet()); } // Only RSK explorer for now + + @Override + public Uri getShareWalletUri() { return Uri.parse(ctx.getCoinData().getWallet()); } + + @Override + public boolean isExtractPossible() { + if (!hasBalanceInfo()) { + ctx.setMessage(R.string.cannot_obtain_data_from_blockchain); + } else if (!isBalanceNotZero()) { + ctx.setMessage(R.string.wallet_empty); + } else if (awaitingConfirmation()) { + ctx.setMessage(R.string.please_wait_while_previous); + } else if (!isBalanceAlterNotZero()) { + ctx.setMessage(ctx.getString(R.string.not_enough_rbtc_for_fee)); + } else { + return true; + } + return false; + } + + @Override + public boolean checkNewTransactionAmount(Amount amount) { + if (!hasBalanceInfo()) return false; + Amount balance; + try { + if (amount.getCurrency().equals(ctx.getCard().tokenSymbol)) { + balance = convertToAmount(coinData.getBalanceInInternalUnits()); + } else if (amount.getCurrency().equals(Blockchain.Rootstock.getCurrency()) && coinData.getBalanceInInternalUnits().isZero()) { + balance = convertToAmount(coinData.getBalanceAlterInInternalUnits()); + } else { + return false; + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } + return amount.compareTo(balance) <= 0; + } + + @Override + public boolean checkNewTransactionAmountAndFee(Amount amount, Amount fee, Boolean isFeeIncluded) { + if (!hasBalanceInfo()) return false; + + try { + Amount balanceRBTC = convertToAmount(coinData.getBalanceAlterInInternalUnits()); + + if (fee == null || amount == null || fee.isZero() || amount.isZero()) + return false; + + + if (amount.getCurrency().equals(ctx.getCard().tokenSymbol)) { + // token transaction + if (fee.compareTo(balanceRBTC) > 0) + return false; + } else if (amount.getCurrency().equals(Blockchain.Rootstock.getCurrency()) && coinData.getBalanceInInternalUnits().isZero()) { + // standard RBTC transaction +// try { + if (isFeeIncluded && (amount.compareTo(balanceRBTC) > 0 || fee.compareTo(balanceRBTC) > 0)) + return false; + + if (!isFeeIncluded && amount.add(fee).compareTo(balanceRBTC) > 0) + return false; + +// } catch (NumberFormatException e) { +// e.printStackTrace(); +// } + } else + + { + return false; + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + return true; + } + + @Override + public SignTask.PaymentToSign constructPayment(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception { + if (amountValue.getCurrency().equals(Blockchain.Rootstock.getCurrency())) { + return constructPaymentRBTC(feeValue, amountValue, IncFee, targetAddress); + } else { + return constructPaymentToken(feeValue, amountValue, IncFee, targetAddress); + } + } + + private SignTask.PaymentToSign constructPaymentRBTC(Amount feeValue, Amount amountValue, boolean IncFee, String targetAddress) throws Exception { + Log.e(TAG, "Construct RBTC payment "+amountValue.toString()+" with fee "+feeValue.toString()+(IncFee?" including":" excluding")); + + BigInteger nonceValue = coinData.getConfirmedTXCount(); + byte[] pbKey = ctx.getCard().getWalletPublicKey(); + + BigInteger weiFee = convertToInternalAmount(feeValue).toBigIntegerExact(); + BigInteger weiAmount = convertToInternalAmount(amountValue).toBigIntegerExact(); + + if (IncFee) { + weiAmount = weiAmount.subtract(weiFee); + } + + BigInteger gasPrice = weiFee.divide(BigInteger.valueOf(21000)); + BigInteger gasLimit = BigInteger.valueOf(21000); + Integer chainId = EthTransaction.ChainEnum.Rootstock_mainnet.getValue(); + + String to = targetAddress; + + if (to.startsWith("0x") || to.startsWith("0X")) { + to = to.substring(2); + } + + final EthTransaction tx = EthTransaction.create(to, weiAmount, nonceValue, gasPrice, gasLimit, chainId); + + return new SignTask.PaymentToSign() { + @Override + public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) { + return signingMethod == TangemCard.SigningMethod.Sign_Hash; + } + + @Override + public byte[][] getHashesToSign() { + byte[][] hashesForSign = new byte[1][]; + hashesForSign[0] = tx.getRawHash(); + return hashesForSign; + } + + @Override + public byte[] getRawDataToSign() throws Exception { + throw new Exception("Signing of raw transaction not supported for RSK"); + } + + @Override + public String getHashAlgToSign() throws Exception { + throw new Exception("Signing of raw transaction not supported for RSK"); + } + + @Override + public byte[] getIssuerTransactionSignature(byte[] dataToSignByIssuer) throws Exception { + throw new Exception("Transaction validation by issuer not supported in this version"); + } + + @Override + public byte[] onSignCompleted(byte[] signFromCard) throws Exception { + byte[] for_hash = tx.getRawHash(); + BigInteger r = new BigInteger(1, Arrays.copyOfRange(signFromCard, 0, 32)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(signFromCard, 32, 64)); + s = CryptoUtil.toCanonicalised(s); + + boolean f = ECKey.verify(for_hash, new ECKey.ECDSASignature(r, s), pbKey); + + if (!f) { + Log.e("RSK-CHECK", "sign Failed."); + } + + tx.signature = new ECDSASignatureETH(r, s); + int v = tx.BruteRecoveryID2(tx.signature, for_hash, pbKey); + if (v != 27 && v != 28) { + Log.e(TAG, "invalid v"); + throw new Exception("Error in RskTokenEngine - invalid v"); + } + tx.signature.v = (byte) v; + Log.e(TAG,"RSK_v "+ String.valueOf(v)); + + byte[] txForSend = tx.getEncoded(); + notifyOnNeedSendPayment(txForSend); + return txForSend; + } + }; + } + + private SignTask.PaymentToSign constructPaymentToken(Amount feeValue, Amount amountValue, boolean IncFee, String targetAddress) throws Exception { + Log.e(TAG, "Construct TOKEN payment "+amountValue.toString()+" with fee "+feeValue.toString()+(IncFee?" including":" excluding")); + + BigInteger nonceValue = coinData.getConfirmedTXCount(); + byte[] pbKey = ctx.getCard().getWalletPublicKey(); +// boolean flag = (ctx.getCard().getSigningMethod() == TangemCard.SigningMethod.Sign_Hash_Validated_By_Issuer); +// Issuer issuer = ctx.getCard().getIssuer(); + + +// BigInteger gigaK = BigInteger.valueOf(1000000000L); + + BigInteger weiFee = convertToInternalAmount(feeValue).toBigIntegerExact(); + + InternalAmount amountDec = convertToInternalAmount(amountValue); + BigInteger amount = amountDec.toBigInteger(); //new BigInteger(amountValue, 10); + + + //amount = amount.subtract(fee); + + BigInteger gasPrice = weiFee.divide(BigInteger.valueOf(60000)); + BigInteger gasLimit = BigInteger.valueOf(60000); + Integer chainId = EthTransaction.ChainEnum.Rootstock_mainnet.getValue(); + BigInteger amountZero = BigInteger.ZERO; + + String to = targetAddress; + + if (to.startsWith("0x") || to.startsWith("0X")) { + to = to.substring(2); + } + + String contractAddress = getContractAddress(ctx.getCard()); + + if (contractAddress.startsWith("0x") || contractAddress.startsWith("0X")) { + contractAddress = contractAddress.substring(2); + } + + String amountLeadZero = amount.toString(16); + if (amountLeadZero.startsWith("0x") || amountLeadZero.startsWith("0X")) { + amountLeadZero = amountLeadZero.substring(2); + } + + while (amountLeadZero.length() < 64) { + amountLeadZero = "0" + amountLeadZero; + } + + String cmd = "a9059cbb000000000000000000000000" + to + amountLeadZero; //TODO only for BAT + + + byte[] data = BTCUtils.fromHex(cmd); + EthTransaction tx = EthTransaction.create(contractAddress, amountZero, nonceValue, gasPrice, gasLimit, chainId, data); + + return new SignTask.PaymentToSign() { + @Override + public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) { + return signingMethod == TangemCard.SigningMethod.Sign_Hash; + } + + @Override + public byte[][] getHashesToSign() { + byte[][] hashesForSign = new byte[1][]; + hashesForSign[0] = tx.getRawHash(); + return hashesForSign; + } + + @Override + public byte[] getRawDataToSign() throws Exception { + throw new Exception("Signing of raw transaction not supported for RSK"); + } + + @Override + public String getHashAlgToSign() throws Exception { + throw new Exception("Signing of raw transaction not supported for RSK"); + } + + @Override + public byte[] getIssuerTransactionSignature(byte[] dataToSignByIssuer) throws Exception { + throw new Exception("Transaction validation by issuer not supported in this version"); + } + + @Override + public byte[] onSignCompleted(byte[] signFromCard) throws Exception { + byte[] for_hash = tx.getRawHash(); + BigInteger r = new BigInteger(1, Arrays.copyOfRange(signFromCard, 0, 32)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(signFromCard, 32, 64)); + s = CryptoUtil.toCanonicalised(s); + + boolean f = ECKey.verify(for_hash, new ECKey.ECDSASignature(r, s), pbKey); + + if (!f) { + Log.e("RSK-CHECK", "sign Failed."); + } + + tx.signature = new ECDSASignatureETH(r, s); + int v = tx.BruteRecoveryID2(tx.signature, for_hash, pbKey); + if (v != 27 && v != 28) { + Log.e(TAG, "invalid v"); + throw new Exception("Error in RskTokenEngine - invalid v"); + } + tx.signature.v = (byte) v; + Log.e(TAG,"RSK_v: "+ String.valueOf(v)); + + byte[] txForSend = tx.getEncoded(); + notifyOnNeedSendPayment(txForSend); + return txForSend; + + } + }; + + } + + @Override + public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) { + final ServerApiRootstock serverApiRootstock = new ServerApiRootstock(); + // request rootstock listener + ServerApiRootstock.RootstockBodyListener rootstockBodyListener = new ServerApiRootstock.RootstockBodyListener() { + @Override + public void onSuccess(String method, InfuraResponse rootstockResponse) { + switch (method) { + case ServerApiRootstock.ROOTSTOCK_ETH_GET_BALANCE: { + String balanceCap = rootstockResponse.getResult(); + balanceCap = balanceCap.substring(2); + BigInteger l = new BigInteger(balanceCap, 16); + coinData.setBalanceReceived(true); + coinData.setBalanceAlterInInternalUnits(new CoinEngine.InternalAmount(l, "wei")); + +// Log.i("$TAG eth_get_balance", balanceCap) + } + break; + + case ServerApiRootstock.ROOTSTOCK_ETH_GET_TRANSACTION_COUNT: { + String nonce = rootstockResponse.getResult(); + nonce = nonce.substring(2); + BigInteger count = new BigInteger(nonce, 16); + coinData.setConfirmedTXCount(count); + + +// Log.i("$TAG eth_getTransCount", nonce) + } + break; + + case ServerApiRootstock.ROOTSTOCK_ETH_GET_PENDING_COUNT: { + String pending = rootstockResponse.getResult(); + pending = pending.substring(2); + BigInteger count = new BigInteger(pending, 16); + coinData.setUnconfirmedTXCount(count); + +// Log.i("$TAG eth_getPendingTxCount", pending) + } + break; +// + case ServerApiRootstock.ROOTSTOCK_ETH_CALL: { + try { + String balanceCap = rootstockResponse.getResult(); + balanceCap = balanceCap.substring(2); + BigInteger l = new BigInteger(balanceCap, 16); + coinData.setBalanceInInternalUnits(new CoinEngine.InternalAmount(l, ctx.getCard().tokenSymbol)); + +// Log.i("$TAG eth_call", balanceCap) + + if (blockchainRequestsCallbacks.allowAdvance()) { + serverApiRootstock.rootstock(ServerApiRootstock.ROOTSTOCK_ETH_GET_BALANCE, 67, coinData.getWallet(), "", ""); + serverApiRootstock.rootstock(ServerApiRootstock.ROOTSTOCK_ETH_GET_TRANSACTION_COUNT, 67, coinData.getWallet(), "", ""); + serverApiRootstock.rootstock(ServerApiRootstock.ROOTSTOCK_ETH_GET_PENDING_COUNT, 67, coinData.getWallet(), "", ""); + } else { + ctx.setError("Terminated by user"); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + break; + + } + if (serverApiRootstock.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError()); + } else { + blockchainRequestsCallbacks.onProgress(); + } + } + + @Override + public void onFail(String method, String message) { + if (!serverApiRootstock.isRequestsSequenceCompleted()) { + ctx.setError(message); + blockchainRequestsCallbacks.onComplete(false); + } + } + }; + serverApiRootstock.setRootstockResponse(rootstockBodyListener); + + serverApiRootstock.rootstock(ServerApiRootstock.ROOTSTOCK_ETH_CALL, 67, coinData.getWallet(), getContractAddress(ctx.getCard()), ""); + } + + @Override + public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) { + ServerApiRootstock serverApiRootstock = new ServerApiRootstock(); + // request rootstock gasPrice listener + ServerApiRootstock.RootstockBodyListener rootstockBodyListener = new ServerApiRootstock.RootstockBodyListener() { + @Override + public void onSuccess(String method, InfuraResponse rootstockResponse) { + String gasPrice = rootstockResponse.getResult(); + gasPrice = gasPrice.substring(2); + + // rounding gas price to integer gwei + BigInteger l = new BigInteger(gasPrice, 16); + + Log.i(TAG, "Rootstock gas price: "+gasPrice+" ("+l.toString()+")"); + BigInteger m; + if (!amount.getCurrency().equals(Blockchain.Rootstock.getCurrency())) m = BigInteger.valueOf(60000); + else m = BigInteger.valueOf(21000); + + Log.i(TAG, "fee multiplier: "+m.toString()); + + CoinEngine.InternalAmount weiMinFee = new CoinEngine.InternalAmount(l.multiply(m), "wei"); + CoinEngine.InternalAmount weiNormalFee = new CoinEngine.InternalAmount(l.multiply(BigInteger.valueOf(12)).divide(BigInteger.valueOf(10)).multiply(m), "wei"); + CoinEngine.InternalAmount weiMaxFee = new CoinEngine.InternalAmount(l.multiply(BigInteger.valueOf(15)).divide(BigInteger.valueOf(10)).multiply(m), "wei"); + Log.i(TAG, "min fee : "+weiMinFee.toValueString()+" wei"); + Log.i(TAG, "normal fee: "+weiNormalFee.toValueString()+" wei"); + Log.i(TAG, "max fee : "+weiMaxFee.toValueString()+" wei"); + + try { + coinData.minFee = convertToAmount(weiMinFee); + coinData.normalFee = convertToAmount(weiNormalFee); + coinData.maxFee = convertToAmount(weiMaxFee); + Log.i(TAG, "min fee : "+coinData.minFee.toString()); + Log.i(TAG, "normal fee: "+coinData.normalFee.toString()); + Log.i(TAG, "max fee : "+coinData.maxFee.toString()); + } catch (Exception e) { + e.printStackTrace(); + } + blockchainRequestsCallbacks.onComplete(true); + } + + @Override + public void onFail(String method, String message) { + ctx.setError(message); + blockchainRequestsCallbacks.onComplete(false); + } + }; + serverApiRootstock.setRootstockResponse(rootstockBodyListener); + + serverApiRootstock.rootstock(ServerApiRootstock.ROOTSTOCK_ETH_GAS_PRICE, 67, coinData.getWallet(), "", ""); + } + + @Override + public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) { + + String txStr = String.format("0x%s", BTCUtils.toHex(txForSend)); + + ServerApiRootstock serverApiRootstock = new ServerApiRootstock(); + // request rootstock listener + ServerApiRootstock.RootstockBodyListener rootstockBodyListener = new ServerApiRootstock.RootstockBodyListener() { + @Override + public void onSuccess(String method, InfuraResponse infuraResponse) { + if (method.equals(ServerApiRootstock.ROOTSTOCK_ETH_SEND_RAW_TRANSACTION)) { + if (infuraResponse.getResult().isEmpty()) { + ctx.setError("Rejected by node: " + infuraResponse.getError()); + blockchainRequestsCallbacks.onComplete(false); + } else { + BigInteger nonce = coinData.getConfirmedTXCount(); + nonce=nonce.add(BigInteger.valueOf(1)); + coinData.setConfirmedTXCount(nonce); + ctx.setError(null); + blockchainRequestsCallbacks.onComplete(true); + } + } + } + + @Override + public void onFail(String method, String message) { + if (method.equals(ServerApiRootstock.ROOTSTOCK_ETH_SEND_RAW_TRANSACTION)) { + ctx.setError(message); + blockchainRequestsCallbacks.onComplete(false); + } + } + }; + + serverApiRootstock.setRootstockResponse(rootstockBodyListener); + + serverApiRootstock.rootstock(ServerApiRootstock.ROOTSTOCK_ETH_SEND_RAW_TRANSACTION, 67, coinData.getWallet(), "", txStr); + + } + +} diff --git a/app/src/main/java/com/tangem/domain/wallet/token/TokenEngine.java b/app/src/main/java/com/tangem/domain/wallet/token/TokenEngine.java index 23bdd7d2e5..dd6e5fb76f 100644 --- a/app/src/main/java/com/tangem/domain/wallet/token/TokenEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/token/TokenEngine.java @@ -90,7 +90,7 @@ public class TokenEngine extends CoinEngine { public String getBalanceHTML() { if (hasBalanceInfo()) { try { - return " " + convertToAmount(coinData.getBalanceInInternalUnits()).toDescriptionString(getTokenDecimals()) + "
+ " + convertToAmount(coinData.getBalanceAlterInInternalUnits()).toDescriptionString(getEthDecimals()) + " for gas"; + return " " + convertToAmount(coinData.getBalanceInInternalUnits()).toDescriptionString(getTokenDecimals()) + "
+ " + convertToAmount(coinData.getBalanceAlterInInternalUnits()).toDescriptionString(getEthDecimals()) + " for fee"; } catch (Exception e) { e.printStackTrace(); return ""; @@ -136,15 +136,15 @@ public class TokenEngine extends CoinEngine { return ctx.getString(R.string.not_implemented); } - private static int getEthDecimals() { + protected static int getEthDecimals() { return 18; } - private int getTokenDecimals() { + protected int getTokenDecimals() { return ctx.getCard().getTokensDecimal(); } - private String getContractAddress(TangemCard card) { + protected String getContractAddress(TangemCard card) { return card.getContractAddress(); } @@ -169,7 +169,7 @@ public class TokenEngine extends CoinEngine { return true; } - private boolean isBalanceAlterNotZero() { + protected boolean isBalanceAlterNotZero() { if (coinData == null) return false; if (coinData.getBalanceAlterInInternalUnits() == null) return false; return coinData.getBalanceAlterInInternalUnits().notZero(); @@ -311,7 +311,7 @@ public class TokenEngine extends CoinEngine { } else if (awaitingConfirmation()) { ctx.setMessage(R.string.please_wait_while_previous); } else if (!isBalanceAlterNotZero()) { - ctx.setMessage(ctx.getString(R.string.not_enough_eth_for_gas)); + ctx.setMessage(ctx.getString(R.string.not_enough_eth_for_fee)); } else { return true; } diff --git a/app/src/main/java/com/tangem/presentation/activity/PreparePaymentActivity.kt b/app/src/main/java/com/tangem/presentation/activity/PreparePaymentActivity.kt index fa94fbac01..2bb08c71c5 100644 --- a/app/src/main/java/com/tangem/presentation/activity/PreparePaymentActivity.kt +++ b/app/src/main/java/com/tangem/presentation/activity/PreparePaymentActivity.kt @@ -5,6 +5,7 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.nfc.NfcAdapter +import android.nfc.NfcManager import android.nfc.Tag import android.os.Bundle import android.support.v7.app.AppCompatActivity @@ -25,6 +26,7 @@ import java.io.IOException import javax.inject.Inject class PreparePaymentActivity : AppCompatActivity(), NfcAdapter.ReaderCallback { + companion object { val TAG: String = PreparePaymentActivity::class.java.simpleName fun callingIntent(context: Context, ctx: TangemContext): Intent { @@ -34,6 +36,7 @@ class PreparePaymentActivity : AppCompatActivity(), NfcAdapter.ReaderCallback { } } + @Inject internal lateinit var navigator: Navigator @@ -58,7 +61,8 @@ class PreparePaymentActivity : AppCompatActivity(), NfcAdapter.ReaderCallback { tvBalance.text = html //TODO - to engine - if (ctx.blockchain == Blockchain.Token && engine.balance.currency != Blockchain.Ethereum.currency) { + if ((ctx.blockchain == Blockchain.Token && engine.balance.currency!=Blockchain.Ethereum.currency) || + ((ctx.blockchain == Blockchain.RootstockToken && engine.balance.currency!=Blockchain.Rootstock.currency))){ rgIncFee!!.visibility = View.INVISIBLE } else { rgIncFee!!.visibility = View.VISIBLE 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 882a85aeee..b8fd021fe8 100644 --- a/app/src/main/java/com/tangem/presentation/fragment/LoadedWallet.kt +++ b/app/src/main/java/com/tangem/presentation/fragment/LoadedWallet.kt @@ -107,7 +107,7 @@ class LoadedWallet : Fragment(), NfcAdapter.ReaderCallback, CardProtocol.Notific override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - if (ctx.blockchain == Blockchain.Token) + if (ctx.blockchain == Blockchain.Token || ctx.blockchain == Blockchain.RootstockToken) tvBalance.setSingleLine(false) ivTangemCard.setImageBitmap(App.localStorage.getCardArtworkBitmap(ctx.card)) @@ -706,6 +706,8 @@ class LoadedWallet : Fragment(), NfcAdapter.ReaderCallback, CardProtocol.Notific Blockchain.Token -> "ethereum" Blockchain.BitcoinCash -> "bitcoin-cash" Blockchain.Litecoin -> "litecoin" + Blockchain.Rootstock -> "bitcoin" + Blockchain.RootstockToken ->"bitcoin" else -> { throw Exception("Can''t get rate for blockchain " + ctx.blockchainName) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8366bb1c7d..f50f0e9f20 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -189,7 +189,8 @@ Cannot calculate fee! Wrong data received from the node The wallet is empty Please wait for confirmation of incoming transaction - Not enough ETH funds for gas + Not enough ETH funds for fee + Not enough RBTC funds for fee PIN2 is required to sign the payment Not enough funds Service unavailable diff --git a/tangemcard-android/src/main/res/drawable/card_ru031.png b/tangemcard-android/src/main/res/drawable/card_ru031.png new file mode 100644 index 0000000000..8533af69ab Binary files /dev/null and b/tangemcard-android/src/main/res/drawable/card_ru031.png differ diff --git a/tangemcard-android/src/main/res/drawable/card_ru032.png b/tangemcard-android/src/main/res/drawable/card_ru032.png new file mode 100644 index 0000000000..9e5d5949e4 Binary files /dev/null and b/tangemcard-android/src/main/res/drawable/card_ru032.png differ diff --git a/tangemserver-android/src/main/java/com/tangem/tangemserver/android/data/LocalStorage.kt b/tangemserver-android/src/main/java/com/tangem/tangemserver/android/data/LocalStorage.kt index 426ac14e0e..c437f13f27 100644 --- a/tangemserver-android/src/main/java/com/tangem/tangemserver/android/data/LocalStorage.kt +++ b/tangemserver-android/src/main/java/com/tangem/tangemserver/android/data/LocalStorage.kt @@ -67,6 +67,8 @@ class LocalStorage putResourceArtworkToCatalog(R.drawable.card_ru028, true) putResourceArtworkToCatalog(R.drawable.card_ru029, true) putResourceArtworkToCatalog(R.drawable.card_ru030, true) + putResourceArtworkToCatalog(R.drawable.card_ru031, true) + putResourceArtworkToCatalog(R.drawable.card_ru032, true) } if (batchesFile.exists()) { try { @@ -226,6 +228,8 @@ class LocalStorage card.batch == "001F" -> R.drawable.card_ru028 card.batch == "0018" -> R.drawable.card_ru029 card.batch == "0020" -> R.drawable.card_ru030 + card.batch == "0021" -> R.drawable.card_ru031 + card.batch == "0022" -> R.drawable.card_ru032 else -> null }