diff --git a/app/src/main/java/com/tangem/data/network/BinanceApi.java b/app/src/main/java/com/tangem/data/network/BinanceApi.java new file mode 100644 index 0000000000..70e84259f9 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/BinanceApi.java @@ -0,0 +1,11 @@ +package com.tangem.data.network; + +import com.tangem.data.network.model.BinanceFees; + +import retrofit2.Call; +import retrofit2.http.GET; + +public interface BinanceApi { + @GET("/fees") + Call binanceFees(); +} 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 497d5baa11..35a7431285 100644 --- a/app/src/main/java/com/tangem/data/network/Server.java +++ b/app/src/main/java/com/tangem/data/network/Server.java @@ -17,7 +17,7 @@ public class Server { public static final String URL_COINMARKET = ServerURL.API_COINMARKETCAP; public static class Method { - static final String V1_TICKER_CONVERT = URL_COINMARKET + "v1/ticker/?convert=USD&lmit=10"; + static final String V1_TICKER_CONVERT = URL_COINMARKET + "v1/ticker/?convert=USD&limit=10"; } } @@ -56,13 +56,21 @@ public class Server { } } -// 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 ApiBinance { + public static final String URL_BINANCE = ServerURL.API_BINANCE; + + public static class Method { + public static final String API_V1 = URL_BINANCE + "api/v1"; + } + } + + public static class ApiBinanceTestnet { + public static final String URL_BINANCE_TESTNET = ServerURL.API_BINANCE_TESTNET; + + public static class Method { + public static final String API_V1 = URL_BINANCE_TESTNET + "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/ServerURL.java b/app/src/main/java/com/tangem/data/network/ServerURL.java index a2f7c27b66..0d38feb2c0 100644 --- a/app/src/main/java/com/tangem/data/network/ServerURL.java +++ b/app/src/main/java/com/tangem/data/network/ServerURL.java @@ -8,4 +8,6 @@ class ServerURL { static final String API_UPDATE_VERSION = "https://raw.githubusercontent.com/"; static final String API_ROOTSTOCK = "https://public-node.rsk.co/"; 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/"; } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/model/BinanceFee.kt b/app/src/main/java/com/tangem/data/network/model/BinanceFee.kt new file mode 100644 index 0000000000..9e439231d7 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/BinanceFee.kt @@ -0,0 +1,16 @@ +package com.tangem.data.network.model + +import com.google.gson.annotations.SerializedName + +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 +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/ui/fragment/LoadedWallet.kt b/app/src/main/java/com/tangem/ui/fragment/LoadedWallet.kt index f694614e47..222473fa12 100644 --- a/app/src/main/java/com/tangem/ui/fragment/LoadedWallet.kt +++ b/app/src/main/java/com/tangem/ui/fragment/LoadedWallet.kt @@ -727,6 +727,7 @@ class LoadedWallet : androidx.fragment.app.Fragment(), NfcAdapter.ReaderCallback Blockchain.RootstockToken -> "bitcoin" Blockchain.Cardano -> "cardano" Blockchain.Ripple -> "ripple" + Blockchain.Binance -> "binance-coin" 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 1640e4a0bb..c5ec5b7121 100644 --- a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt +++ b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt @@ -7,6 +7,7 @@ import com.tangem.wallet.eth.EthEngine import com.tangem.wallet.token.TokenEngine import com.tangem.wallet.bch.BtcCashEngine import com.tangem.data.Blockchain +import com.tangem.wallet.binance.BinanceEngine import com.tangem.wallet.cardano.CardanoData import com.tangem.wallet.cardano.CardanoEngine import com.tangem.wallet.ltc.LtcEngine @@ -39,6 +40,7 @@ object CoinEngineFactory { Blockchain.RootstockToken -> RskTokenEngine() Blockchain.Cardano -> CardanoEngine() Blockchain.Ripple -> XrpEngine() + Blockchain.Binance, Blockchain.BinanceTestNet -> BinanceEngine() else -> null } } @@ -66,6 +68,8 @@ object CoinEngineFactory { CardanoEngine(context) else if (Blockchain.Ripple == context.blockchain) XrpEngine(context) + else if (Blockchain.Binance == context.blockchain || Blockchain.BinanceTestNet == context.blockchain) + BinanceEngine(context) else return null } catch (e: Exception) { diff --git a/app/src/main/java/com/tangem/wallet/binance/BinanceData.java b/app/src/main/java/com/tangem/wallet/binance/BinanceData.java index e485a7e157..9881e3e85e 100644 --- a/app/src/main/java/com/tangem/wallet/binance/BinanceData.java +++ b/app/src/main/java/com/tangem/wallet/binance/BinanceData.java @@ -1,10 +1,51 @@ package com.tangem.wallet.binance; +import android.os.Bundle; +import android.util.Log; + import com.tangem.wallet.CoinData; import com.tangem.wallet.CoinEngine; public class BinanceData extends CoinData { - private String balance; + private String balance, chainId; + private Long sequence; + private Integer accountNumber; + + @Override + public void loadFromBundle(Bundle B) { + super.loadFromBundle(B); + + if (B.containsKey("Balance")) balance = B.getString("Balance"); + else balance = null; + if (B.containsKey("ChainId")) chainId = B.getString("ChainId"); + else chainId = null; + if (B.containsKey("Sequence")) sequence = B.getLong("Sequence"); + else sequence = null; + if (B.containsKey("AccountNumber")) accountNumber = B.getInt("AccountNumber"); + else accountNumber = null; + } + + @Override + public void saveToBundle(Bundle B) { + super.saveToBundle(B); + try { + if (balance != null) B.putString("Balance", balance); + if (chainId != null) B.putString("ChainId", chainId); + if (sequence != null) B.putLong("Sequence", sequence); + if (accountNumber != null) B.putLong("AccountNumber", accountNumber); + } catch (Exception e) { + Log.e("Can't save to bundle ", e.getMessage()); + } + } + + @Override + public void clearInfo() { + super.clearInfo(); + balance = null; + chainId = null; + sequence = null; + accountNumber = null; + } public CoinEngine.Amount getBalance() { return new CoinEngine.Amount(balance, "BNB"); @@ -17,4 +58,28 @@ public class BinanceData extends CoinData { public boolean hasBalanceInfo() { return balance != null; } + + public Integer getAccountNumber() { + return accountNumber; + } + + public void setAccountNumber(Integer accountNumber) { + this.accountNumber = accountNumber; + } + + public Long getSequence() { + return sequence; + } + + public void setSequence(Long sequence) { + this.sequence = sequence; + } + + public String getChainId() { + return chainId; + } + + public void setChainId(String chain_id) { + this.chainId = chain_id; + } } diff --git a/app/src/main/java/com/tangem/wallet/binance/BinanceEngine.java b/app/src/main/java/com/tangem/wallet/binance/BinanceEngine.java index 7c0e02f2b9..69641e9c94 100644 --- a/app/src/main/java/com/tangem/wallet/binance/BinanceEngine.java +++ b/app/src/main/java/com/tangem/wallet/binance/BinanceEngine.java @@ -2,12 +2,19 @@ package com.tangem.wallet.binance; import android.net.Uri; import android.text.InputFilter; +import android.util.Log; +import androidx.annotation.NonNull; + +import com.ripple.crypto.ecdsa.ECDSASignature; 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.data.network.BinanceApi; +import com.tangem.data.network.Server; +import com.tangem.data.network.model.BinanceFees; import com.tangem.util.CryptoUtil; import com.tangem.util.DecimalDigitsInputFilter; import com.tangem.wallet.BalanceValidator; @@ -16,24 +23,41 @@ 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.BinanceDexApiClientGenerator; import com.tangem.wallet.binance.client.BinanceDexApiRestClient; import com.tangem.wallet.binance.client.BinanceDexEnvironment; +import com.tangem.wallet.binance.client.domain.Account; +import com.tangem.wallet.binance.client.domain.Balance; +import com.tangem.wallet.binance.client.domain.TransactionMetadata; +import com.tangem.wallet.binance.client.domain.broadcast.TransactionOption; 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 com.tangem.wallet.binance.client.encoding.message.TransactionRequestAssemblerExtSign; +import com.tangem.wallet.binance.client.encoding.message.TransferMessage; +import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Utils; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; import java.util.Arrays; +import java.util.List; + +import okhttp3.RequestBody; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; public class BinanceEngine extends CoinEngine { private static final String TAG = BinanceEngine.class.getSimpleName(); public BinanceData coinData = null; + BinanceDexApiRestClient client = null; public BinanceEngine(TangemContext context) throws Exception { super(context); @@ -43,7 +67,16 @@ public class BinanceEngine extends CoinEngine { } else if (context.getCoinData() instanceof BinanceData) { coinData = (BinanceData) context.getCoinData(); } else { - throw new Exception("Invalid type of Blockchain data for XrpEngine"); + throw new Exception("Invalid type of Blockchain data for " + TAG); + } + if (ctx.getBlockchain() == Blockchain.Binance) { + client = BinanceDexApiClientFactory.newInstance().newRestClient(BinanceDexEnvironment.PROD.getBaseUrl()); + coinData.setChainId("Binance-Chain-Tigris"); + } else if (ctx.getBlockchain() == Blockchain.BinanceTestNet) { + client = BinanceDexApiClientFactory.newInstance().newRestClient(BinanceDexEnvironment.TEST_NET.getBaseUrl()); + coinData.setChainId("Binance-Chain-Nile"); + } else { + throw new Exception("Invalid blockchain for BinanceEngine"); } } @@ -60,15 +93,15 @@ public class BinanceEngine extends CoinEngine { } @Override - public boolean awaitingConfirmation() { //TODO:check + public boolean awaitingConfirmation() { return false; } @Override - public String getBalanceHTML() { //TODO + public String getBalanceHTML() { Amount balance = getBalance(); if (balance != null) { - return " " + balance.toDescriptionString(getDecimals()) + "
+ " + convertToAmount(coinData.getReserveInInternalUnits()).toDescriptionString(getDecimals()) + " reserve"; + return balance.toDescriptionString(getDecimals()); } else { return ""; } @@ -147,7 +180,14 @@ public class BinanceEngine extends CoinEngine { @Override public Uri getWalletExplorerUri() { - return Uri.parse("https://testnet-explorer.binance.org/address/" + ctx.getCoinData().getWallet()); //TODO: add mainnet explorer + if (ctx.getBlockchain() == Blockchain.Binance) { + return Uri.parse("https://explorer.binance.org/address/" + ctx.getCoinData().getWallet()); + } else if (ctx.getBlockchain() == Blockchain.BinanceTestNet) { + return Uri.parse("https://testnet-explorer.binance.org/address/" + ctx.getCoinData().getWallet()); + } else { + Log.e(TAG, "Invalid blockchain for BinanceEngine"); + return Uri.parse("https://explorer.binance.org/address/" + ctx.getCoinData().getWallet()); + } } public Uri getShareWalletUri() { @@ -258,7 +298,7 @@ public class BinanceEngine extends CoinEngine { } 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); + throw new Exception("Invalid blockchain for BinanceEngine"); } } @@ -273,7 +313,8 @@ public class BinanceEngine extends CoinEngine { } @Override - public InternalAmount convertToInternalAmount(Amount amount) {; + public InternalAmount convertToInternalAmount(Amount amount) { + ; return new InternalAmount(amount, getBalanceCurrency()); } @@ -316,7 +357,7 @@ public class BinanceEngine extends CoinEngine { public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception { checkBlockchainDataExists(); - String amount, fee; + String amount; if (IncFee) { amount = amountValue.subtract(feeValue).setScale(getDecimals(), RoundingMode.DOWN).toPlainString(); @@ -324,82 +365,170 @@ public class BinanceEngine extends CoinEngine { amount = amountValue.setScale(getDecimals(), RoundingMode.DOWN).toPlainString(); } - Transfer tx = new Transfer(); + Transfer transfer = new Transfer(); + transfer.setCoin("BNB"); + transfer.setFromAddress(ctx.getCoinData().getWallet()); + transfer.setToAddress(targetAddress); + transfer.setAmount(amount); - tx.setCoin("BNB"); - tx.setFromAddress(ctx.getCoinData().getWallet()); - tx.setToAddress(targetAddress); - tx.setAmount(amount); + TransactionOption options = TransactionOption.DEFAULT_INSTANCE; - BinanceDexApiRestClient client = BinanceDexApiClientFactory.newInstance().newRestClient(BinanceDexEnvironment.TEST_NET.getBaseUrl()); + TransactionRequestAssemblerExtSign txAssembler = client.prepareTransfer(transfer, coinData, ctx.getCard().getWalletPublicKeyRar(), options, true); + // TransactionRequestAssembler.buildTransfer as reference + TransferMessage msgBean = txAssembler.createTransferMessage(transfer); + byte[] msg = txAssembler.encodeTransferMessage(msgBean); + byte[] dataForSign = txAssembler.prepareForSign(msgBean); + return new SignTask.TransactionToSign() { -// 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; -// } -// }; + @Override + public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) { + return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw; + } + + @Override + public byte[][] getHashesToSign() { + byte[][] hashForSign = new byte[1][]; + hashForSign[1] = CryptoUtil.doubleSha256(dataForSign); + return hashForSign; + } + + @Override + public byte[] getRawDataToSign() { + return dataForSign; + } + + @Override + public String getHashAlgToSign() { + return "sha-256x2"; + } + + @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 { + 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); + ECKey.ECDSASignature sig = new ECKey.ECDSASignature(r, s); + byte[] sigDer = sig.encodeToDER(); + if (!ECDSASignature.isStrictlyCanonical(sigDer)) { + throw new IllegalStateException("Signature is not strictly canonical"); + } + + // TransactionRequestAssembler.buildTransfer as reference + byte[] signature = txAssembler.encodeSignature(sigDer); + return txAssembler.encodeStdTx(msg, signature); + } + }; + } + + public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) { + try { + Account account = client.getAccount(ctx.getCoinData().getWallet()); + + for (Balance balance : account.getBalances()) { + if (balance.getSymbol().equals("BNB")) { + coinData.setBalanceReceived(true); + coinData.setBalance(balance.getFree()); + break; + } + } + + coinData.setAccountNumber(account.getAccountNumber()); + coinData.setSequence(account.getSequence()); + + if (ctx.getBlockchain() == Blockchain.Binance) { + coinData.setValidationNodeDescription(Server.ApiBinance.URL_BINANCE); + } else if (ctx.getBlockchain() == Blockchain.BinanceTestNet) { + coinData.setValidationNodeDescription(Server.ApiBinanceTestnet.URL_BINANCE_TESTNET); + } else { + throw new Exception("Invalid blockchain for BinanceEngine"); + } + + blockchainRequestsCallbacks.onComplete(true); + } catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, "FAIL RIPPLE_FEE Exception"); + ctx.setError(e.getMessage()); + blockchainRequestsCallbacks.onComplete(false); + } + } + + public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) { + try { + String baseUrl = null; + + if (ctx.getBlockchain() == Blockchain.Binance) { + baseUrl = Server.ApiBinance.Method.API_V1; + } else if (ctx.getBlockchain() == Blockchain.BinanceTestNet) { + baseUrl = Server.ApiBinanceTestnet.Method.API_V1; + } else { + throw new Exception("Invalid blockchain for BinanceEngine"); + } + + Retrofit retrofitBinance = new Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + + BinanceApi binanceApi = retrofitBinance.create(BinanceApi.class); + Call call = binanceApi.binanceFees(); + call.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.code() == 200) { + Long fee = response.body().getFixed_fee_params().getFee(); + Amount feeAmount = new Amount(BigDecimal.valueOf(fee).divide(BigDecimal.valueOf(100000000)).setScale(8, RoundingMode.DOWN), getFeeCurrency()); + coinData.minFee = coinData.normalFee = coinData.maxFee = feeAmount; + Log.i(TAG, "requestFee onResponse " + response.code()); + blockchainRequestsCallbacks.onComplete(true); + } else { + ctx.setError(response.code()); + Log.e(TAG, "requestFee onResponse " + response.code()); + blockchainRequestsCallbacks.onComplete(false); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + ctx.setError(t.getMessage()); + Log.e(TAG, "requestFee onFailure " + t.getMessage()); + blockchainRequestsCallbacks.onComplete(false); + } + }); + } catch (Exception e) { + ctx.setError(e.getMessage()); + e.printStackTrace(); + Log.e(TAG, "FAIL Binance fee exception"); + blockchainRequestsCallbacks.onComplete(false); + } + } + + public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) { + RequestBody requestBody = TransactionRequestAssemblerExtSign.createRequestBody(txForSend); + try { + List metadatas = client.broadcastNoWallet(requestBody, true); + if (!metadatas.isEmpty() && metadatas.get(0).isOk()) { + ctx.setError(null); + blockchainRequestsCallbacks.onComplete(true); + } else { + Log.e(TAG, "Transaction send error"); + ctx.setError("Transaction send error"); + blockchainRequestsCallbacks.onComplete(false); + } + } catch (Exception e) { + Log.e(TAG, "Transaction send error"); + ctx.setError("Transaction send error"); + blockchainRequestsCallbacks.onComplete(false); + } + } + + public boolean allowSelectFeeInclusion() { + return false; } } diff --git a/app/src/main/java/com/tangem/wallet/binance/client/BinanceDexApiRestClient.java b/app/src/main/java/com/tangem/wallet/binance/client/BinanceDexApiRestClient.java index b05deed3b0..2e8f6675b2 100644 --- a/app/src/main/java/com/tangem/wallet/binance/client/BinanceDexApiRestClient.java +++ b/app/src/main/java/com/tangem/wallet/binance/client/BinanceDexApiRestClient.java @@ -1,16 +1,21 @@ package com.tangem.wallet.binance.client; +import com.tangem.wallet.TangemContext; +import com.tangem.wallet.binance.BinanceData; import com.tangem.wallet.binance.client.domain.*; import com.tangem.wallet.binance.client.domain.broadcast.*; import com.tangem.wallet.binance.client.domain.request.ClosedOrdersRequest; import com.tangem.wallet.binance.client.domain.request.OpenOrdersRequest; import com.tangem.wallet.binance.client.domain.request.TradesRequest; import com.tangem.wallet.binance.client.domain.request.TransactionsRequest; +import com.tangem.wallet.binance.client.encoding.message.TransactionRequestAssemblerExtSign; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.util.List; +import okhttp3.RequestBody; + public interface BinanceDexApiRestClient { Time getTime(); @@ -56,6 +61,8 @@ public interface BinanceDexApiRestClient { TransactionPage getTransactions(TransactionsRequest request); + public List broadcastNoWallet(RequestBody requestBody, boolean sync) throws BinanceDexApiException; + List newOrder(NewOrder newOrder, Wallet wallet, TransactionOption options, boolean sync) throws IOException, NoSuchAlgorithmException; @@ -65,6 +72,9 @@ public interface BinanceDexApiRestClient { List transfer(Transfer transfer, Wallet wallet, TransactionOption options, boolean sync) throws IOException, NoSuchAlgorithmException; + TransactionRequestAssemblerExtSign prepareTransfer(Transfer transfer, BinanceData binanceData, byte[] pubKey, TransactionOption options, boolean sync) + throws IOException, NoSuchAlgorithmException; + List freeze(TokenFreeze freeze, Wallet wallet, TransactionOption options, boolean sync) throws IOException, NoSuchAlgorithmException; diff --git a/app/src/main/java/com/tangem/wallet/binance/client/encoding/message/TransactionRequestAssemblerExtSign.java b/app/src/main/java/com/tangem/wallet/binance/client/encoding/message/TransactionRequestAssemblerExtSign.java new file mode 100644 index 0000000000..97ef075d3e --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/binance/client/encoding/message/TransactionRequestAssemblerExtSign.java @@ -0,0 +1,161 @@ +package com.tangem.wallet.binance.client.encoding.message; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.protobuf.ByteString; +import com.tangem.wallet.binance.BinanceData; +import com.tangem.wallet.binance.client.domain.broadcast.TransactionOption; +import com.tangem.wallet.binance.client.domain.broadcast.Transfer; +import com.tangem.wallet.binance.client.encoding.Crypto; +import com.tangem.wallet.binance.client.encoding.EncodeUtils; +import com.tangem.wallet.binance.proto.StdSignature; +import com.tangem.wallet.binance.proto.StdTx; + +import java.io.IOException; +import java.math.BigDecimal; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.List; + +import okhttp3.RequestBody; + +/** + * Assemble a transaction message body. + * https://testnet-dex.binance.org/doc/encoding.html + */ +public class TransactionRequestAssemblerExtSign { + private static final okhttp3.MediaType MEDIA_TYPE = okhttp3.MediaType.parse("text/plain; charset=utf-8"); + private static final BigDecimal MULTIPLY_FACTOR = BigDecimal.valueOf(1e8); + private static final BigDecimal MAX_NUMBER = new BigDecimal(Long.MAX_VALUE); + + //private Wallet wallet; + private BinanceData binanceData; + private byte[] pubKey; + private TransactionOption options; + + public TransactionRequestAssemblerExtSign(BinanceData binanceData, byte[] pubKey, TransactionOption options) { + this.binanceData = binanceData; + this.pubKey = pubKey; + this.options = options; + } + + public static long doubleToLong(String d) { + BigDecimal encodeValue = new BigDecimal(d).multiply(MULTIPLY_FACTOR); + if (encodeValue.compareTo(MAX_NUMBER) > 0) { + throw new IllegalArgumentException(d + " is too large."); + } + return encodeValue.longValue(); + + } + + public byte[] prepareForSign(BinanceDexTransactionMessage msg) + throws JsonProcessingException, NoSuchAlgorithmException { + SignData sd = new SignData(); + sd.setChainId(binanceData.getChainId()); + sd.setAccountNumber(String.valueOf(binanceData.getAccountNumber())); + sd.setSequence(String.valueOf(binanceData.getAccountNumber())); + sd.setMsgs(new BinanceDexTransactionMessage[]{msg}); + + sd.setMemo(options.getMemo()); + sd.setSource(String.valueOf(options.getSource())); + sd.setData(options.getData()); + return EncodeUtils.toJsonEncodeBytes(sd); + } + + public byte[] encodeSignature(byte[] signatureBytes) throws IOException { + StdSignature stdSignature = StdSignature.newBuilder().setPubKey(ByteString.copyFrom(pubKey)) + .setSignature(ByteString.copyFrom(signatureBytes)) + .setAccountNumber(binanceData.getAccountNumber()) + .setSequence(binanceData.getSequence()) + .build(); + + return EncodeUtils.aminoWrap( + stdSignature.toByteArray(), MessageType.StdSignature.getTypePrefixBytes(), false); + } + + public byte[] encodeStdTx(byte[] msg, byte[] signature) throws IOException { + StdTx.Builder stdTxBuilder = StdTx.newBuilder() + .addMsgs(ByteString.copyFrom(msg)) + .addSignatures(ByteString.copyFrom(signature)) + .setMemo(options.getMemo()) + .setSource(options.getSource()); + if (options.getData() != null) { + stdTxBuilder = stdTxBuilder.setData(ByteString.copyFrom(options.getData())); + } + StdTx stdTx = stdTxBuilder.build(); + return EncodeUtils.aminoWrap(stdTx.toByteArray(), MessageType.StdTx.getTypePrefixBytes(), true); + } + + public static RequestBody createRequestBody(byte[] stdTx) { + return RequestBody.create(MEDIA_TYPE, EncodeUtils.bytesToHex(stdTx)); + } + + public TransferMessage createTransferMessage(Transfer transfer) { + Token token = new Token(); + token.setDenom(transfer.getCoin()); + token.setAmount(doubleToLong(transfer.getAmount())); + List coins = Collections.singletonList(token); + + InputOutput input = new InputOutput(); + input.setAddress(transfer.getFromAddress()); + input.setCoins(coins); + InputOutput output = new InputOutput(); + output.setAddress(transfer.getToAddress()); + output.setCoins(coins); + + TransferMessage msgBean = new TransferMessage(); + msgBean.setInputs(Collections.singletonList(input)); + msgBean.setOutputs(Collections.singletonList(output)); + return msgBean; + } + + private com.tangem.wallet.binance.proto.Send.Input toProtoInput(InputOutput input) { + byte[] address = Crypto.decodeAddress(input.getAddress()); + com.tangem.wallet.binance.proto.Send.Input.Builder builder = + com.tangem.wallet.binance.proto.Send.Input.newBuilder().setAddress(ByteString.copyFrom(address)); + + for (Token coin : input.getCoins()) { + com.tangem.wallet.binance.proto.Send.Token protCoin = + com.tangem.wallet.binance.proto.Send.Token.newBuilder().setAmount(coin.getAmount()) + .setDenom(coin.getDenom()).build(); + builder.addCoins(protCoin); + } + return builder.build(); + } + + private com.tangem.wallet.binance.proto.Send.Output toProtoOutput(InputOutput output) { + byte[] address = Crypto.decodeAddress(output.getAddress()); + com.tangem.wallet.binance.proto.Send.Output.Builder builder = + com.tangem.wallet.binance.proto.Send.Output.newBuilder().setAddress(ByteString.copyFrom(address)); + + for (Token coin : output.getCoins()) { + com.tangem.wallet.binance.proto.Send.Token protCoin = + com.tangem.wallet.binance.proto.Send.Token.newBuilder().setAmount(coin.getAmount()) + .setDenom(coin.getDenom()).build(); + builder.addCoins(protCoin); + } + return builder.build(); + } + + public byte[] encodeTransferMessage(TransferMessage msg) + throws IOException { + com.tangem.wallet.binance.proto.Send.Builder builder = com.tangem.wallet.binance.proto.Send.newBuilder(); + for (InputOutput input : msg.getInputs()) { + builder.addInputs(toProtoInput(input)); + } + for (InputOutput output : msg.getOutputs()) { + builder.addOutputs(toProtoOutput(output)); + } + com.tangem.wallet.binance.proto.Send proto = builder.build(); + return EncodeUtils.aminoWrap(proto.toByteArray(), MessageType.Send.getTypePrefixBytes(), false); + } + + public byte[] buildTransfer(Transfer transfer) + throws IOException, NoSuchAlgorithmException { + TransferMessage msgBean = createTransferMessage(transfer); + byte[] msg = encodeTransferMessage(msgBean); + return prepareForSign(msgBean); +// byte[] signature = encodeSignature(prepareForSign(msgBean)); +// byte[] stdTx = encodeStdTx(msg, signature); +// return createRequestBody(stdTx); + } +} diff --git a/app/src/main/java/com/tangem/wallet/binance/client/impl/BinanceDexApiAsyncRestClientImpl.java b/app/src/main/java/com/tangem/wallet/binance/client/impl/BinanceDexApiAsyncRestClientImpl.java index d801f50344..ad6d48d156 100644 --- a/app/src/main/java/com/tangem/wallet/binance/client/impl/BinanceDexApiAsyncRestClientImpl.java +++ b/app/src/main/java/com/tangem/wallet/binance/client/impl/BinanceDexApiAsyncRestClientImpl.java @@ -1,5 +1,9 @@ package com.tangem.wallet.binance.client.impl; +import android.os.Build; + +import androidx.annotation.RequiresApi; + import com.tangem.wallet.binance.client.*; import com.tangem.wallet.binance.client.domain.*; import com.tangem.wallet.binance.client.domain.request.ClosedOrdersRequest; @@ -94,6 +98,7 @@ public class BinanceDexApiAsyncRestClientImpl implements BinanceDexApiAsyncRestC new BinanceDexApiCallbackAdapter<>(callback)); } + @RequiresApi(api = Build.VERSION_CODES.N) @Override public void getClosedOrders(String address, BinanceDexApiCallback callback) { ClosedOrdersRequest request = new ClosedOrdersRequest(); @@ -101,6 +106,7 @@ public class BinanceDexApiAsyncRestClientImpl implements BinanceDexApiAsyncRestC getClosedOrders(request, callback); } + @RequiresApi(api = Build.VERSION_CODES.N) @Override public void getClosedOrders(ClosedOrdersRequest request, BinanceDexApiCallback callback) { String sidStr = request.getSide() == null ? null : request.getSide().name(); diff --git a/app/src/main/java/com/tangem/wallet/binance/client/impl/BinanceDexApiRestClientImpl.java b/app/src/main/java/com/tangem/wallet/binance/client/impl/BinanceDexApiRestClientImpl.java index 34b5698a82..7f2cc260d2 100644 --- a/app/src/main/java/com/tangem/wallet/binance/client/impl/BinanceDexApiRestClientImpl.java +++ b/app/src/main/java/com/tangem/wallet/binance/client/impl/BinanceDexApiRestClientImpl.java @@ -4,6 +4,7 @@ import android.os.Build; import androidx.annotation.RequiresApi; +import com.tangem.wallet.binance.BinanceData; import com.tangem.wallet.binance.client.*; import com.tangem.wallet.binance.client.domain.*; import com.tangem.wallet.binance.client.domain.broadcast.*; @@ -12,6 +13,8 @@ import com.tangem.wallet.binance.client.domain.request.OpenOrdersRequest; import com.tangem.wallet.binance.client.domain.request.TradesRequest; import com.tangem.wallet.binance.client.domain.request.TransactionsRequest; import com.tangem.wallet.binance.client.encoding.message.TransactionRequestAssembler; +import com.tangem.wallet.binance.client.encoding.message.TransactionRequestAssemblerExtSign; + import okhttp3.RequestBody; import java.io.IOException; @@ -167,6 +170,10 @@ public class BinanceDexApiRestClientImpl implements BinanceDexApiRestClient { } } + public List broadcastNoWallet(RequestBody requestBody, boolean sync) throws BinanceDexApiException { + return BinanceDexApiClientGenerator.executeSync(binanceDexApi.broadcast(sync, requestBody)); + } + public List newOrder(NewOrder newOrder, Wallet wallet, TransactionOption options, boolean sync) throws IOException, NoSuchAlgorithmException { wallet.ensureWalletIsReady(this); @@ -191,6 +198,14 @@ public class BinanceDexApiRestClientImpl implements BinanceDexApiRestClient { return broadcast(requestBody, sync, wallet); } + public TransactionRequestAssemblerExtSign prepareTransfer(Transfer transfer, BinanceData binanceData, byte[] pubKey, TransactionOption options, boolean sync) + throws IOException, NoSuchAlgorithmException { + TransactionRequestAssemblerExtSign assembler = new TransactionRequestAssemblerExtSign(binanceData, pubKey, options); + return assembler; +// RequestBody requestBody = assembler.buildTransfer(transfer); +// return broadcast(requestBody, sync, wallet); + } + public List freeze(TokenFreeze freeze, Wallet wallet, TransactionOption options, boolean sync) throws IOException, NoSuchAlgorithmException { wallet.ensureWalletIsReady(this);