diff --git a/app/build.gradle b/app/build.gradle index 69dc18e165..089f9cbf2a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -84,7 +84,8 @@ dependencies { implementation 'com.scottyab:rootbeer-lib:0.0.7' implementation 'com.squareup.retrofit2:converter-gson:2.5.0' implementation 'com.squareup.retrofit2:retrofit:2.5.0' - implementation 'com.squareup.retrofit2:converter-scalars:2.1.0' + implementation 'com.squareup.retrofit2:converter-scalars:2.5.0' + implementation 'com.squareup.retrofit2:converter-jackson:2.5.0' implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0' implementation 'com.skyfishjy.ripplebackground:library:1.0.1' implementation 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0' @@ -108,4 +109,13 @@ dependencies { //noinspection DuplicatePlatformClasses implementation 'org.json:json:20180813' implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' + + //dependencies for binance + implementation 'com.google.protobuf:protobuf-java:3.6.1' + implementation 'io.grpc:grpc-protobuf:1.17.1' + implementation 'io.grpc:grpc-stub:1.17.1' + implementation 'commons-codec:commons-codec:1.10' + implementation 'org.apache.commons:commons-lang3:3.6' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-joda:2.9.8' + implementation 'joda-time:joda-time:2.10.1' } \ 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 c613c3e3d6..c670c6fb61 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -18,7 +18,9 @@ public enum Blockchain { Rootstock("RSK", "RBTC", 1.0, R.drawable.tangem2, "Rootstock"), RootstockToken("RskToken", "RBTC", 1.0, R.drawable.tangem2, "Rootstock"), Cardano("CARDANO", "ADA", 1000000.0, R.drawable.tangem2, "Cardano"), - Ripple ("XRP", "XRP", 1000000.0, R.drawable.tangem2, "Ripple"); + Ripple ("XRP", "XRP", 1000000.0, R.drawable.tangem2, "Ripple"), + Binance("BINANCE", "BNB", 100000000.0, R.drawable.tangem2, "Binance"), + BinanceTestNet("BINANCE/test", "BNB", 100000000.0, R.drawable.tangem2, "Binance 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/BinanceApi.java b/app/src/main/java/com/tangem/data/network/BinanceApi.java deleted file mode 100644 index 532aaae7c9..0000000000 --- a/app/src/main/java/com/tangem/data/network/BinanceApi.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.data.network; - -import com.tangem.data.network.model.BinanceAccount; -import com.tangem.data.network.model.BinanceError; -import com.tangem.data.network.model.BinanceFees; - -import retrofit2.Call; -import retrofit2.http.Body; -import retrofit2.http.GET; -import retrofit2.http.Headers; -import retrofit2.http.POST; - -public interface BinanceApi { - @GET(Server.ApiBinance.Method.ACCOUNT) - Call binanceAccount(); - - @GET(Server.ApiBinance.Method.FEES) - Call binanceFees(); - - @Headers("Content-Type: text/plain") - @POST(Server.ApiBinance.Method.BROADCAST) - Call binanceBroadcast(@Body String txForSend); -} 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 b657448aa4..497d5baa11 100644 --- a/app/src/main/java/com/tangem/data/network/Server.java +++ b/app/src/main/java/com/tangem/data/network/Server.java @@ -56,16 +56,13 @@ public class Server { } } - public static class ApiBinance { - public static final String URL_BINANCE = ServerURL.API_BINANCE; - static final String API_V1 = "api/v1/"; - - public static class Method { - static final String ACCOUNT = URL_BINANCE + API_V1 + "account"; - static final String FEES = URL_BINANCE + API_V1 + "fees"; - static final String BROADCAST = URL_BINANCE + API_V1 + "broadcast"; - } - } +// public static class ApiBinance { +// public static final String URL_BINANCE = ServerURL.API_BINANCE; +// +// public static class Method { +// static final String API_V1 = URL_BINANCE + "api/v1"; +// } +// } public static class ApiBlockcypher { public static final String URL_BLOCKCYPHER = ServerURL.API_BLOCKCYPHER; diff --git a/app/src/main/java/com/tangem/data/network/ServerApiBinance.java b/app/src/main/java/com/tangem/data/network/ServerApiBinance.java deleted file mode 100644 index e361fe64e2..0000000000 --- a/app/src/main/java/com/tangem/data/network/ServerApiBinance.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.tangem.data.network; - -public class ServerApiBinance { -} 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 b66e76a6ce..a2f7c27b66 100644 --- a/app/src/main/java/com/tangem/data/network/ServerURL.java +++ b/app/src/main/java/com/tangem/data/network/ServerURL.java @@ -7,6 +7,5 @@ class ServerURL { 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/"; - static final String API_BINANCE = "https://testnet-dex.binance.org/"; static final String API_BLOCKCYPHER = "https://api.blockcypher.com/"; } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/model/BinanceResponse.kt b/app/src/main/java/com/tangem/data/network/model/BinanceResponse.kt deleted file mode 100644 index 0e11bd307d..0000000000 --- a/app/src/main/java/com/tangem/data/network/model/BinanceResponse.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.data.network.model - -import com.google.gson.annotations.SerializedName -import java.security.MessageDigest - -data class BinanceAccount( - @SerializedName("address") - var address: String? = null, - - @SerializedName("sequence") - var sequence: Long? = null, - - @SerializedName("balances") - var balances: List? = null -) - -data class BinanceBalance( - @SerializedName("symbol") - var symbol: String? = null, - - @SerializedName("free") - var free: String? = null -) - -data class BinanceFees( - @SerializedName("fixed_fee_params") - var fixed_fee_params: BinanceFixedFee? = null -) - -data class BinanceFixedFee( - @SerializedName("msg_type") - var msg_type: String? = null, - - @SerializedName("fee") - var fee: Long? = null -) - -data class BinanceError( - @SerializedName("message") - var message: String? = null -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/binance/BinanceData.java b/app/src/main/java/com/tangem/wallet/binance/BinanceData.java new file mode 100644 index 0000000000..e485a7e157 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/binance/BinanceData.java @@ -0,0 +1,20 @@ +package com.tangem.wallet.binance; + +import com.tangem.wallet.CoinData; +import com.tangem.wallet.CoinEngine; + +public class BinanceData extends CoinData { + private String balance; + + public CoinEngine.Amount getBalance() { + return new CoinEngine.Amount(balance, "BNB"); + } + + public void setBalance(String balance) { + this.balance = balance; + } + + public boolean hasBalanceInfo() { + return balance != null; + } +} diff --git a/app/src/main/java/com/tangem/wallet/binance/BinanceEngine.java b/app/src/main/java/com/tangem/wallet/binance/BinanceEngine.java new file mode 100644 index 0000000000..7c0e02f2b9 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/binance/BinanceEngine.java @@ -0,0 +1,405 @@ +package com.tangem.wallet.binance; + +import android.net.Uri; +import android.text.InputFilter; + +import com.tangem.card_common.data.TangemCard; +import com.tangem.card_common.reader.CardProtocol; +import com.tangem.card_common.tasks.SignTask; +import com.tangem.card_common.util.Util; +import com.tangem.data.Blockchain; +import com.tangem.util.CryptoUtil; +import com.tangem.util.DecimalDigitsInputFilter; +import com.tangem.wallet.BalanceValidator; +import com.tangem.wallet.CoinData; +import com.tangem.wallet.CoinEngine; +import com.tangem.wallet.R; +import com.tangem.wallet.TangemContext; +import com.tangem.wallet.binance.client.BinanceDexApiClientFactory; +import com.tangem.wallet.binance.client.BinanceDexApiRestClient; +import com.tangem.wallet.binance.client.BinanceDexEnvironment; +import com.tangem.wallet.binance.client.domain.broadcast.Transfer; +import com.tangem.wallet.binance.client.encoding.Bech32; +import com.tangem.wallet.binance.client.encoding.Crypto; + +import org.bitcoinj.core.Utils; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.util.Arrays; + + +public class BinanceEngine extends CoinEngine { + private static final String TAG = BinanceEngine.class.getSimpleName(); + + public BinanceData coinData = null; + + public BinanceEngine(TangemContext context) throws Exception { + super(context); + if (context.getCoinData() == null) { + coinData = new BinanceData(); + context.setCoinData(coinData); + } else if (context.getCoinData() instanceof BinanceData) { + coinData = (BinanceData) context.getCoinData(); + } else { + throw new Exception("Invalid type of Blockchain data for XrpEngine"); + } + } + + public BinanceEngine() { + 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() { //TODO:check + return false; + } + + @Override + public String getBalanceHTML() { //TODO + Amount balance = getBalance(); + if (balance != null) { + return " " + balance.toDescriptionString(getDecimals()) + "
+ " + convertToAmount(coinData.getReserveInInternalUnits()).toDescriptionString(getDecimals()) + " reserve"; + } else { + return ""; + } + } + + @Override + public String getBalanceCurrency() { + return "BNB"; + } + + @Override + public String getOfflineBalanceHTML() { //TODO:check + 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.getBalance() == null) return false; + return coinData.getBalance().notZero(); + } + + @Override + public boolean hasBalanceInfo() { + if (coinData == null) return false; + return coinData.hasBalanceInfo(); + } + + 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 String getFeeCurrency() { + return "BNB"; + } + + @Override + public boolean validateAddress(String address) { + if (address == null || address.isEmpty()) { + return false; + } + + try { + Crypto.decodeAddress(address); + } catch (Exception e) { + return false; + } + + if (ctx.getBlockchain() == Blockchain.Binance && !address.startsWith("bnb1")) { + return false; + } + + if (ctx.getBlockchain() == Blockchain.BinanceTestNet && !address.startsWith("tbnb1")) { + return false; + } + + return true; + } + + @Override + public boolean isNeedCheckNode() { + return true; + } + + @Override + public Uri getWalletExplorerUri() { + return Uri.parse("https://testnet-explorer.binance.org/address/" + ctx.getCoinData().getWallet()); //TODO: add mainnet explorer + } + + public Uri getShareWalletUri() { + return Uri.parse(ctx.getCoinData().getWallet()); + } //TODO:check + + @Override + public InputFilter[] getAmountInputFilters() { + return new InputFilter[]{new DecimalDigitsInputFilter(getDecimals())}; + } + + @Override + public boolean checkNewTransactionAmount(Amount amount) { + if (coinData == null) return false; + if (amount.compareTo(coinData.getBalance()) > 0) { + return false; + } + return true; + } + + @Override + public boolean checkNewTransactionAmountAndFee(Amount amountValue, Amount feeValue, Boolean isIncludeFee) { + try { + checkBlockchainDataExists(); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + if (feeValue == null || amountValue == null) + return false; + + if (feeValue.isZero() || amountValue.isZero()) + return false; + + if (isIncludeFee && (amountValue.compareTo(coinData.getBalance()) > 0 || amountValue.compareTo(feeValue) < 0)) + return false; + + if (!isIncludeFee && amountValue.add(feeValue).compareTo(coinData.getBalance()) > 0) + return false; + + return true; + } + + @Override + public boolean validateBalance(BalanceValidator balanceValidator) { + try { + 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; + } + + if (coinData.isBalanceReceived()) { + balanceValidator.setScore(100); + balanceValidator.setFirstLine("Verified balance"); + balanceValidator.setSecondLine("Balance confirmed in blockchain"); + if (coinData.getBalance().isZero()) { + balanceValidator.setFirstLine("Empty wallet"); + balanceValidator.setSecondLine(""); + } + } + + if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalance().notZero()) { + balanceValidator.setScore(80); + balanceValidator.setFirstLine("Verified offline balance"); + balanceValidator.setSecondLine("Can't obtain balance from blockchain. Restore internet connection to be more confident. "); + } + + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + @Override + public Amount getBalance() { + if (!hasBalanceInfo()) return null; + return coinData.getBalance(); + } + + @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()); + } + + public String calculateAddress(byte[] pkCompressed) throws Exception { + byte[] pubKeyHash = Utils.sha256hash160(pkCompressed); + + if (ctx.getBlockchain() == Blockchain.Binance) { + return Bech32.encode("bnb", Crypto.convertBits(pubKeyHash, 0, pubKeyHash.length, 8, 5, false)); + } else if (ctx.getBlockchain() == Blockchain.BinanceTestNet) { + return Bech32.encode("tbnb", Crypto.convertBits(pubKeyHash, 0, pubKeyHash.length, 8, 5, false)); + } else { + throw new Exception("Invalid blockchain for " + TAG); + } + } + + @Override + public Amount convertToAmount(InternalAmount internalAmount) { + return new Amount(internalAmount, getBalanceCurrency()); + } + + @Override + public Amount convertToAmount(String strAmount, String currency) { + return new Amount(strAmount, currency); + } + + @Override + public InternalAmount convertToInternalAmount(Amount amount) {; + return new InternalAmount(amount, getBalanceCurrency()); + } + + @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), getBalanceCurrency()); + } + + @Override + public byte[] convertToByteArray(InternalAmount internalAmount) { + byte[] bytes = Util.longToByteArray(internalAmount.longValueExact()); + return bytes; + } + + @Override + public CoinData createCoinData() { + return new BinanceData(); + } + + @Override + public String getUnspentInputsDescription() { + return ""; + } + + @Override + public void defineWallet() throws CardProtocol.TangemException { + try { + String wallet = calculateAddress(ctx.getCard().getWalletPublicKeyRar()); + ctx.getCoinData().setWallet(wallet); + } catch (Exception e) { + ctx.getCoinData().setWallet("ERROR"); + throw new CardProtocol.TangemException("Can't define wallet address"); + } + } + + @Override + public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception { + checkBlockchainDataExists(); + + String amount, fee; + + if (IncFee) { + amount = amountValue.subtract(feeValue).setScale(getDecimals(), RoundingMode.DOWN).toPlainString(); + } else { + amount = amountValue.setScale(getDecimals(), RoundingMode.DOWN).toPlainString(); + } + + Transfer tx = new Transfer(); + + tx.setCoin("BNB"); + tx.setFromAddress(ctx.getCoinData().getWallet()); + tx.setToAddress(targetAddress); + tx.setAmount(amount); + + BinanceDexApiRestClient client = BinanceDexApiClientFactory.newInstance().newRestClient(BinanceDexEnvironment.TEST_NET.getBaseUrl()); + + +// XrpPayment payment = new XrpPayment(); +// +// // Put `as` AccountID field Account, `Object` o +// payment.as(AccountID.Account, coinData.getWallet()); +// payment.as(AccountID.Destination, targetAddress); +// payment.as(com.ripple.core.coretypes.Amount.Amount, amount); +// payment.as(UInt32.Sequence, coinData.getSequence()); +// payment.as(com.ripple.core.coretypes.Amount.Fee, fee); +// +// XrpSignedTransaction signedTx = payment.prepare(canonisePubKey(ctx.getCard().getWalletPublicKeyRar())); +// +// return new SignTask.TransactionToSign() { +// +// @Override +// public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) { +// return signingMethod == TangemCard.SigningMethod.Sign_Hash; +// } +// +// @Override +// public byte[][] getHashesToSign() throws Exception { +// byte[][] dataForSign = new byte[1][]; +// if (ctx.getCard().getWalletPublicKeyRar().length == 33) +// dataForSign[0] = HashUtils.halfSha512(signedTx.signingData); +// else if (ctx.getCard().getWalletPublicKeyRar().length == 32) +// dataForSign[0] = signedTx.signingData; +// else +// throw new Exception("Invalid pubkey length"); +// return dataForSign; +// } +// +// @Override +// public byte[] getRawDataToSign() throws Exception { +// throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName()); +// } +// +// @Override +// public String getHashAlgToSign() throws Exception { +// throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName()); +// } +// +// @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 { +// if (ctx.getCard().getWalletPublicKeyRar().length == 33) { +// int size = signFromCard.length / 2; +// BigInteger r = new BigInteger(1, Arrays.copyOfRange(signFromCard, 0, size)); +// BigInteger s = new BigInteger(1, Arrays.copyOfRange(signFromCard, size, size * 2)); +// s = CryptoUtil.toCanonicalised(s); +// ECDSASignature sig = new ECDSASignature(r, s); +// byte[] sigDer = sig.encodeToDER(); +// if (!ECDSASignature.isStrictlyCanonical(sigDer)) { +// throw new IllegalStateException("Signature is not strictly canonical"); +// } +// signedTx.addSign(sigDer); +// } else if (ctx.getCard().getWalletPublicKeyRar().length == 32) +// signedTx.addSign(signFromCard); +// else +// throw new Exception("Invalid pubkey length"); +// byte[] txForSend = BTCUtils.fromHex(signedTx.tx_blob); +// notifyOnNeedSendTransaction(txForSend); +// return txForSend; +// } +// }; + } +} diff --git a/app/src/main/java/com/tangem/wallet/binance/client/BinanceDexApi.java b/app/src/main/java/com/tangem/wallet/binance/client/BinanceDexApi.java new file mode 100644 index 0000000000..95342b68bd --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/binance/client/BinanceDexApi.java @@ -0,0 +1,82 @@ +package com.tangem.wallet.binance.client; + +import com.tangem.wallet.binance.client.domain.*; +import okhttp3.RequestBody; +import retrofit2.Call; +import retrofit2.http.*; + +import java.util.List; + +public interface BinanceDexApi { + @GET("/api/v1/time") + Call