diff --git a/app/build.gradle b/app/build.gradle index 07bb7d859f..af2ce223fa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -99,7 +99,7 @@ dependencies { implementation 'com.google.dagger:dagger:2.24' kapt 'com.google.dagger:dagger-compiler:2.21' annotationProcessor 'com.google.dagger:dagger-compiler:2.21' - implementation 'com.google.zxing:core:3.4.0' + implementation 'com.google.zxing:core:3.3.3' // Do not update to 3.4.0, it requires minSdk 24 implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.madgag.spongycastle:core:1.56.0.0' implementation 'com.madgag.spongycastle:prov:1.56.0.0' diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index 402348982e..b2771a6bb2 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -23,6 +23,7 @@ public enum Blockchain { Ripple("XRP", "XRP", 1000000.0, R.drawable.ic_logo_xrp, "XRP"), Binance("BINANCE", "BNB", 100000000.0, R.drawable.ic_logo_binance, "Binance"), BinanceTestNet("BINANCE/test", "BNB", 100000000.0, R.drawable.ic_logo_binance, "Binance Testnet"), + BinanceAsset("BinanceAsset", "BNB", 100000000.0, R.drawable.ic_logo_binance, "Binance"), Matic("MATIC", "MTX", 1.0, R.drawable.tangem2, "Matic"), MaticTestNet("MATIC/test", "MTX", 1.0, R.drawable.tangem2, "Matic Testnet"), Stellar("XLM", "XLM", 10000000.0, R.drawable.ic_logo_stellar, "Stellar"), @@ -104,4 +105,25 @@ public enum Blockchain { return getImageResource(); } + public String getUriScheme() { + String scheme = null; + switch (this) { + case Bitcoin: + case BitcoinDual: + scheme = "bitcoin"; + break; + case Ethereum: + case Token: + case TokenEmv: + scheme = "ethereum"; + break; + case Litecoin: + scheme = "litecoin"; + break; + case Ripple: + scheme = "ripple"; + } + return scheme; + } + } diff --git a/app/src/main/java/com/tangem/data/network/ServerApiBinance.java b/app/src/main/java/com/tangem/data/network/ServerApiBinance.java index b242100ae0..8527a9c0d4 100644 --- a/app/src/main/java/com/tangem/data/network/ServerApiBinance.java +++ b/app/src/main/java/com/tangem/data/network/ServerApiBinance.java @@ -4,6 +4,7 @@ import android.util.Log; import com.tangem.wallet.TangemContext; import com.tangem.wallet.Transaction; +import com.tangem.wallet.binance.BinanceAssetData; import com.tangem.wallet.binance.BinanceData; import com.tangem.wallet.binance.client.BinanceDexApiRestClient; import com.tangem.wallet.binance.client.domain.Account; @@ -57,6 +58,16 @@ public class ServerApiBinance { } } + if (binanceData instanceof BinanceAssetData) { + BinanceAssetData binanceAssetData = (BinanceAssetData) binanceData; + for (Balance balance : account.getBalances()) { + if (balance.getSymbol().equals(ctx.getCard().getContractAddress())) { + binanceAssetData.setAssetBalance(balance.getFree()); + break; + } + } + } + if (!binanceData.isBalanceReceived()) { binanceData.setBalanceReceived(true); binanceData.setBalance("0"); @@ -69,7 +80,7 @@ public class ServerApiBinance { @Override public void onError(Throwable e) { Log.e(TAG, "getBalance onError" + e.getMessage()); - if (e.getMessage().contains("code=404")) { + if (e.getMessage().contains("account not found")) { ((BinanceData)ctx.getCoinData()).setError404(true); responseListener.onFail(); } else { diff --git a/app/src/main/java/com/tangem/ui/fragment/VerifyCardFragment.kt b/app/src/main/java/com/tangem/ui/fragment/VerifyCardFragment.kt index 7e26ac59ae..3fa306f8c0 100644 --- a/app/src/main/java/com/tangem/ui/fragment/VerifyCardFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/VerifyCardFragment.kt @@ -417,6 +417,7 @@ class VerifyCardFragment : BaseFragment(), NavigationResultListener, NfcAdapter. requestPIN2Count = 0 val engine = CoinEngineFactory.create(ctx) ?: return if (!engine.hasBalanceInfo()) { + Toast.makeText(context, R.string.general_error_cannot_erase_wallet_with_non_zero_balance, Toast.LENGTH_LONG).show() return } if (engine.isBalanceNotZero) { diff --git a/app/src/main/java/com/tangem/wallet/CoinEngine.java b/app/src/main/java/com/tangem/wallet/CoinEngine.java index 5fce47c602..edf38cad4c 100644 --- a/app/src/main/java/com/tangem/wallet/CoinEngine.java +++ b/app/src/main/java/com/tangem/wallet/CoinEngine.java @@ -197,10 +197,11 @@ public abstract class CoinEngine { public abstract Uri getShareWalletUri(); public Uri getShareWalletUriEx(){ - if (ctx.getBlockchain() == Blockchain.BitcoinCash) - return getShareWalletUri(); + String scheme = ctx.getBlockchain().getUriScheme(); + if (scheme != null) + return Uri.parse(scheme + ":" + getShareWalletUri()); else - return Uri.parse(ctx.getBlockchain().name().toLowerCase() + ":" + getShareWalletUri().toString()); + return getShareWalletUri(); } public abstract boolean checkNewTransactionAmount(Amount amount); diff --git a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt index 8bfbb7f361..0cf887e769 100644 --- a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt +++ b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt @@ -3,6 +3,7 @@ package com.tangem.wallet import android.util.Log import com.tangem.data.Blockchain import com.tangem.wallet.bch.BtcCashEngine +import com.tangem.wallet.binance.BinanceAssetEngine import com.tangem.wallet.binance.BinanceEngine import com.tangem.wallet.btc.BtcEngine import com.tangem.wallet.btcmultisig.BtcMultisigEngine @@ -52,6 +53,7 @@ object CoinEngineFactory { Blockchain.Cardano -> CardanoEngine() Blockchain.Ripple -> XrpEngine() Blockchain.Binance, Blockchain.BinanceTestNet -> BinanceEngine() + Blockchain.BinanceAsset -> BinanceAssetEngine() Blockchain.Matic, Blockchain.MaticTestNet -> MaticTokenEngine() Blockchain.StellarTestNet, Blockchain.Stellar -> XlmEngine() Blockchain.StellarAsset -> XlmAssetEngine() @@ -93,6 +95,8 @@ object CoinEngineFactory { XrpEngine(context) else if (Blockchain.Binance == context.blockchain || Blockchain.BinanceTestNet == context.blockchain) BinanceEngine(context) + else if (Blockchain.BinanceAsset == context.blockchain) + BinanceAssetEngine(context) else if (Blockchain.Matic == context.blockchain || Blockchain.MaticTestNet == context.blockchain) MaticTokenEngine(context) else if (Blockchain.Stellar == context.blockchain || Blockchain.StellarTestNet == context.blockchain) diff --git a/app/src/main/java/com/tangem/wallet/TangemContext.java b/app/src/main/java/com/tangem/wallet/TangemContext.java index a027d33f3b..9d3c4d5194 100644 --- a/app/src/main/java/com/tangem/wallet/TangemContext.java +++ b/app/src/main/java/com/tangem/wallet/TangemContext.java @@ -47,6 +47,9 @@ public class TangemContext { if (blockchain == Blockchain.Stellar && card.isToken()) { return Blockchain.StellarAsset; } + if (blockchain == Blockchain.Binance && card.isToken()) { + return Blockchain.BinanceAsset; + } return blockchain; } @@ -65,7 +68,7 @@ public class TangemContext { if (blockchain == Blockchain.NftToken) { return card.getTokenSymbol().substring(4) + "
" + getBlockchain().getOfficialName() + " non-fungible token"; } - if (blockchain == Blockchain.StellarAsset) { + if (blockchain == Blockchain.StellarAsset || blockchain == Blockchain.BinanceAsset) { return card.getTokenSymbol() + "
" + getBlockchain().getOfficialName() + " asset"; } if (blockchain == Blockchain.StellarTag) { diff --git a/app/src/main/java/com/tangem/wallet/binance/BinanceAssetData.java b/app/src/main/java/com/tangem/wallet/binance/BinanceAssetData.java new file mode 100644 index 0000000000..6c06d4dd75 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/binance/BinanceAssetData.java @@ -0,0 +1,55 @@ +package com.tangem.wallet.binance; + +import android.os.Bundle; +import android.util.Log; + +import com.tangem.wallet.CoinEngine; + +public class BinanceAssetData extends BinanceData { + private String assetBalance; + private String assetSymbol; + + @Override + public void loadFromBundle(Bundle B) { + super.loadFromBundle(B); + + if (B.containsKey("AssetBalance")) assetBalance = B.getString("AssetBalance"); + else assetBalance = null; + if (B.containsKey("AssetSymbol")) assetSymbol = B.getString("AssetSymbol"); + else assetSymbol = null; + } + + @Override + public void saveToBundle(Bundle B) { + super.saveToBundle(B); + try { + if (assetBalance != null) B.putString("AssetBalance", assetBalance); + if (assetSymbol != null) B.putString("AssetSymbol", assetSymbol); + } catch (Exception e) { + Log.e("Can't save to bundle ", e.getMessage()); + } + } + + @Override + public void clearInfo() { + super.clearInfo(); + assetBalance = null; + } + + @Override + public boolean hasBalanceInfo() { + return super.hasBalanceInfo() || assetBalance != null; + } + + public CoinEngine.Amount getAssetBalance() { + return assetBalance == null ? null : new CoinEngine.Amount(assetBalance, assetSymbol); + } + + public void setAssetBalance(String assetBalance) { + this.assetBalance = assetBalance; + } + + public void setAssetSymbol(String assetSymbol) { + this.assetSymbol = assetSymbol; + } +} diff --git a/app/src/main/java/com/tangem/wallet/binance/BinanceAssetEngine.java b/app/src/main/java/com/tangem/wallet/binance/BinanceAssetEngine.java new file mode 100644 index 0000000000..3bdc80e3b8 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/binance/BinanceAssetEngine.java @@ -0,0 +1,586 @@ +package com.tangem.wallet.binance; + +import android.net.Uri; +import android.text.InputFilter; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.tangem.App; +import com.tangem.data.Blockchain; +import com.tangem.data.network.BinanceApi; +import com.tangem.data.network.Server; +import com.tangem.data.network.ServerApiBinance; +import com.tangem.data.network.model.BinanceFees; +import com.tangem.tangem_card.data.TangemCard; +import com.tangem.tangem_card.reader.CardProtocol; +import com.tangem.tangem_card.tasks.SignTask; +import com.tangem.tangem_card.util.Util; +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.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.MessageType; +import com.tangem.wallet.binance.client.encoding.message.TransactionRequestAssemblerExtSign; +import com.tangem.wallet.binance.client.encoding.message.TransferMessage; + +import org.bitcoinj.core.Utils; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class BinanceAssetEngine extends CoinEngine { + private static final String TAG = BinanceAssetEngine.class.getSimpleName(); + + public BinanceAssetData coinData = null; + private BinanceDexApiRestClient client = null; + + public BinanceAssetEngine(TangemContext context) throws Exception { + super(context); + if (context.getCoinData() == null) { + coinData = new BinanceAssetData(); + context.setCoinData(coinData); + } else if (context.getCoinData() instanceof BinanceAssetData) { + coinData = (BinanceAssetData) context.getCoinData(); + } else { + throw new Exception("Invalid type of Blockchain data for " + TAG); + } + client = BinanceDexApiClientFactory.newInstance().newRestClient(BinanceDexEnvironment.PROD.getBaseUrl()); + coinData.setChainId("Binance-Chain-Tigris"); + } + + public BinanceAssetEngine() { + 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() { + return App.pendingTransactionsStorage.hasTransactions(ctx.getCard()); + } + + @Override + public String getBalanceHTML() { + Amount balance = coinData.getBalance(); + Amount assetBalance = coinData.getAssetBalance(); + if (balance != null) { + if (assetBalance != null) { + return assetBalance.toDescriptionString(getDecimals()) + "
+ " + balance.toDescriptionString(getDecimals()) + " for fee"; + } + return new Amount(BigDecimal.ZERO, ctx.getCard().tokenSymbol).toDescriptionString(getDecimals()) + "
+ " + balance.toDescriptionString(getDecimals()) + " for fee"; + } else { + return ""; + } + } + + @Override + public String getBalanceCurrency() { + if (hasBalanceInfo()) { + if (coinData.getAssetBalance() != null) { + return ctx.getCard().tokenSymbol; + } else { + return getFeeCurrency(); + } + } else { + return getFeeCurrency(); + } + } + + @Override + public boolean isBalanceNotZero() { + if (!hasBalanceInfo()) + return false; + return (coinData.getBalance() != null && coinData.getBalance().notZero()) || + (coinData.getAssetBalance() != null && coinData.getAssetBalance().notZero()); + } + + @Override + public boolean hasBalanceInfo() { + if (coinData == null) return false; + return coinData.hasBalanceInfo() || coinData.isError404(); + } + + public boolean isExtractPossible() { + if (!hasBalanceInfo()) { + ctx.setMessage(R.string.loaded_wallet_error_obtaining_blockchain_data); + } else if (!isBalanceNotZero()) { + ctx.setMessage(R.string.general_wallet_empty); + } else if (awaitingConfirmation()) { + ctx.setMessage(R.string.loaded_wallet_message_wait); + } else if (coinData.getBalance() == null || coinData.getBalance().isZero()) { + ctx.setMessage(ctx.getString(R.string.confirm_transaction_error_not_enough_bnb_for_fee)); + } 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() { + 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() { + return Uri.parse(ctx.getCoinData().getWallet()); + } + + @Override + public InputFilter[] getAmountInputFilters() { + return new InputFilter[]{new DecimalDigitsInputFilter(getDecimals())}; + } + + @Override + public boolean checkNewTransactionAmount(Amount amount) { + if (!hasBalanceInfo()) return false; + Amount balance; + try { + if (amount.getCurrency().equals(ctx.getCard().tokenSymbol)) { + balance = coinData.getAssetBalance(); + } else if (amount.getCurrency().equals(getFeeCurrency())) { + balance = coinData.getBalance(); + } else { + return false; + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } + return amount.compareTo(balance) <= 0; + } + + @Override + public boolean checkNewTransactionAmountAndFee(Amount amount, Amount fee, Boolean isFeeIncluded) { + if (!hasBalanceInfo()) return false; + + try { + Amount balance = coinData.getBalance(); + + if (fee == null || amount == null || fee.isZero() || amount.isZero()) + return false; + + + if (amount.getCurrency().equals(ctx.getCard().tokenSymbol)) { + // token transaction + if (fee.compareTo(balance) > 0) + return false; + } else if (amount.getCurrency().equals(getFeeCurrency())) { + // standard ETH transaction + if (isFeeIncluded && (amount.compareTo(balance) > 0 || fee.compareTo(balance) > 0)) + return false; + + if (!isFeeIncluded && amount.add(fee).compareTo(balance) > 0) + return false; + } else { + return false; + } + } catch (Exception e) { + e.printStackTrace(); + 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()))) { + + if (coinData.isError404()) { + balanceValidator.setScore(0); + balanceValidator.setFirstLine(R.string.balance_validator_first_line_no_account); + balanceValidator.setSecondLine(R.string.balance_validator_second_line_create_account); + } else { + balanceValidator.setScore(0); + balanceValidator.setFirstLine(R.string.balance_validator_first_line_unknown_balance); + balanceValidator.setSecondLine(R.string.balance_validator_second_line_unverified_balance); + return false; + } + } + + if (coinData.isBalanceReceived()) { + balanceValidator.setScore(100); + balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_balance); + balanceValidator.setSecondLine(R.string.balance_validator_second_line_confirmed_in_blockchain); + if (coinData.getBalance().isZero() && (coinData.getAssetBalance() == null || coinData.getAssetBalance().isZero())) { + balanceValidator.setFirstLine(R.string.balance_validator_first_line_empty_wallet); + balanceValidator.setSecondLine(R.string.empty_string); + } + } + + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + @Override + public Amount getBalance() { + if (!hasBalanceInfo()) return null; + if (coinData.getAssetBalance() != null) { + return coinData.getAssetBalance(); + } else { + 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 (!hasBalanceInfo()) { + return ""; + } + try { + if (coinData.getAssetBalance().notZero()) { + return ""; + } else { + return coinData.getBalance().toEquivalentString(coinData.getRateAlter()); + } + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } + + public String calculateAddress(byte[] pkCompressed) throws Exception { + byte[] pubKeyHash = Utils.sha256hash160(pkCompressed); + return Bech32.encode("bnb", Crypto.convertBits(pubKeyHash, 0, pubKeyHash.length, 8, 5, false)); + } + + @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) { + return Util.longToByteArray(internalAmount.longValueExact()); + } + + @Override + public CoinData createCoinData() { + return new BinanceAssetData(); + } + + @Override + public String getUnspentInputsDescription() { + return ""; + } + + @Override + public void defineWallet() throws CardProtocol.TangemException { + try { + String wallet = calculateAddress(ctx.getCard().getWalletPublicKeyRar()); + ctx.getCoinData().setWallet(wallet); + coinData.setAssetSymbol(ctx.getCard().tokenSymbol); + } 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; + + if (IncFee && amountValue.getCurrency().equals(getFeeCurrency())) { //Coin transfer only + amount = amountValue.subtract(feeValue).setScale(getDecimals(), RoundingMode.DOWN).toPlainString(); + } else { + amount = amountValue.setScale(getDecimals(), RoundingMode.DOWN).toPlainString(); + } + + byte[] pubKey = ctx.getCard().getWalletPublicKeyRar(); + byte[] pubKeyPrefix = MessageType.PubKey.getTypePrefixBytes(); + byte[] pubKeyForSign = new byte[pubKey.length + pubKeyPrefix.length + 1]; + System.arraycopy(pubKeyPrefix, 0, pubKeyForSign, 0, pubKeyPrefix.length); + pubKeyForSign[pubKeyPrefix.length] = (byte) 33; + System.arraycopy(pubKey, 0, pubKeyForSign, pubKeyPrefix.length + 1, pubKey.length); + + Transfer transfer = new Transfer(); + transfer.setCoin(amountValue.getCurrency().equals(getFeeCurrency()) ? getFeeCurrency() : ctx.getCard().getContractAddress()); + transfer.setFromAddress(ctx.getCoinData().getWallet()); + transfer.setToAddress(targetAddress); + transfer.setAmount(amount); + + TransactionOption options = TransactionOption.DEFAULT_INSTANCE; + + TransactionRequestAssemblerExtSign txAssembler = client.prepareTransfer(transfer, coinData, pubKeyForSign, 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() { + + @Override + public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) { + return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw; + } + + @Override + public byte[][] getHashesToSign() throws NoSuchAlgorithmException { + byte[][] hashForSign = new byte[1][]; + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + hashForSign[0] = digest.digest(dataForSign); + return hashForSign; + } + + @Override + public byte[] getRawDataToSign() { + return dataForSign; + } + + @Override + public String getHashAlgToSign() { + return "sha-256"; + } + + @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); + + byte[] resultSig = new byte[64]; + System.arraycopy(Utils.bigIntegerToBytes(r, 32), 0, resultSig, 0, 32); + System.arraycopy(Utils.bigIntegerToBytes(s, 32), 0, resultSig, 32, 32); + + // TransactionRequestAssembler.buildTransfer as reference + byte[] signature = txAssembler.encodeSignature(resultSig); + byte[] txForSend = txAssembler.encodeStdTx(msg, signature); + + notifyOnNeedSendTransaction(txForSend); + return txForSend; + } + }; + } + + public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) { + try { + ServerApiBinance serverApiBinance = new ServerApiBinance(); + + ServerApiBinance.ResponseListener responseListener = new ServerApiBinance.ResponseListener() { + @Override + public void onSuccess() { + blockchainRequestsCallbacks.onComplete(true); + } + + @Override + public void onFail() { + blockchainRequestsCallbacks.onComplete(false); + } + }; + + serverApiBinance.setResponseListener(responseListener); + serverApiBinance.getBalance(ctx, client); + + coinData.setValidationNodeDescription(Server.ApiBinance.URL_BINANCE); + + } catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, "FAIL Binance balance exception"); + ctx.setError(e.getMessage()); + blockchainRequestsCallbacks.onComplete(false); + } + } + + public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) { + try { + String baseUrl = Server.ApiBinance.Method.API_V1; + + 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) { + for (BinanceFees fee : response.body()) { + if (fee.getFixed_fee_params() != null) { + Long longFee = Long.valueOf(fee.getFixed_fee_params().getFee()); + Amount feeAmount = new Amount(BigDecimal.valueOf(longFee).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 { + ServerApiBinance serverApiBinance = new ServerApiBinance(); + + ServerApiBinance.ResponseListener responseListener = new ServerApiBinance.ResponseListener() { + @Override + public void onSuccess() { + ctx.setError(null); + blockchainRequestsCallbacks.onComplete(true); + } + + @Override + public void onFail() { + ctx.setError("Broadcast error"); + blockchainRequestsCallbacks.onComplete(false); + } + }; + + serverApiBinance.setResponseListener(responseListener); + serverApiBinance.sendTransaction(txForSend, client); + + } catch (Exception e) { + Log.e(TAG, e.getMessage()); + ctx.setError(e.getMessage()); + blockchainRequestsCallbacks.onComplete(false); + } + } + + @Override + public int pendingTransactionTimeoutInSeconds() { + return 9; + } + + @Override + public boolean allowSelectFeeLevel() { + return false; + } + + @Override + public boolean allowSelectFeeInclusion() { + return coinData.getAssetBalance() == null; + } + + @Override + public boolean needMultipleLinesForBalance() { + return true; + } +} 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 48ae73ef42..ca6286a35d 100644 --- a/app/src/main/java/com/tangem/wallet/binance/BinanceEngine.java +++ b/app/src/main/java/com/tangem/wallet/binance/BinanceEngine.java @@ -120,7 +120,7 @@ public class BinanceEngine extends CoinEngine { @Override public boolean hasBalanceInfo() { if (coinData == null) return false; - return coinData.hasBalanceInfo(); + return coinData.hasBalanceInfo() || coinData.isError404(); } public boolean isExtractPossible() { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a2cd5b4f1e..4077dc857d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -211,6 +211,7 @@ Cannot calculate fee! Wrong data received from the node Please wait for confirmation of incoming transaction Not enough ETH funds for fee + Not enough BNB funds for fee Not enough XLM funds for fee Not enough RBTC funds for fee Target account is not created! Send 1+ XLM to create it diff --git a/app/src/tangemAccess/java/com/tangem/ui/PrepareTransactionFragment.kt b/app/src/tangemAccess/java/com/tangem/ui/PrepareTransactionFragment.kt index 48a385a3d8..1b5fa7d2c7 100644 --- a/app/src/tangemAccess/java/com/tangem/ui/PrepareTransactionFragment.kt +++ b/app/src/tangemAccess/java/com/tangem/ui/PrepareTransactionFragment.kt @@ -154,17 +154,8 @@ class PrepareTransactionFragment : BaseFragment(), NavigationResultListener, Nfc val schemeSplit = code!!.split(":") when (schemeSplit.size) { 2 -> { - if (ctx.blockchain.officialName.toLowerCase(Locale.ROOT).replace("\\s", "") == schemeSplit[0]) { - val uri = Uri.parse(schemeSplit[1]) - etWallet?.setText(uri.path) -// val amount = uri.getQueryParameter("amount") //TODO: enable after redesign -// if (amount != null) { -// etAmount?.setText(amount) -// rgIncFee.check(R.id.rbFeeOut) -// } - } else if (ctx.blockchain == Blockchain.Ripple && schemeSplit[0] == "ripple") { - val uri = Uri.parse(schemeSplit[1]) - etWallet?.setText(uri.path) + if (schemeSplit[0] == ctx.blockchain.uriScheme) { + etWallet?.setText(schemeSplit[1]) } else { etWallet?.setText(code) } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/BinanceTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/BinanceTransactionBuilder.kt index 49c7c20368..63743abc73 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/BinanceTransactionBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/BinanceTransactionBuilder.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.blockchains.binance.client.domain.broadcast.Transfe import com.tangem.blockchain.blockchains.binance.client.encoding.message.MessageType import com.tangem.blockchain.blockchains.binance.client.encoding.message.TransactionRequestAssemblerExtSign import com.tangem.blockchain.blockchains.binance.client.encoding.message.TransferMessage +import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.extensions.Result @@ -28,13 +29,14 @@ class BinanceTransactionBuilder( private var transferMessage: TransferMessage? = null fun buildToSign(transactionData: TransactionData): Result { + val amount = transactionData.amount - if (!transactionData.amount.isAboveZero()) return Result.Failure(Exception("Transaction amount is not defined")) + if (!amount.isAboveZero()) return Result.Failure(Exception("Transaction amount is not defined")) val accountNumber = accountNumber ?: return Result.Failure(Exception("No account number")) val sequence = sequence ?: return Result.Failure(Exception("No sequence")) val transfer = Transfer() - transfer.coin = transactionData.amount.currencySymbol + transfer.coin = if (amount.type == AmountType.Coin) amount.currencySymbol else amount.address transfer.fromAddress = transactionData.sourceAddress transfer.toAddress = transactionData.destinationAddress transfer.amount = transactionData.amount.value!! diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/BinanceWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/BinanceWalletManager.kt index e8d2b759eb..4bb91c341f 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/BinanceWalletManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/BinanceWalletManager.kt @@ -1,12 +1,12 @@ package com.tangem.blockchain.blockchains.binance import android.util.Log -import com.tangem.blockchain.blockchains.binance.network.BinanceInfoResponse import com.tangem.blockchain.blockchains.binance.network.BinanceNetworkManager import com.tangem.blockchain.common.* import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.common.CompletionResult +import com.tangem.blockchain.blockchains.binance.network.BinanceInfoResponse class BinanceWalletManager( cardId: String, @@ -18,7 +18,7 @@ class BinanceWalletManager( private val blockchain = wallet.blockchain override suspend fun update() { - val result = networkManager.getInfo(wallet.address) + val result = networkManager.getInfo(wallet.address, wallet.amounts[AmountType.Token]?.address) when (result) { is Result.Success -> updateWallet(result.data) is Result.Failure -> updateError(result.error) @@ -28,6 +28,7 @@ class BinanceWalletManager( private fun updateWallet(response: BinanceInfoResponse) { Log.d(this::class.java.simpleName, "Balance is ${response.balance}") wallet.amounts[AmountType.Coin]?.value = response.balance + wallet.amounts[AmountType.Token]?.value = response.assetBalance transactionBuilder.accountNumber = response.accountNumber transactionBuilder.sequence = response.sequence diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/network/BinanceNetworkManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/network/BinanceNetworkManager.kt index 92532a3ceb..2ccdd82ca5 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/network/BinanceNetworkManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/binance/network/BinanceNetworkManager.kt @@ -25,20 +25,22 @@ class BinanceNetworkManager(isTestNet: Boolean = false) { ) } - suspend fun getInfo(address: String): Result { + suspend fun getInfo(address: String, assetCode: String? = null): Result { return try { val accountData = retryIO { client.getAccount(address) } var coinBalance = BigDecimal.ZERO + var assetBalance = BigDecimal.ZERO for (balance in accountData.balances) { - if (balance.symbol == "BNB") { - coinBalance = balance.free.toBigDecimal() - break + when (balance.symbol) { + "BNB" -> coinBalance = balance.free.toBigDecimal() + assetCode -> assetBalance = balance.free.toBigDecimal() } } Result.Success(BinanceInfoResponse( balance = coinBalance, + assetBalance = assetBalance, accountNumber = accountData.accountNumber.toLong(), sequence = accountData.sequence )) @@ -46,6 +48,7 @@ class BinanceNetworkManager(isTestNet: Boolean = false) { if (exception.message == "account not found") { Result.Success(BinanceInfoResponse( balance = BigDecimal.ZERO, //TODO check account not found logic + assetBalance = null, accountNumber = null, sequence = null )) @@ -88,7 +91,8 @@ class BinanceNetworkManager(isTestNet: Boolean = false) { } data class BinanceInfoResponse( - val balance: BigDecimal?, + val balance: BigDecimal, + val assetBalance: BigDecimal?, val accountNumber: Long?, val sequence: Long? ) \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/CardSession.kt b/tangem-core/src/main/java/com/tangem/CardSession.kt index 34f243fc4f..6021923099 100644 --- a/tangem-core/src/main/java/com/tangem/CardSession.kt +++ b/tangem-core/src/main/java/com/tangem/CardSession.kt @@ -37,7 +37,7 @@ interface CardSessionRunnable { * @property viewDelegate is an interface that allows interaction with users and shows relevant UI. * @property cardId ID, Unique Tangem card ID number. If not null, the SDK will check that you the card * with which you tapped a phone has this [cardId] and SDK will return - * the [TangemSdkError.WrongCard] otherwise. + * the [TangemSdkError.WrongCardNumber] otherwise. * @property initialMessage A custom description that will be shown at the beginning of the NFC session. * If null, a default header and text body will be used. */ @@ -77,7 +77,16 @@ class CardSession( runnable.run(this) { result -> when (result) { is CompletionResult.Success -> stop() - is CompletionResult.Failure -> stopWithError(result.error) + is CompletionResult.Failure -> { + if (result.error is TangemSdkError.ExtendedLengthNotSupported) { + if (session.environment.terminalKeys != null) { + session.environment.terminalKeys = null + startWithRunnable(runnable, callback) + return@run + } + } + stopWithError(result.error) + } } callback(result) } @@ -133,8 +142,8 @@ class CardSession( is CompletionResult.Success -> { val receivedCardId = result.data.cardId if (cardId != null && receivedCardId != cardId) { - stopWithError(TangemSdkError.WrongCard()) - callback(CompletionResult.Failure(TangemSdkError.WrongCard())) + stopWithError(TangemSdkError.WrongCardNumber()) + callback(CompletionResult.Failure(TangemSdkError.WrongCardNumber())) return@run } val allowedCardTypes = environment.cardFilter.allowedCardTypes @@ -166,6 +175,8 @@ class CardSession( * @param error An error that will be shown. */ private fun stopWithError(error: Exception) { + if (!isBusy) return + reader.closeSession() isBusy = false @@ -175,9 +186,12 @@ class CardSession( error.localizedMessage } if (error !is TangemSdkError.UserCancelled) { - Log.e("tag", "Finishing with error: $errorMessage") + Log.e(tag, "Finishing with error: $errorMessage") viewDelegate.onError(errorMessage) + } else { + Log.i(tag, "User cancelled NFC session") } + } fun send(apdu: CommandApdu, callback: (result: CompletionResult) -> Unit) { diff --git a/tangem-core/src/main/java/com/tangem/SessionEnvironment.kt b/tangem-core/src/main/java/com/tangem/SessionEnvironment.kt index e51d5e5d97..5f8734f685 100644 --- a/tangem-core/src/main/java/com/tangem/SessionEnvironment.kt +++ b/tangem-core/src/main/java/com/tangem/SessionEnvironment.kt @@ -17,10 +17,10 @@ data class SessionEnvironment( var pin1: ByteArray = DEFAULT_PIN.calculateSha256(), var pin2: ByteArray = DEFAULT_PIN2.calculateSha256(), var card: Card? = null, - val terminalKeys: KeyPair? = null, + var terminalKeys: KeyPair? = null, var encryptionMode: EncryptionMode = EncryptionMode.NONE, var encryptionKey: ByteArray? = null, - val cvc: ByteArray? = null, + var cvc: ByteArray? = null, var cardFilter: CardFilter = CardFilter(), val handleErrors: Boolean = true ) { diff --git a/tangem-core/src/main/java/com/tangem/TangemSdk.kt b/tangem-core/src/main/java/com/tangem/TangemSdk.kt index 8fc1fb4e2e..ce0c0e8a47 100644 --- a/tangem-core/src/main/java/com/tangem/TangemSdk.kt +++ b/tangem-core/src/main/java/com/tangem/TangemSdk.kt @@ -183,29 +183,51 @@ class TangemSdk( } /** - * This method launches a [WriteUserDataCommand] on a new thread. + * This method launches a [WriteUserDataCommand] on a new thread, writing UserData and UserCounter fields. * - * This command writes some of UserData, UserProtectedData, UserCounter and UserProtectedCounter fields. - * User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS. - * The App defines purpose of use, format and it's payload. For example, this field may contain cashed information + * User_Data is never changed or parsed by the executable code the Tangem COS. + * The App defines purpose of use, format and its payload. For example, this field may contain cashed information * from blockchain to accelerate preparing new transaction. - * User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing + * The initial value of User_Counter can be set by an App and increased on every signing * of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use. * For example, this fields may contain blockchain nonce value. * * Writing of UserCounter and UserData is protected only by PIN1. - * UserProtectedCounter and UserProtectedData need additionally PIN2 to confirmation. */ fun writeUserData( cardId: String, userData: ByteArray? = null, - userProtectedData: ByteArray? = null, userCounter: Int? = null, + initialMessage: Message? = null, + callback: (result: CompletionResult) -> Unit + ) { + val command = WriteUserDataCommand(userData = userData,userCounter = userCounter) + startSessionWithRunnable(command, cardId, initialMessage, callback) + } + + /** + * This method launches a [WriteUserDataCommand] on a new thread, + * writing UserProtectedData and UserProtectedCounter fields. + * + * User_ProtectedData is never changed or parsed by the executable code the Tangem COS. + * The App defines purpose of use, format and its payload. For example, this field may contain cashed information + * from blockchain to accelerate preparing new transaction. + * The initial value of User_ProtectedCounter can be set by an App and increased on every signing + * of a new transaction (on SIGN command that calculate new signatures). The App defines the purpose of use. + * For example, this fields may contain blockchain nonce value. + * + * UserProtectedCounter and UserProtectedData require PIN2 for confirmation. + */ + fun writeProtectedUserData( + cardId: String, + userProtectedData: ByteArray? = null, userProtectedCounter: Int? = null, initialMessage: Message? = null, callback: (result: CompletionResult) -> Unit ) { - val command = WriteUserDataCommand(userData, userProtectedData, userCounter, userProtectedCounter) + val command = WriteUserDataCommand( + userProtectedData = userProtectedData, userProtectedCounter = userProtectedCounter + ) startSessionWithRunnable(command, cardId, initialMessage, callback) } @@ -325,7 +347,7 @@ class TangemSdk( * @runnable: A custom task, adopting [CardSessionRunnable] protocol * @cardId: CID, Unique Tangem card ID number. If not null, the SDK will check that you the card * with which you tapped a phone has this [cardId] and SDK will return - * the [TangemSdkError.WrongCard] otherwise. + * the [TangemSdkError.WrongCardNumber] otherwise. * @initialMessage: A custom description that shows at the beginning of the NFC session. * If null, default message will be used. * @callback: Standard [TangemSdk] callback. @@ -343,7 +365,7 @@ class TangemSdk( * @cardId: CID, Unique Tangem card ID number. If not null, the SDK will check that you the card * with which you tapped a phone has this [cardId] and SDK will return - * the [TangemSdkError.WrongCard] otherwise. + * the [TangemSdkError.WrongCardNumber] otherwise. * @initialMessage: A custom description that shows at the beginning of the NFC session. * If null, default message will be used. * @callback: At first, you should check that the [TangemSdkError] is not null, diff --git a/tangem-core/src/main/java/com/tangem/TangemSdkError.kt b/tangem-core/src/main/java/com/tangem/TangemSdkError.kt index 4ea2612c51..bcf51d0617 100644 --- a/tangem-core/src/main/java/com/tangem/TangemSdkError.kt +++ b/tangem-core/src/main/java/com/tangem/TangemSdkError.kt @@ -16,6 +16,10 @@ sealed class TangemSdkError(val code: Int) : Exception(code.toString()) { * (e.g. a user detaches card from the phone's NFC module) while the NFC session is in progress. */ class TagLost : TangemSdkError(10001) + /** + * This error is returned when NFC driver on an Android device does not support sending more than 261 bytes. + */ + class ExtendedLengthNotSupported : TangemSdkError(10002) class SerializeCommandError : TangemSdkError(20001) @@ -67,16 +71,6 @@ sealed class TangemSdkError(val code: Int) : Exception(code.toString()) { //Read Errors class Pin1Required : TangemSdkError(40401) - /** - * This error is returned when a [Task] expects a user to use a particular card, - * but the user tries to use a different card. - */ - class WrongCard : TangemSdkError(40403) - /** - * This error is returned when a user scans a card of a [com.tangem.common.extensions.CardType] - * that is not specified in [Config.cardFilter]. - */ - class WrongCardType : TangemSdkError(40404) //CreateWallet Errors class AlreadyCreated : TangemSdkError(40501) @@ -128,11 +122,6 @@ sealed class TangemSdkError(val code: Int) : Exception(code.toString()) { class OverwritingDataIsProhibited : TangemSdkError(40008) class DataCannotBeWritten : TangemSdkError(40009) class MissingIssuerPubicKey : TangemSdkError(40010) - /** - * This error is returned when a [ScanTask] returns a [Card] without some of the essential fields. - */ - class CardError : TangemSdkError(40011) - //SDK Errors class UnknownError: TangemSdkError(50001) @@ -150,7 +139,20 @@ sealed class TangemSdkError(val code: Int) : Exception(code.toString()) { * is executed before performing other commands. */ class MissingPreflightRead : TangemSdkError(50004) - + /** + * This error is returned when a [Task] expects a user to use a particular card, + * but the user tries to use a different card. + */ + class WrongCardNumber : TangemSdkError(50005) + /** + * This error is returned when a user scans a card of a [com.tangem.common.extensions.CardType] + * that is not specified in [Config.cardFilter]. + */ + class WrongCardType : TangemSdkError(50006) + /** + * This error is returned when a [ScanTask] returns a [Card] without some of the essential fields. + */ + class CardError : TangemSdkError(50007) } diff --git a/tangem-core/src/main/java/com/tangem/commands/Command.kt b/tangem-core/src/main/java/com/tangem/commands/Command.kt index fad1b0d025..e7e90c5123 100644 --- a/tangem-core/src/main/java/com/tangem/commands/Command.kt +++ b/tangem-core/src/main/java/com/tangem/commands/Command.kt @@ -38,7 +38,7 @@ abstract class Command : CardSessionRunnable { abstract fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): T override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { - Log.i("Command", "Sending ${this::class.java.simpleName}") + Log.i("Command", "Initializing ${this::class.java.simpleName}") if (session.environment.handleErrors) { if (performPreCheck(session, callback)) return } diff --git a/tangem-core/src/main/java/com/tangem/common/tlv/TlvBuilder.kt b/tangem-core/src/main/java/com/tangem/common/tlv/TlvBuilder.kt index 3cade3e92a..8c7ec12fad 100644 --- a/tangem-core/src/main/java/com/tangem/common/tlv/TlvBuilder.kt +++ b/tangem-core/src/main/java/com/tangem/common/tlv/TlvBuilder.kt @@ -14,7 +14,7 @@ class TlvBuilder { fun serialize(): ByteArray { Log.v("TLV", - "List of encoded TLVs:\n${tlvs.joinToString("\n")}") + "Data encoded to TLVs:\n${tlvs.joinToString("\n")}") return tlvs.serialize() } diff --git a/tangem-core/src/main/java/com/tangem/common/tlv/TlvDecoder.kt b/tangem-core/src/main/java/com/tangem/common/tlv/TlvDecoder.kt index 62c0899f2d..866fbbd41c 100644 --- a/tangem-core/src/main/java/com/tangem/common/tlv/TlvDecoder.kt +++ b/tangem-core/src/main/java/com/tangem/common/tlv/TlvDecoder.kt @@ -20,7 +20,7 @@ class TlvDecoder(val tlvList: List) { init { Log.v("TLV", - "List of decoded TLVs:\n${tlvList.joinToString("\n")}") + "Decoding data from TLV:\n${tlvList.joinToString("\n")}") } /** @@ -33,7 +33,7 @@ class TlvDecoder(val tlvList: List) { */ inline fun decodeOptional(tag: TlvTag): T? = try { - decode(tag) + decode(tag, false) } catch (exception: TangemSdkError.DecodingFailedMissingTag) { null } @@ -47,14 +47,18 @@ class TlvDecoder(val tlvList: List) { * * @return [Tlv] value converted to a nullable type [T]. * - * @throws [TaskError.MissingTag] exception if no [Tlv] is found by the Tag. + * @throws [TangemSdkError.DecodingFailedMissingTag] exception if no [Tlv] is found by the Tag. */ - inline fun decode(tag: TlvTag): T { + inline fun decode(tag: TlvTag, logError: Boolean = true): T { val tlvValue: ByteArray = tlvList.find { it.tag == tag }?.value ?: if (tag.valueType() == TlvValueType.BoolValue && T::class == Boolean::class) { return false as T } else { - Log.e(this::class.simpleName!!, "Tag $tag not found") + if (logError) { + Log.e(this::class.simpleName!!, "TLV $tag not found") + } else { + Log.v(this::class.simpleName!!, "TLV $tag not found, but it is not required") + } throw TangemSdkError.DecodingFailedMissingTag() } diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/TestUserDataActivity.kt b/tangem-devkit/src/main/java/com/tangem/devkit/TestUserDataActivity.kt index 1fd9eeedb6..5628c01f29 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/TestUserDataActivity.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/TestUserDataActivity.kt @@ -50,9 +50,7 @@ class TestUserDataActivity : AppCompatActivity() { tangemSdk.writeUserData( writeOptions.cardId!!, writeOptions.userData, - writeOptions.userProtectedData, - writeOptions.userCounter, - writeOptions.userProtectedCounter + writeOptions.userCounter ) { when (it) { is CompletionResult.Failure -> handleError(tv_write_result, it.error) diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/_main/entryPoint/ActionListFragment.kt b/tangem-devkit/src/main/java/com/tangem/devkit/_main/entryPoint/ActionListFragment.kt index 5a1008d717..2656204d6b 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/_main/entryPoint/ActionListFragment.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/_main/entryPoint/ActionListFragment.kt @@ -71,7 +71,8 @@ class ActionListFragment : BaseFragment() { ActionType.ReadIssuerExData, ActionType.WriteIssuerExData, ActionType.ReadUserData, - ActionType.WriteUserData + ActionType.WriteUserData, + ActionType.WriteProtectedUserData ) } } \ No newline at end of file diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/extensions/AndroidComponents.kt b/tangem-devkit/src/main/java/com/tangem/devkit/extensions/AndroidComponents.kt index c24ff4494c..d7ae706d37 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/extensions/AndroidComponents.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/extensions/AndroidComponents.kt @@ -4,7 +4,6 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.content.Intent -import androidx.activity.ComponentActivity import androidx.fragment.app.Fragment /** @@ -17,11 +16,7 @@ fun Context.copyToClipboard(value: Any, label: String = "") { clipboard.setPrimaryClip(clip) } -fun Fragment.shareText(text: String) { - requireActivity().shareText(text) -} - -fun ComponentActivity.shareText(text: String) { +fun Context.shareText(text: String) { val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_TEXT, text) @@ -29,4 +24,8 @@ fun ComponentActivity.shareText(text: String) { } val shareIntent = Intent.createChooser(sendIntent, null) startActivity(shareIntent) +} + +fun Fragment.shareText(text: String) { + requireContext().shareText(text) } \ No newline at end of file diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/actions/WriteUserDataAction.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/actions/WriteUserDataAction.kt index d7d6da6107..7727546c05 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/actions/WriteUserDataAction.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/actions/WriteUserDataAction.kt @@ -11,12 +11,10 @@ class WriteUserDataAction : BaseAction() { override fun executeMainAction(payload: PayloadHolder, attrs: AttrForAction, callback: ActionCallback) { val userData = (attrs.itemList.findItem(TlvId.UserData)?.getData() as? String)?.toByteArray() ?: return - val protectedUserData = (attrs.itemList.findItem(TlvId.ProtectedUserData)?.getData() as? String)?.toByteArray() - ?: return val cardId = attrs.itemList.findItem(TlvId.CardId)?.viewModel?.data ?: return val counter = (attrs.itemList.findItem(TlvId.Counter)?.viewModel?.data as? Int) ?: 1 - attrs.tangemSdk.writeUserData(stringOf(cardId), userData, protectedUserData, counter, counter) { + attrs.tangemSdk.writeUserData(stringOf(cardId), userData, counter) { handleResult(payload, it, null, attrs, callback) } } @@ -24,7 +22,6 @@ class WriteUserDataAction : BaseAction() { override fun getActionByTag(payload: PayloadHolder, id: Id, attrs: AttrForAction): ((ActionCallback) -> Unit)? { return when (id) { TlvId.CardId -> { callback -> ScanAction().executeMainAction(payload, attrs, callback) } - TlvId.Counter -> { callback -> ReadUserDataAction().executeMainAction(payload, attrs, callback) } else -> null } } diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/actions/WriteUserProtectedDataAction.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/actions/WriteUserProtectedDataAction.kt new file mode 100644 index 0000000000..e315107b8b --- /dev/null +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/actions/WriteUserProtectedDataAction.kt @@ -0,0 +1,29 @@ +package com.tangem.devkit.ucase.domain.actions + +import com.tangem.devkit._arch.structure.Id +import com.tangem.devkit._arch.structure.PayloadHolder +import com.tangem.devkit._arch.structure.abstraction.findItem +import com.tangem.devkit.ucase.domain.paramsManager.ActionCallback +import com.tangem.devkit.ucase.variants.TlvId +import ru.dev.gbixahue.eu4d.lib.kotlin.stringOf + + +class WriteUserProtectedDataAction : BaseAction() { + override fun executeMainAction(payload: PayloadHolder, attrs: AttrForAction, callback: ActionCallback) { + val protectedUserData = (attrs.itemList.findItem(TlvId.ProtectedUserData)?.getData() as? String)?.toByteArray() + ?: return + val cardId = attrs.itemList.findItem(TlvId.CardId)?.viewModel?.data ?: return + val counter = (attrs.itemList.findItem(TlvId.Counter)?.viewModel?.data as? Int) ?: 1 + + attrs.tangemSdk.writeProtectedUserData(stringOf(cardId), protectedUserData, counter) { + handleResult(payload, it, null, attrs, callback) + } + } + + override fun getActionByTag(payload: PayloadHolder, id: Id, attrs: AttrForAction): ((ActionCallback) -> Unit)? { + return when (id) { + TlvId.CardId -> { callback -> ScanAction().executeMainAction(payload, attrs, callback) } + else -> null + } + } +} \ No newline at end of file diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/paramsManager/managers/PersonalizationItemsManager.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/paramsManager/managers/PersonalizationItemsManager.kt index 90a1ceab7b..a8125f2a51 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/paramsManager/managers/PersonalizationItemsManager.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/paramsManager/managers/PersonalizationItemsManager.kt @@ -7,10 +7,7 @@ import com.tangem.devkit.commons.Store import com.tangem.devkit.ucase.domain.actions.PersonalizeAction import com.tangem.devkit.ucase.domain.paramsManager.ActionCallback import com.tangem.devkit.ucase.variants.personalize.converter.PersonalizationConfigConverter -import com.tangem.devkit.ucase.variants.personalize.converter.PersonalizationJsonConverter import com.tangem.devkit.ucase.variants.personalize.dto.PersonalizationConfig -import com.tangem.devkit.ucase.variants.personalize.dto.PersonalizationJson -import ru.dev.gbixahue.eu4d.lib.android.global.log.Log /** [REDACTED_AUTHOR] @@ -30,27 +27,6 @@ class PersonalizationItemsManager( action.executeMainAction(this, getAttrsForAction(tangemSdk), callback) } - fun importJsonConfig(jsonString: String) { - if (jsonString.isEmpty()) return - - val jsonDto = try { - PersonalizationJson.getJsonConverter().fromJson(jsonString, PersonalizationJson::class.java) - } catch (ex: Exception) { - Log.e(this, "Can't convert imported string to Json object. Error: $ex") - return - } - - val config = PersonalizationJsonConverter().aToB(jsonDto) - updateByItemList(converter.convert(config)) - } - - fun exportJsonConfig(): String { - val config = converter.convert(itemList, PersonalizationConfig.default()) - val jsonDto = PersonalizationJsonConverter().bToA(config) - val jsonString = PersonalizationJson.getJsonConverter().toJson(jsonDto) - return jsonString - } - @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy() { val config = converter.convert(itemList, PersonalizationConfig.default()) diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/paramsManager/managers/SimpleManagers.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/paramsManager/managers/SimpleManagers.kt index a5085dccb2..bb6b16540e 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/paramsManager/managers/SimpleManagers.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/paramsManager/managers/SimpleManagers.kt @@ -96,9 +96,18 @@ class WriteUserDataItemsManager : BaseItemsManager(WriteUserDataAction()) { setItems(listOf( EditTextItem(TlvId.CardId, null), EditTextItem(TlvId.Counter, "1"), - EditTextItem(TlvId.UserData, "User data to be written on a card"), - EditTextItem(TlvId.ProtectedUserData, "Protected user data to be written on a card") - + EditTextItem(TlvId.UserData, "User data to be written on a card") )) } +} + + class WriteProtectedUserDataItemsManager : BaseItemsManager(WriteUserProtectedDataAction()) { + init { + setItemChangeConsequences(CardIdConsequence()) + setItems(listOf( + EditTextItem(TlvId.CardId, null), + EditTextItem(TlvId.Counter, "1"), + EditTextItem(TlvId.ProtectedUserData, "Protected user data to be written on a card") + )) + } } \ No newline at end of file diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/resources/Ids.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/resources/Ids.kt index 1daedb6326..90e2d54588 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/resources/Ids.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/resources/Ids.kt @@ -16,6 +16,7 @@ enum class ActionType : Id { WriteIssuerExData, ReadUserData, WriteUserData, + WriteProtectedUserData, Personalize, Depersonalize, Unknown, diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/resources/initializers/ActionResources.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/resources/initializers/ActionResources.kt index 2d7adcea1c..7ca4301b5f 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/resources/initializers/ActionResources.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/resources/initializers/ActionResources.kt @@ -24,6 +24,7 @@ class ActionResources { holder.register(ActionType.WriteIssuerExData, ActionRes(R.string.action_issuer_write_ex_data, R.string.info_action_issuer_write_ex_data, R.id.action_nav_entry_point_to_nav_issuer_write_ex_data)) holder.register(ActionType.ReadUserData, ActionRes(R.string.action_user_read_data, R.string.info_action_user_read_data, R.id.action_nav_entry_point_to_nav_user_read_data)) holder.register(ActionType.WriteUserData, ActionRes(R.string.action_user_write_data, R.string.info_action_user_write_data, R.id.action_nav_entry_point_to_nav_user_write_data)) + holder.register(ActionType.WriteProtectedUserData, ActionRes(R.string.action_user_write_protected_data, R.string.info_action_user_write_protected_data, R.id.action_nav_entry_point_to_nav_user_write_protected_data)) // holder.register(ActionType.Unknown, getIfNotContains()) } } \ No newline at end of file diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/DefaultValues.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/DefaultValues.kt index 7135d43975..4af4f57691 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/DefaultValues.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/DefaultValues.kt @@ -106,8 +106,8 @@ internal class Helper { KeyValue("RSK", "RSK"), KeyValue("XPR", "XPR"), KeyValue("CARDANO", "CARDANO"), - KeyValue("BNB", "BNB"), - KeyValue("XTZ", "XTZ"), + KeyValue("BNB", "BINANCE"), + KeyValue("XTZ", "TEZOS"), KeyValue("DUC", "DUC") ) } diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/PersonalizationConfigConverter.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/PersonalizationConfigConverter.kt index e6733ed9f4..ce10098ef1 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/PersonalizationConfigConverter.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/PersonalizationConfigConverter.kt @@ -71,9 +71,9 @@ class ItemsToPersonalizationConfig : ItemsToModel { export.requireTerminalTxSignature = getTyped(SignHashExPropId.RequireTerminalTxSig) export.checkPIN3onCard = getTyped(SignHashExPropId.CheckPin3) export.itsToken = getTyped(TokenId.ItsToken) - export.cardData.token_symbol = getTyped(TokenId.Symbol) - export.cardData.token_contract_address = getTyped(TokenId.ContractAddress) - export.cardData.token_decimal = getTyped(TokenId.Decimal) + export.cardData.token_symbol = getTypedUnsafe(TokenId.Symbol) + export.cardData.token_contract_address = getTypedUnsafe(TokenId.ContractAddress) + export.cardData.token_decimal = getTypedUnsafe(TokenId.Decimal) export.cardData = export.cardData.apply { this.product_note = getTyped(ProductMaskId.Note) } export.cardData = export.cardData.apply { this.product_tag = getTyped(ProductMaskId.Tag) } export.cardData = export.cardData.apply { this.product_id_card = getTyped(ProductMaskId.IdCard) } @@ -114,6 +114,10 @@ class ItemsToPersonalizationConfig : ItemsToModel { return getTypedBy(valuesHolder, id)!! } + private inline fun getTypedUnsafe(id: Id): Type? { + return getTypedBy(valuesHolder, id) + } + private inline fun getTypedBy(holder: ConfigValuesHolder, id: Id): Type? { Log.d(this, "getTyped for id: $id") var typedValue = holder.get(id)?.get() diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/PersonalizationJsonConverter.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/PersonalizationJsonConverter.kt index 322f1643b6..a2590ebb3c 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/PersonalizationJsonConverter.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/PersonalizationJsonConverter.kt @@ -78,7 +78,7 @@ internal class JsonToConfig : Converter(R.id.et_json_export) val btnExportJson = parent.findViewById