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); + + } + +} +