diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000000..a55e7a179b --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index 8d1bafa737..460111e07a 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -20,7 +20,9 @@ public enum Blockchain { Cardano("CARDANO", "ADA", 1000000.0, R.drawable.tangem2, "Cardano"), Ripple ("XRP", "XRP", 1000000.0, R.drawable.tangem2, "XRP"), Binance("BINANCE", "BNB", 100000000.0, R.drawable.tangem2, "Binance"), - BinanceTestNet("BINANCE/test", "BNB", 100000000.0, R.drawable.tangem2, "Binance Testnet"); + BinanceTestNet("BINANCE/test", "BNB", 100000000.0, R.drawable.tangem2, "Binance Testnet"), + Matic("MATIC", "MTX", 1.0, R.drawable.tangem2, "Matic"), + MaticTestNet("MATIC/test", "MTX", 1.0, R.drawable.tangem2, "Matic Testnet"); Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) { mID = ID; diff --git a/app/src/main/java/com/tangem/data/network/MaticApi.java b/app/src/main/java/com/tangem/data/network/MaticApi.java new file mode 100644 index 0000000000..de29ef8178 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/MaticApi.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 MaticApi { + @Headers("Content-Type: application/json") + @POST(Server.ApiMaticTesnet.Method.MAIN) + Call matic(@Body InfuraBody body); +} \ No newline at end of file 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 eb9119b2ad..af6af12579 100644 --- a/app/src/main/java/com/tangem/data/network/Server.java +++ b/app/src/main/java/com/tangem/data/network/Server.java @@ -43,6 +43,17 @@ public class Server { } } + /** + * https://testnet2.matic.network + */ + public static class ApiMaticTesnet { + public static final String URL_MATIC_TESTNET = ServerURL.API_MATIC_TESTNET ; + + public static class Method { + static final String MAIN = URL_MATIC_TESTNET; + } + } + /** * https://estimatefee.com/ */ diff --git a/app/src/main/java/com/tangem/data/network/ServerApiMatic.java b/app/src/main/java/com/tangem/data/network/ServerApiMatic.java new file mode 100644 index 0000000000..8686efa7ef --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/ServerApiMatic.java @@ -0,0 +1,105 @@ +package com.tangem.data.network; + +import androidx.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 ServerApiMatic { + private static String TAG = ServerApiMatic.class.getSimpleName(); + + /** + * HTTP + * Infura + *

+ * eth_getBalance + * eth_getTransactionCount + * eth_call + * eth_sendRawTransaction + * eth_gasPrice + */ + public static final String MATIC_ETH_GET_BALANCE = "eth_getBalance"; + public static final String MATIC_ETH_GET_TRANSACTION_COUNT = "eth_getTransactionCount"; + public static final String MATIC_ETH_GET_PENDING_COUNT = "eth_getPendingCount"; + public static final String MATIC_ETH_CALL = "eth_call"; + public static final String MATIC_ETH_SEND_RAW_TRANSACTION = "eth_sendRawTransaction"; + public static final String MATIC_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 ResponseListener responseListener; + + public interface ResponseListener { + void onSuccess(String method, InfuraResponse infuraResponse); + + void onFail(String method, String message); + } + + public void setResponseListener(ResponseListener listener) { + responseListener = listener; + } + + public void requestData(String method, int id, String wallet, String contract, String tx) { + requestsCount++; + MaticApi maticApi = App.Companion.getNetworkComponent().getRetrofitMaticTesnet().create(MaticApi.class); + + InfuraBody infuraBody; + switch (method) { + case MATIC_ETH_GET_BALANCE: + case MATIC_ETH_GET_TRANSACTION_COUNT: + infuraBody = new InfuraBody(method, new String[]{wallet, "latest"}, id); + break; + case MATIC_ETH_GET_PENDING_COUNT: + infuraBody = new InfuraBody(MATIC_ETH_GET_TRANSACTION_COUNT, new String[]{wallet, "pending"}, id); + break; + case MATIC_ETH_CALL: + String address = wallet.substring(2); + infuraBody = new InfuraBody(method, new Object[]{new InfuraBody.EthCallParams("0x70a08231000000000000000000000000" + address, contract), "latest"}, id); + break; + + case MATIC_ETH_SEND_RAW_TRANSACTION: + infuraBody = new InfuraBody(method, new String[]{tx}, id); + break; + + case MATIC_ETH_GAS_PRICE: + infuraBody = new InfuraBody(method, id); + break; + + default: + infuraBody = new InfuraBody(); + } + + Call call = maticApi.matic(infuraBody); + call.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.code() == 200) { + requestsCount--; + responseListener.onSuccess(method, response.body()); + Log.i(TAG, "requestData " + method + " onResponse " + response.code()); + } else { + responseListener.onFail(method, String.valueOf(response.code())); + Log.e(TAG, "requestData " + method + " onResponse " + response.code()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + responseListener.onFail(method, String.valueOf(t.getMessage())); + Log.e(TAG, "requestData " + 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 0d38feb2c0..bf8d47fbda 100644 --- a/app/src/main/java/com/tangem/data/network/ServerURL.java +++ b/app/src/main/java/com/tangem/data/network/ServerURL.java @@ -10,4 +10,5 @@ class ServerURL { static final String API_BLOCKCYPHER = "https://api.blockcypher.com/"; static final String API_BINANCE = "https://dex.binance.org/"; static final String API_BINANCE_TESTNET = "https://testnet-dex.binance.org/"; + static final String API_MATIC_TESTNET = "https://testnet2.matic.network"; } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/model/InfuraBody.java b/app/src/main/java/com/tangem/data/network/model/InfuraBody.java index bf3aa67f29..7f104e419e 100644 --- a/app/src/main/java/com/tangem/data/network/model/InfuraBody.java +++ b/app/src/main/java/com/tangem/data/network/model/InfuraBody.java @@ -4,6 +4,7 @@ public class InfuraBody { private String method; private Object[] params; private int id; + private String jsonrpc = "2.0"; public InfuraBody() { } diff --git a/app/src/main/java/com/tangem/di/NetworkComponent.kt b/app/src/main/java/com/tangem/di/NetworkComponent.kt index 90074caed1..cc5a3ef060 100644 --- a/app/src/main/java/com/tangem/di/NetworkComponent.kt +++ b/app/src/main/java/com/tangem/di/NetworkComponent.kt @@ -17,6 +17,9 @@ interface NetworkComponent { @get:Named(Server.ApiInfura.URL_INFURA) val retrofitInfura: Retrofit + @get:Named(Server.ApiMaticTesnet.URL_MATIC_TESTNET) + val retrofitMaticTesnet: Retrofit + @get:Named(Server.ApiEstimatefee.URL_ESTIMATEFEE) val retrofitEstimatefee: Retrofit diff --git a/app/src/main/java/com/tangem/di/NetworkModule.kt b/app/src/main/java/com/tangem/di/NetworkModule.kt index 75a253c676..912ec80d24 100644 --- a/app/src/main/java/com/tangem/di/NetworkModule.kt +++ b/app/src/main/java/com/tangem/di/NetworkModule.kt @@ -46,6 +46,18 @@ internal class NetworkModule { return builder.build() } + @Singleton + @Provides + @Named(Server.ApiMaticTesnet.URL_MATIC_TESTNET) + fun provideRetrofitMaticTestnet(): Retrofit { + val builder = Retrofit.Builder() + .baseUrl(Server.ApiMaticTesnet.URL_MATIC_TESTNET) + .addConverterFactory(GsonConverterFactory.create()) + if (BuildConfig.DEBUG) + builder.client(createOkHttpClient()) + return builder.build() + } + @Singleton @Provides @Named(Server.ApiEstimatefee.URL_ESTIMATEFEE) diff --git a/app/src/main/java/com/tangem/ui/fragment/LoadedWalletViewModel.kt b/app/src/main/java/com/tangem/ui/fragment/LoadedWalletViewModel.kt index 0c0ed03153..70de761bbf 100644 --- a/app/src/main/java/com/tangem/ui/fragment/LoadedWalletViewModel.kt +++ b/app/src/main/java/com/tangem/ui/fragment/LoadedWalletViewModel.kt @@ -38,6 +38,8 @@ class LoadedWalletViewModel : ViewModel() { Blockchain.Ripple -> "ripple" Blockchain.Binance -> "binance-coin" Blockchain.BinanceTestNet -> "binance-coin" + Blockchain.Matic -> "bitcoin" + Blockchain.MaticTestNet -> "bitcoin" else -> { throw Exception("Can''t get rate for blockchain " + ctx.blockchainName) } diff --git a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt index c5ec5b7121..030e8ea07f 100644 --- a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt +++ b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt @@ -11,6 +11,7 @@ import com.tangem.wallet.binance.BinanceEngine import com.tangem.wallet.cardano.CardanoData import com.tangem.wallet.cardano.CardanoEngine import com.tangem.wallet.ltc.LtcEngine +import com.tangem.wallet.matic.MaticTokenEngine import com.tangem.wallet.nftToken.NftTokenEngine import com.tangem.wallet.rsk.RskEngine import com.tangem.wallet.rsk.RskTokenEngine @@ -41,6 +42,7 @@ object CoinEngineFactory { Blockchain.Cardano -> CardanoEngine() Blockchain.Ripple -> XrpEngine() Blockchain.Binance, Blockchain.BinanceTestNet -> BinanceEngine() + Blockchain.Matic, Blockchain.MaticTestNet -> MaticTokenEngine() else -> null } } @@ -70,6 +72,10 @@ object CoinEngineFactory { XrpEngine(context) else if (Blockchain.Binance == context.blockchain || Blockchain.BinanceTestNet == context.blockchain) BinanceEngine(context) + else if (Blockchain.Matic == context.blockchain || Blockchain.MaticTestNet == context.blockchain) + MaticTokenEngine(context + + ) else return null } catch (e: Exception) { diff --git a/app/src/main/java/com/tangem/wallet/EthTransaction.java b/app/src/main/java/com/tangem/wallet/EthTransaction.java index dd91845fb2..c7ba6e98b3 100644 --- a/app/src/main/java/com/tangem/wallet/EthTransaction.java +++ b/app/src/main/java/com/tangem/wallet/EthTransaction.java @@ -82,7 +82,8 @@ public class EthTransaction { Kovan(42), Ethereum_Classic_mainnet(61), Ethereum_Classic_testnet(62), - Geth_private_chains(1337); + Geth_private_chains(1337), + Matic_Testnet(8995); private int value; diff --git a/app/src/main/java/com/tangem/wallet/matic/MaticTokenEngine.java b/app/src/main/java/com/tangem/wallet/matic/MaticTokenEngine.java new file mode 100644 index 0000000000..dec40a7171 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/matic/MaticTokenEngine.java @@ -0,0 +1,241 @@ +package com.tangem.wallet.matic; + +import android.net.Uri; +import android.util.Log; + +import com.tangem.data.Blockchain; +import com.tangem.data.network.ServerApiMatic; +import com.tangem.data.network.model.InfuraResponse; +import com.tangem.wallet.BTCUtils; +import com.tangem.wallet.CoinEngine; +import com.tangem.wallet.EthTransaction; +import com.tangem.wallet.R; +import com.tangem.wallet.TangemContext; +import com.tangem.wallet.token.TokenEngine; + +import java.math.BigInteger; + +public class MaticTokenEngine extends TokenEngine { + + private static final String TAG = MaticTokenEngine.class.getSimpleName(); + + public MaticTokenEngine(TangemContext ctx) throws Exception { + super(ctx); + } + + public MaticTokenEngine() { + super(); + } + + @Override + public Blockchain getBlockchain() { + return ctx.getBlockchain(); + } + + @Override + protected int getChainIdNum() { + return EthTransaction.ChainEnum.Matic_Testnet.getValue(); + } + + @Override + public String getBalanceHTML() { + if (hasBalanceInfo()) { + try { + return " " + convertToAmount(coinData.getBalanceInInternalUnits()).toDescriptionString(getTokenDecimals()); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } else { + return ""; + } + } + + @Override + public String getBalanceEquivalent() { + return ""; + } + + @Override + public Uri getWalletExplorerUri() { + return Uri.parse("https://explorer.testnet2.matic.network/account/" + ctx.getCoinData().getWallet()); + } + + @Override + public Uri getShareWalletUri() { + if (ctx.getCard().getDenomination() != null) { + return Uri.parse(ctx.getCoinData().getWallet());// + "?value=" + mCard.getDenomination() +"e18"); + } else { + 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 { + return true; + } + return false; + } + + @Override + public boolean checkNewTransactionAmountAndFee(Amount amount, Amount fee, Boolean isFeeIncluded) { + return true; + } + + @Override + public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) { + final ServerApiMatic serverApiMatic = new ServerApiMatic(); + // request requestData listener + ServerApiMatic.ResponseListener responseListener = new ServerApiMatic.ResponseListener() { + @Override + public void onSuccess(String method, InfuraResponse infuraResponse) { + switch (method) { + case ServerApiMatic.MATIC_ETH_GET_BALANCE: { + String balanceCap = infuraResponse.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 ServerApiMatic.MATIC_ETH_GET_TRANSACTION_COUNT: { + String nonce = infuraResponse.getResult(); + nonce = nonce.substring(2); + BigInteger count = new BigInteger(nonce, 16); + coinData.setConfirmedTXCount(count); + + +// Log.i("$TAG eth_getTransCount", nonce) + } + break; + + case ServerApiMatic.MATIC_ETH_GET_PENDING_COUNT: { + String pending = infuraResponse.getResult(); + pending = pending.substring(2); + BigInteger count = new BigInteger(pending, 16); + coinData.setUnconfirmedTXCount(count); + +// Log.i("$TAG eth_getPendingTxCount", pending) + } + break; +// + case ServerApiMatic.MATIC_ETH_CALL: { + try { + + String balanceCap = infuraResponse.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()) { + serverApiMatic.requestData(ServerApiMatic.MATIC_ETH_GET_BALANCE, 67, coinData.getWallet(), "", ""); + serverApiMatic.requestData(ServerApiMatic.MATIC_ETH_GET_TRANSACTION_COUNT, 67, coinData.getWallet(), "", ""); + serverApiMatic.requestData(ServerApiMatic.MATIC_ETH_GET_PENDING_COUNT, 67, coinData.getWallet(), "", ""); + } else { + ctx.setError("Terminated by user"); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + break; + + } + if (serverApiMatic.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError()); + } else { + blockchainRequestsCallbacks.onProgress(); + } + } + + @Override + public void onFail(String method, String message) { + if (!serverApiMatic.isRequestsSequenceCompleted()) { + ctx.setError(message); + blockchainRequestsCallbacks.onComplete(false); + } + } + }; + serverApiMatic.setResponseListener(responseListener); + + if (validateAddress(getContractAddress(ctx.getCard()))) { + serverApiMatic.requestData(ServerApiMatic.MATIC_ETH_CALL, 67, coinData.getWallet(), getContractAddress(ctx.getCard()), ""); + } else { + ctx.setError("Smart contract address not defined"); + blockchainRequestsCallbacks.onComplete(false); + } + } + + @Override + public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) { + coinData.minFee = coinData.normalFee = coinData.maxFee = new Amount(0L, getFeeCurrency()); + blockchainRequestsCallbacks.onComplete(true); + } + + @Override + public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) { + + String txStr = String.format("0x%s", BTCUtils.toHex(txForSend)); + + final ServerApiMatic serverApiMatic = new ServerApiMatic(); + // request requestData listener + ServerApiMatic.ResponseListener responseListener = new ServerApiMatic.ResponseListener() { + @Override + public void onSuccess(String method, InfuraResponse infuraResponse) { + if (method.equals(ServerApiMatic.MATIC_ETH_SEND_RAW_TRANSACTION)) { + if (infuraResponse.getResult()==null || 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(ServerApiMatic.MATIC_ETH_SEND_RAW_TRANSACTION)) { + ctx.setError(message); + blockchainRequestsCallbacks.onComplete(false); + } + } + }; + + serverApiMatic.setResponseListener(responseListener); + + serverApiMatic.requestData(ServerApiMatic.MATIC_ETH_SEND_RAW_TRANSACTION, 67, coinData.getWallet(), "", txStr); + + } + + @Override + public boolean needMultipleLinesForBalance() { + return false; + } + + @Override + public boolean allowSelectFeeLevel() { + return false; + } + + @Override + public boolean allowSelectFeeInclusion() { + return false; + } +} diff --git a/build.gradle b/build.gradle index a07f7985d1..ba5d47436c 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0' + classpath 'com.android.tools.build:gradle:3.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' }