diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java
index 5ad40fedc9..cf900eb9c4 100644
--- a/app/src/main/java/com/tangem/data/Blockchain.java
+++ b/app/src/main/java/com/tangem/data/Blockchain.java
@@ -13,7 +13,8 @@ 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");
Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) {
mID = ID;
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 b6f74767ef..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;
}
}
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..5b3cbdcc9c
--- /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++;
+ InfuraApi infuraApi = App.getNetworkComponent().getRetrofitInfura().create(InfuraApi.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 = infuraApi.infura(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/domain/wallet/CoinEngineFactory.kt b/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.kt
index 951b7eb457..7bba67ec11 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,7 @@ 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
/**
* Factory for create specific engine
@@ -44,6 +45,8 @@ object CoinEngineFactory {
TokenEngine(context)
else if (Blockchain.Litecoin == context.blockchain)
LtcEngine(context)
+ else if (Blockchain.Rootstock == context.blockchain)
+ RskEngine(context)
else
return null
} catch (e: Exception) {
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..e2c1c83e17
--- /dev/null
+++ b/app/src/main/java/com/tangem/domain/wallet/rsk/RskEngine.java
@@ -0,0 +1,295 @@
+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 {
+ super(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");
+ }
+ }
+
+ private static int getDecimals() {
+ return 18;
+ }
+
+ @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 infura 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 infura eth 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);
+
+ }
+
+}
+