diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index 782234d4ed..234548d273 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -26,7 +26,9 @@ public enum Blockchain { Stellar("XLM", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar"), StellarTestNet("XLM/test", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar Testnet"), StellarAsset("Asset", "XLM", 10000000.0, R.drawable.ic_logo_stellar, "Stellar"), - Eos("EOS", "EOS", 10000.0, R.drawable.tangem2, "EOS"); + StellarTag("XLM-Tag", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar"), + Eos("EOS", "EOS", 10000.0, R.drawable.tangem2, "EOS"), + Ducatus("DUC", "DUC", 100000000.0, R.drawable.tangem2, "Ducatus"); Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) { mID = ID; diff --git a/app/src/main/java/com/tangem/data/network/InsightApi.java b/app/src/main/java/com/tangem/data/network/InsightApi.java index 63eb570f1c..4095326dc5 100644 --- a/app/src/main/java/com/tangem/data/network/InsightApi.java +++ b/app/src/main/java/com/tangem/data/network/InsightApi.java @@ -2,6 +2,7 @@ package com.tangem.data.network; import com.tangem.data.network.model.InsightBody; import com.tangem.data.network.model.InsightResponse; +import com.tangem.data.network.model.InsightUtxo; import java.util.List; @@ -18,7 +19,7 @@ public interface InsightApi { Call insightAddress(@Path("address") String address); @GET(ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS) - Call> insightUnspent(@Path("address") String address); + Call> insightUnspent(@Path("address") String address); @GET(ServerApiInsight.INSIGHT_TRANSACTION) Call insightTransaction(@Path("txId") String txId); diff --git a/app/src/main/java/com/tangem/data/network/ServerApiInsight.java b/app/src/main/java/com/tangem/data/network/ServerApiInsight.java index e3eae3384a..5a768471e6 100644 --- a/app/src/main/java/com/tangem/data/network/ServerApiInsight.java +++ b/app/src/main/java/com/tangem/data/network/ServerApiInsight.java @@ -6,6 +6,7 @@ import androidx.annotation.NonNull; import com.tangem.data.network.model.InsightBody; import com.tangem.data.network.model.InsightResponse; +import com.tangem.data.network.model.InsightUtxo; import java.util.List; @@ -18,11 +19,11 @@ import retrofit2.converter.gson.GsonConverterFactory; public class ServerApiInsight { private static String TAG = ServerApiInsight.class.getSimpleName(); - public static final String INSIGHT_ADDRESS = "/addr/{address}"; - public static final String INSIGHT_UNSPENT_OUTPUTS = "/addr/{address}/utxo"; - public static final String INSIGHT_TRANSACTION = "/rawtx/{txId}"; - public static final String INSIGHT_FEE = "/utils/estimatefee?nbBlocks=2,3,6"; - public static final String INSIGHT_SEND = "/tx/send"; + public static final String INSIGHT_ADDRESS = "addr/{address}"; + public static final String INSIGHT_UNSPENT_OUTPUTS = "addr/{address}/utxo"; + public static final String INSIGHT_TRANSACTION = "rawtx/{txId}"; + public static final String INSIGHT_FEE = "utils/estimatefee?nbBlocks=2,3,6"; + public static final String INSIGHT_SEND = "tx/send"; private int requestsCount = 0; @@ -38,7 +39,7 @@ public class ServerApiInsight { public interface ResponseListener { void onSuccess(String method, InsightResponse insightResponse); - void onSuccess(String method, List utxoList); + void onSuccess(String method, List utxoList); void onFail(String method, String message); } @@ -49,7 +50,7 @@ public class ServerApiInsight { public void requestData(String method, String wallet, String tx) { requestsCount++; - String insightURL = "http://130.185.109.17:3001/insigth-api"; //TODO: make random selection + String insightURL = "https://insight.ducatus.io/insight-lite-api/"; //TODO: make random selection this.lastNode = insightURL; //TODO: show node instead of URL Retrofit retrofitInsight = new Retrofit.Builder() @@ -61,10 +62,10 @@ public class ServerApiInsight { InsightApi insightApi = retrofitInsight.create(InsightApi.class); if (method.equals(INSIGHT_UNSPENT_OUTPUTS)) { - Call> call = insightApi.insightUnspent(wallet); - call.enqueue(new Callback>() { + Call> call = insightApi.insightUnspent(wallet); + call.enqueue(new Callback>() { @Override - public void onResponse(@NonNull Call> call, @NonNull Response> response) { + public void onResponse(@NonNull Call> call, @NonNull Response> response) { requestsCount--; if (response.code() == 200) { @@ -77,7 +78,7 @@ public class ServerApiInsight { } @Override - public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { requestsCount--; responseListener.onFail(method, String.valueOf(t.getMessage())); Log.e(TAG, "requestData " + method + " onFailure " + t.getMessage()); @@ -92,13 +93,9 @@ public class ServerApiInsight { call = insightApi.insightAddress(wallet); break; - case INSIGHT_TRANSACTION: - call = insightApi.insightTransaction(tx); - break; - - case INSIGHT_FEE: - call = insightApi.insightFee(); - break; +// case INSIGHT_FEE: +// call = insightApi.insightFee(); +// break; case INSIGHT_SEND: call = insightApi.insightSend(new InsightBody(tx)); diff --git a/app/src/main/java/com/tangem/data/network/ServerApiStellar.java b/app/src/main/java/com/tangem/data/network/ServerApiStellar.java index f364820eb1..77427e3fc4 100644 --- a/app/src/main/java/com/tangem/data/network/ServerApiStellar.java +++ b/app/src/main/java/com/tangem/data/network/ServerApiStellar.java @@ -33,7 +33,7 @@ import io.reactivex.schedulers.Schedulers; public class ServerApiStellar { public ServerApiStellar(Blockchain blockchain) { - if (blockchain == Blockchain.Stellar || blockchain == Blockchain.StellarAsset) { + if (blockchain == Blockchain.Stellar || blockchain == Blockchain.StellarAsset || blockchain == Blockchain.StellarTag) { currentURL = ServerURL.API_STELLAR; } else { currentURL = ServerURL.API_STELLAR_TESTNET; @@ -173,10 +173,11 @@ public class ServerApiStellar { stellarRequest.setError(null); try { Server server; - if (ctx.getBlockchain() == Blockchain.Stellar || ctx.getBlockchain() == Blockchain.StellarAsset) { + Blockchain blockchain = ctx.getBlockchain(); + if (blockchain == Blockchain.Stellar || blockchain == Blockchain.StellarAsset || blockchain == Blockchain.StellarTag) { Network.usePublicNetwork(); server = new Server(currentURL); - } else if (ctx.getBlockchain() == Blockchain.StellarTestNet) { + } else if (blockchain == Blockchain.StellarTestNet) { Network.useTestNetwork(); server = new Server(currentURL); } else { diff --git a/app/src/main/java/com/tangem/data/network/model/InsightResponse.kt b/app/src/main/java/com/tangem/data/network/model/InsightResponse.kt index 4910b36695..a16c535649 100644 --- a/app/src/main/java/com/tangem/data/network/model/InsightResponse.kt +++ b/app/src/main/java/com/tangem/data/network/model/InsightResponse.kt @@ -12,27 +12,29 @@ data class InsightResponse( @SerializedName("addrStr") var addrStr: String = "", +// @SerializedName("2") +// var fee2: String = "", +// +// @SerializedName("3") +// var fee3: String = "", +// +// @SerializedName("6") +// var fee6: String = "", + + @SerializedName("error") + var error: String = "" +) + +data class InsightUtxo( @SerializedName("txid") var txid: String = "", @SerializedName("satoshis") var satoshis: Long? = null, - @SerializedName("height") - var height: Int? = null, + @SerializedName("vout") + var vout: Int? = null, - @SerializedName("2") - var fee2: String = "", - - @SerializedName("3") - var fee3: String = "", - - @SerializedName("6") - var fee6: String = "", - - @SerializedName("rawtx") - var rawtx: String = "", - - @SerializedName("error") - var error: String = "" + @SerializedName("scriptPubKey") + var scriptPubKey: String? = null ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt index a8b9b4ac02..cecb0ddeee 100644 --- a/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt @@ -7,10 +7,13 @@ import android.net.Uri import android.nfc.NfcAdapter import android.nfc.Tag import android.nfc.tech.IsoDep +import android.nfc.tech.Ndef +import android.nfc.tech.NfcV import android.os.Bundle import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan +import android.util.Log import android.view.* import android.widget.PopupMenu import android.widget.TextView @@ -20,9 +23,13 @@ import androidx.core.os.bundleOf import androidx.lifecycle.ViewModelProviders import com.tangem.App import com.tangem.Constant +import com.tangem.data.Blockchain import com.tangem.data.Logger import com.tangem.tangem_card.data.TangemCard import com.tangem.tangem_card.reader.CardProtocol +import com.tangem.tangem_card.reader.TLV +import com.tangem.tangem_card.reader.TLVException +import com.tangem.tangem_card.reader.TLVList import com.tangem.tangem_card.tasks.CustomReadCardTask import com.tangem.tangem_card.tasks.ReadCardInfoTask import com.tangem.tangem_sdk.android.data.PINStorage @@ -45,10 +52,16 @@ import com.tangem.wallet.BuildConfig import com.tangem.wallet.CoinEngineFactory import com.tangem.wallet.R import com.tangem.wallet.TangemContext +import com.tangem.wallet.xlmtag.XlmTagEngine import kotlinx.android.synthetic.main.fragment_main.* import kotlinx.android.synthetic.main.layout_touch_card.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import java.io.File import java.util.* +import kotlin.coroutines.CoroutineContext class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.ReaderCallback, CardProtocol.Notifications, androidx.appcompat.widget.PopupMenu.OnMenuItemClickListener, @@ -68,6 +81,11 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader private var zipFile: File? = null private var unknownBlockchain = false + private val parentJob = Job() + private val coroutineContext: CoroutineContext + get() = parentJob + Dispatchers.IO + private val scope = CoroutineScope(coroutineContext) + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { setHasOptionsMenu(true) return super.onCreateView(inflater, container, savedInstanceState) @@ -152,6 +170,8 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader return } + parseNfcvTag(tag) + try { // get IsoDep handle and run cardReader thread val isoDep = IsoDep.get(tag) @@ -175,6 +195,77 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader } } + private fun parseNfcvTag(tag: Tag) { + if (NfcV.get(tag) != null) { + if (Ndef.get(tag) != null) { + try { + onNdefDiscovered(Ndef.get(tag), tag.id) + } catch (e: Exception) { + e.printStackTrace() + (activity as MainActivity).nfcManager.notifyReadResult(false) + } + return + } else { + (activity as MainActivity).nfcManager.notifyReadResult(false) + return + } + } + } + + private fun onNdefDiscovered(ndef: Ndef, uid: ByteArray) { + scope.launch { + + + try { + ndef.connect() + val records = ndef.ndefMessage.records + for (record in records) { + if (record.toUri() != null) { + when (record.toUri().toString()) { + "vnd.android.nfc://ext/tangem.com:wallet" -> { + //mConsole.write(Util.bytesToHex(record.getPayload()), MessageAdapter.MSG_OKAY, "", null, false); + val payload = record.payload + Log.v(TAG, "tangem.com:wallet[${payload.size} bytes]:") + try { + val tlvNDEF: TLVList = TLVList.fromBytes(Arrays.copyOfRange(payload, 2, payload.size)) + val cardDataTlv = TLVList.fromBytes((tlvNDEF.getTLV(TLV.Tag.TAG_CardData)).Value) + Log.v(TAG, "\n" + tlvNDEF.getParsedTLVs("")) + val card = TangemCard(uid.toString()) + card.batch = cardDataTlv.getTLV(TLV.Tag.TAG_Batch).asHexString + card.setIssuer(cardDataTlv.getTLV(TLV.Tag.TAG_Issuer_ID).Value.toString(), null) + card.blockchainID = Blockchain.StellarTag.id + card.walletPublicKey = tlvNDEF.getTLV(TLV.Tag.TAG_Wallet_PublicKey).Value + card.status = TangemCard.Status.Loaded + card.tagSignature = tlvNDEF.getTLV(TLV.Tag.TAG_Signature).Value + + val ctx = TangemContext(card) + val engineCoin = XlmTagEngine(ctx) + engineCoin.defineWallet() + launch(Dispatchers.Main) { + + val bundle = Bundle() + bundle.putParcelable(Constant.EXTRA_LAST_DISCOVERED_TAG, lastTag) + ctx.saveToBundle(bundle) + navigateForResult(Constant.REQUEST_CODE_SHOW_CARD_ACTIVITY, + R.id.action_main_to_tagFragment, bundle) + } + } catch (e: TLVException) { + e.printStackTrace() + Log.v(TAG, e.message) + } + } + else -> Log.v(TAG, record.toUri().toString()) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + (activity as MainActivity).nfcManager.notifyReadResult(false) + } + + } + } + override fun onReadStart(cardProtocol: CardProtocol) { rlProgressBar?.post { rlProgressBar?.visibility = View.VISIBLE } } diff --git a/app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt b/app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt new file mode 100644 index 0000000000..dd2d27594c --- /dev/null +++ b/app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt @@ -0,0 +1,214 @@ +package com.tangem.ui.fragment.additional + +import android.app.Activity +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.text.Html +import android.view.View +import android.widget.Toast +import androidx.core.content.ContextCompat +import com.tangem.data.network.ServerApiCommon +import com.tangem.server_android.ServerApiTangem +import com.tangem.ui.fragment.BaseFragment +import com.tangem.ui.fragment.wallet.LoadedWalletFragment +import com.tangem.ui.fragment.wallet.LoadedWalletViewModel +import com.tangem.ui.navigation.NavigationResultListener +import com.tangem.util.LOG +import com.tangem.util.UtilHelper +import com.tangem.wallet.* +import com.tangem.wallet.xlmtag.XlmTagEngine +import kotlinx.android.synthetic.main.fr_loaded_wallet.* +import kotlinx.android.synthetic.main.layout_btn_details.* +import kotlinx.android.synthetic.main.layout_tangem_card.* + + +class TagFragment : BaseFragment(), NavigationResultListener { + override val layoutId = R.layout.fragment_tag + private lateinit var viewModel: LoadedWalletViewModel + private lateinit var ctx: TangemContext + private var serverApiCommon: ServerApiCommon = ServerApiCommon() + private var serverApiTangem: ServerApiTangem = ServerApiTangem() + + private var requestCounter: Int = 0 + set(value) { + field = value + LOG.i(LoadedWalletFragment.TAG, "requestCounter, set $field") + if (field <= 0) { + LOG.e(LoadedWalletFragment.TAG, "+++++++++++ FINISH REFRESH") + if (srl != null && srl.isRefreshing) + srl.isRefreshing = false + } else if (srl != null && !srl.isRefreshing) + srl.isRefreshing = true + } + + override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ctx = TangemContext.loadFromBundle(context, arguments) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val engine = XlmTagEngine(ctx) + + + btnLoad.visibility = View.GONE + btnDetails.visibility = View.GONE + btnExtract.text = getString(R.string.tag_claim) + btnExtract.isEnabled = false //TODO: enable when we implement extraction + btnExtract.backgroundTintList = + ContextCompat.getColorStateList(requireContext(), R.color.btn_dark) + + ivTangemCard.setImageResource(R.drawable.card_default_nft) + + tvBalance.setSingleLine(!engine.needMultipleLinesForBalance()) + tvWallet.text = ctx.coinData.wallet + tvWallet.setOnClickListener { shareWallet() } + btnExplore.setOnClickListener { startActivity(Intent(Intent.ACTION_VIEW, engine.walletExplorerUri)) } + btnCopy.setOnClickListener { shareWallet() } + btnNewScan.setOnClickListener { navigateUp() } + srl?.setOnRefreshListener { refresh(true) } + + requestBalanceAndUnspentTransactions() +// update() + } + + + private fun shareWallet() { + val txtShare = ctx.coinData.wallet + val clipboard = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.primaryClip = ClipData.newPlainText(txtShare, txtShare) + Toast.makeText(activity, R.string.loaded_wallet_toast_copied, Toast.LENGTH_LONG).show() + } + + + private fun update() { + ctx.coinData.setIsBalanceEqual(true) + + if (srl.isRefreshing) { + tvBalanceLine1.setTextColor(resources.getColor(R.color.primary)) + tvBalanceLine1.text = getString(R.string.loaded_wallet_verifying_in_blockchain) + tvBalanceLine2.text = "" + tvBalance.text = "" + tvBalanceEquivalent.text = "" + } else { + val validator = BalanceValidator() + validator.check(ctx, false) + context?.let { ContextCompat.getColor(it, validator.color) }?.let { tvBalanceLine1?.setTextColor(it) } + tvBalanceLine1?.text = getString(validator.firstLine) + tvBalanceLine2?.text = getString(validator.getSecondLine(false)) + } + + val engine = CoinEngineFactory.create(ctx) + when { + engine!!.hasBalanceInfo() -> { + @Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + Html.fromHtml(engine.balanceHTML, Html.FROM_HTML_MODE_LEGACY) + else + Html.fromHtml(engine.balanceHTML) + tvBalance.text = html + tvBalanceEquivalent.text = engine.balanceEquivalent + } + + ctx.card?.offlineBalance != null -> { + @Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + Html.fromHtml(engine.offlineBalanceHTML, Html.FROM_HTML_MODE_LEGACY) + else + Html.fromHtml(engine.offlineBalanceHTML) + tvBalance.text = html + } + + else -> tvBalance.text = "" + } + + if (ctx.card!!.tokenSymbol.length > 1) { + @Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + Html.fromHtml(ctx.blockchainName, Html.FROM_HTML_MODE_LEGACY) + else + Html.fromHtml(ctx.blockchainName) + tvBlockchain.text = html + } else + tvBlockchain.text = ctx.blockchainName + + } + + private fun requestBalanceAndUnspentTransactions() { + if (UtilHelper.isOnline(context as Activity)) { + val coinEngine = CoinEngineFactory.create(ctx) + requestCounter++ + coinEngine!!.requestBalanceAndUnspentTransactions( + object : CoinEngine.BlockchainRequestsCallbacks { + + override fun onComplete(success: Boolean) { + LOG.i(TAG, "requestBalanceAndUnspentTransactions onComplete: $success, request counter $requestCounter") + if (activity == null) return + requestCounter-- + if (!success) { + LOG.e(TAG, "requestBalanceAndUnspentTransactions ctx.error: " + ctx.error) + } + update() + } + + override fun onProgress() { + if (activity == null) return + LOG.i(TAG, "requestBalanceAndUnspentTransactions onProgress") +// update() + } + + override fun allowAdvance(): Boolean { + return try { + context?.let { UtilHelper.isOnline(it) }!! + } catch (e: KotlinNullPointerException) { + e.printStackTrace() + false + } + } + } + ) + } else { + ctx.error = getString(R.string.general_error_no_connection) + update() + } + } + + private fun refresh(clearData: Boolean = true) { + if (ctx.card == null) return + + // clear all card data and request again + ctx.coinData.clearInfo() + + if (clearData) { + ctx.error = null + ctx.message = null + } + + LOG.w(TAG, "============= START REFRESH") + requestCounter = 0 + srl?.isRefreshing = true + + update() + + ctx.coinData.setIsBalanceEqual(true) + + + requestBalanceAndUnspentTransactions() + + if (requestCounter == 0) { + // if no connection and no requests posted + srl?.isRefreshing = false + update() + } + } + + companion object { + val TAG: String = TagFragment::class.java.simpleName + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt index 10aaf161f1..5df7370dce 100644 --- a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt +++ b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt @@ -1,23 +1,24 @@ package com.tangem.wallet import android.util.Log - -import com.tangem.wallet.btc.BtcEngine -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.eos.EosEngine +import com.tangem.wallet.bch.BtcCashEngine import com.tangem.wallet.binance.BinanceEngine +import com.tangem.wallet.btc.BtcEngine import com.tangem.wallet.cardano.CardanoData import com.tangem.wallet.cardano.CardanoEngine +import com.tangem.wallet.ducatus.DucatusEngine +import com.tangem.wallet.eos.EosEngine +import com.tangem.wallet.eth.EthEngine import com.tangem.wallet.ltc.LtcEngine import com.tangem.wallet.matic.MaticTokenEngine import com.tangem.wallet.nftToken.NftTokenEngine import com.tangem.wallet.rsk.RskEngine import com.tangem.wallet.rsk.RskTokenEngine +import com.tangem.wallet.token.TokenEngine import com.tangem.wallet.xlm.XlmAssetEngine import com.tangem.wallet.xlm.XlmEngine +import com.tangem.wallet.xlmtag.XlmTagEngine import com.tangem.wallet.xrp.XrpEngine /** @@ -48,7 +49,9 @@ object CoinEngineFactory { Blockchain.Matic, Blockchain.MaticTestNet -> MaticTokenEngine() Blockchain.StellarTestNet, Blockchain.Stellar -> XlmEngine() Blockchain.StellarAsset -> XlmAssetEngine() + Blockchain.StellarTag -> XlmTagEngine() Blockchain.Eos -> EosEngine() + Blockchain.Ducatus -> DucatusEngine() else -> null } } @@ -84,8 +87,12 @@ object CoinEngineFactory { XlmEngine(context) else if (Blockchain.StellarAsset == context.blockchain) XlmAssetEngine(context) + else if (Blockchain.StellarTag == context.blockchain) + XlmTagEngine(context) else if (Blockchain.Eos == context.blockchain) EosEngine(context) + else if (Blockchain.Ducatus == context.blockchain) + DucatusEngine(context) else return null } catch (e: Exception) { diff --git a/app/src/main/java/com/tangem/wallet/Transaction.java b/app/src/main/java/com/tangem/wallet/Transaction.java index 0d4e54fd51..07dc9b42e5 100644 --- a/app/src/main/java/com/tangem/wallet/Transaction.java +++ b/app/src/main/java/com/tangem/wallet/Transaction.java @@ -566,7 +566,7 @@ public final class Transaction { public static Script buildOutput(String address) throws BitcoinException { //noinspection TryWithIdenticalCatches byte[] addressWithCheckSumAndNetworkCode = Base58.decodeBase58(address); - if (addressWithCheckSumAndNetworkCode[0] == 0 || addressWithCheckSumAndNetworkCode[0] == 111 || addressWithCheckSumAndNetworkCode[0] == 48) { //0 for BTC/BCH 1 address | 48 for LTC L address + if (addressWithCheckSumAndNetworkCode[0] == 0 || addressWithCheckSumAndNetworkCode[0] == 111 || addressWithCheckSumAndNetworkCode[0] == 48 || addressWithCheckSumAndNetworkCode[0] == 49) { //0 for BTC/BCH 1 address | 48 for LTC L address | 49 for Ducatus return buildOutputP2H(address); } @@ -601,7 +601,7 @@ public final class Transaction { //noinspection TryWithIdenticalCatches try { byte[] addressWithCheckSumAndNetworkCode = Base58.decodeBase58(address); - if (addressWithCheckSumAndNetworkCode[0] != 0 && addressWithCheckSumAndNetworkCode[0] != 111 && addressWithCheckSumAndNetworkCode[0] != 48) { + if (addressWithCheckSumAndNetworkCode[0] != 0 && addressWithCheckSumAndNetworkCode[0] != 111 && addressWithCheckSumAndNetworkCode[0] != 48 && addressWithCheckSumAndNetworkCode[0] != 49) { throw new BitcoinException(BitcoinException.ERR_UNSUPPORTED, "Unknown address type", address); } diff --git a/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java b/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java index 03a2d3c2ce..2acdefb4bc 100644 --- a/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java +++ b/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java @@ -24,6 +24,7 @@ import com.tangem.wallet.TangemContext; import com.tangem.wallet.Transaction; import com.tangem.wallet.UnspentOutputInfo; import com.tangem.wallet.btc.BtcData; +import com.tangem.wallet.btc.Unspents; import org.json.JSONArray; import org.json.JSONException; @@ -424,7 +425,15 @@ public class BtcCashEngine extends CoinEngine { @Override public String getUnspentInputsDescription() { - return coinData.getUnspentInputsDescription(); + Unspents unspents = coinData.getUnspentInputsDescription(); + if (unspents == null) { + return ""; + } else { + return String.format( + ctx.getContext().getString(R.string.details_unspents_number), + unspents.getUnspetns(), + unspents.getGatheredUnspents()); + } } @Override 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 de90f5c65a..bfe3b630cf 100644 --- a/app/src/main/java/com/tangem/wallet/binance/BinanceData.java +++ b/app/src/main/java/com/tangem/wallet/binance/BinanceData.java @@ -53,7 +53,7 @@ public class BinanceData extends CoinData { } public CoinEngine.Amount getBalance() { - return new CoinEngine.Amount(balance, "BNB"); + return balance == null ? null : new CoinEngine.Amount(balance, "BNB"); } public void setBalance(String balance) { diff --git a/app/src/main/java/com/tangem/wallet/btc/BtcData.java b/app/src/main/java/com/tangem/wallet/btc/BtcData.java index 71c1d7c8f1..0d0eca1d85 100644 --- a/app/src/main/java/com/tangem/wallet/btc/BtcData.java +++ b/app/src/main/java/com/tangem/wallet/btc/BtcData.java @@ -22,18 +22,19 @@ public class BtcData extends CoinData { //for blockchain.info private boolean hasUnconfirmed = false; - public String getUnspentInputsDescription() { + public Unspents getUnspentInputsDescription() { try { int gatheredUnspents = 0; - if (unspentTransactions == null) return ""; + if (unspentTransactions == null) return null; for (int i = 0; i < unspentTransactions.size(); i++) { if (unspentTransactions.get(i).script != null && unspentTransactions.get(i).script.length() > 1) gatheredUnspents++; } - return unspentTransactions.size() + " unspents (" + gatheredUnspents + " received)"; + + return new Unspents(unspentTransactions.size(), gatheredUnspents); } catch (Exception e) { e.printStackTrace(); - return ""; + return null; } } diff --git a/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java b/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java index 021ce73e38..fd49ed7ec4 100644 --- a/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java +++ b/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java @@ -437,7 +437,15 @@ public class BtcEngine extends CoinEngine { @Override public String getUnspentInputsDescription() { - return coinData.getUnspentInputsDescription(); + Unspents unspents = coinData.getUnspentInputsDescription(); + if (unspents == null) { + return ""; + } else { + return String.format( + ctx.getContext().getString(R.string.details_unspents_number), + unspents.getUnspetns(), + unspents.getGatheredUnspents()); + } } @Override diff --git a/app/src/main/java/com/tangem/wallet/btc/Unspents.kt b/app/src/main/java/com/tangem/wallet/btc/Unspents.kt new file mode 100644 index 0000000000..12029683e3 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/btc/Unspents.kt @@ -0,0 +1,3 @@ +package com.tangem.wallet.btc + +data class Unspents(val unspetns: Int, val gatheredUnspents: Int) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/cardano/CardanoData.java b/app/src/main/java/com/tangem/wallet/cardano/CardanoData.java index e3ba66dc73..44e69eed30 100644 --- a/app/src/main/java/com/tangem/wallet/cardano/CardanoData.java +++ b/app/src/main/java/com/tangem/wallet/cardano/CardanoData.java @@ -105,7 +105,7 @@ public class CardanoData extends CoinData { } public CoinEngine.InternalAmount getBalanceInInternalUnits() { - return new CoinEngine.InternalAmount(BigDecimal.valueOf(balance),"Lovelace"); + return balance == null ? null : new CoinEngine.InternalAmount(BigDecimal.valueOf(balance),"Lovelace"); } public void setBalance(Long balance) { diff --git a/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java b/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java index e108b9373b..17e8b9789c 100644 --- a/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java +++ b/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java @@ -7,6 +7,7 @@ import android.util.Log; import com.tangem.App; import com.tangem.data.network.ServerApiInsight; import com.tangem.data.network.model.InsightResponse; +import com.tangem.data.network.model.InsightUtxo; import com.tangem.tangem_card.data.TangemCard; import com.tangem.tangem_card.reader.CardProtocol; import com.tangem.tangem_card.tasks.SignTask; @@ -24,11 +25,11 @@ import com.tangem.wallet.Transaction; import com.tangem.wallet.UnspentOutputInfo; import com.tangem.wallet.btc.BtcData; import com.tangem.wallet.btc.BtcEngine; +import com.tangem.wallet.btc.Unspents; import java.io.ByteArrayOutputStream; import java.math.BigDecimal; import java.math.BigInteger; -import java.math.RoundingMode; import java.nio.ByteBuffer; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; @@ -48,7 +49,7 @@ public class DucatusEngine extends BtcEngine { } else if (context.getCoinData() instanceof BtcData) { coinData = (BtcData) context.getCoinData(); } else { - throw new Exception("Invalid type of Blockchain data for LtcEngine"); + throw new Exception("Invalid type of Blockchain data for DucatusEngine"); } } @@ -83,7 +84,7 @@ public class DucatusEngine extends BtcEngine { @Override public String getBalanceCurrency() { - return "LTC"; + return "DUC"; } @Override @@ -118,7 +119,7 @@ public class DucatusEngine extends BtcEngine { @Override public String getFeeCurrency() { - return "LTC"; + return "DUC"; } @Override @@ -168,16 +169,12 @@ public class DucatusEngine extends BtcEngine { @Override public Uri getWalletExplorerUri() { - return Uri.parse("https://live.blockcypher.com/ltc/address/" + ctx.getCoinData().getWallet()); + return Uri.parse("https://insight.ducatus.io/insight/address/" + ctx.getCoinData().getWallet()); } @Override public Uri getShareWalletUri() { - if (ctx.getCard().getDenomination() != null) { - return Uri.parse("litecoin:" + ctx.getCoinData().getWallet() + "?amount=" + convertToAmount(convertToInternalAmount(ctx.getCard().getDenomination())).toValueString(8)); - } else { - return Uri.parse("litecoin:" + ctx.getCoinData().getWallet()); - } + return Uri.parse(ctx.getCoinData().getWallet()); } @Override @@ -317,7 +314,7 @@ public class DucatusEngine extends BtcEngine { @Override public String calculateAddress(byte[] pkUncompressed) throws NoSuchProviderException, NoSuchAlgorithmException { - byte netSelectionByte = (byte) 0x30; + byte netSelectionByte = (byte) 0x31; byte hash1[] = Util.calculateSHA256(pkUncompressed); byte hash2[] = Util.calculateRIPEMD160(hash1); @@ -381,23 +378,35 @@ public class DucatusEngine extends BtcEngine { @Override public String getUnspentInputsDescription() { - return coinData.getUnspentInputsDescription(); + Unspents unspents = coinData.getUnspentInputsDescription(); + if (unspents == null) { + return ""; + } else { + return String.format( + ctx.getContext().getString(R.string.details_unspents_number), + unspents.getUnspetns(), + unspents.getGatheredUnspents()); + } } @Override public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception { - final ArrayList unspentOutputs; + ArrayList unspentOutputs = new ArrayList<>(); checkBlockchainDataExists(); String myAddress = ctx.getCoinData().getWallet(); byte[] pbKey = ctx.getCard().getWalletPublicKey(); - // Build script for our address - List rawTxList = coinData.getUnspentTransactions(); - byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(myAddress).bytes; +// // Build script for our address +// List rawTxList = coinData.getUnspentTransactions(); +// byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(myAddress).bytes; +// +// // Collect unspent +// unspentOutputs = BTCUtils.getOutputs(rawTxList, outputScriptWeAreAbleToSpend); - // Collect unspent - unspentOutputs = BTCUtils.getOutputs(rawTxList, outputScriptWeAreAbleToSpend); + for (BtcData.UnspentTransaction utxo : coinData.getUnspentTransactions()) { + unspentOutputs.add(new UnspentOutputInfo(BTCUtils.fromHex(utxo.txID), new Transaction.Script(BTCUtils.fromHex(utxo.script)), utxo.amount, utxo.outputN, -1, utxo.txID, null)); + } long fullAmount = 0; for (int i = 0; i < unspentOutputs.size(); ++i) { @@ -413,8 +422,8 @@ public class DucatusEngine extends BtcEngine { change = change - fees; } - final long amountFinal=amount; - final long changeFinal=change; + final long amountFinal = amount; + final long changeFinal = change; if (amount + fees > fullAmount) { throw new CardProtocol.TangemException_WrongAmount(String.format("Balance (%d) < change (%d) + amount (%d)", fullAmount, change, amount)); @@ -422,7 +431,7 @@ public class DucatusEngine extends BtcEngine { final byte[][] txForSign = new byte[unspentOutputs.size()][]; final byte[][] bodyDoubleHash = new byte[unspentOutputs.size()][]; - final byte[][] bodyHash= new byte[unspentOutputs.size()][]; + final byte[][] bodyHash = new byte[unspentOutputs.size()][]; for (int i = 0; i < unspentOutputs.size(); ++i) { txForSign[i] = BTCUtils.buildTXForSign(myAddress, targetAddress, myAddress, unspentOutputs, i, amount, change); @@ -434,13 +443,14 @@ public class DucatusEngine extends BtcEngine { @Override public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) { - return signingMethod==TangemCard.SigningMethod.Sign_Hash || signingMethod==TangemCard.SigningMethod.Sign_Raw; + return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw; } @Override public byte[][] getHashesToSign() throws Exception { - byte[][] dataForSign=new byte[unspentOutputs.size()][]; - if (txForSign.length > 10) throw new Exception("To much hashes in one transaction!"); + byte[][] dataForSign = new byte[unspentOutputs.size()][]; + if (txForSign.length > 10) + throw new Exception("To much hashes in one transaction!"); for (int i = 0; i < unspentOutputs.size(); ++i) { dataForSign[i] = bodyDoubleHash[i]; } @@ -479,7 +489,7 @@ public class DucatusEngine extends BtcEngine { unspentOutputs.get(i).scriptForBuild = DerEncodingUtil.packSignDer(r, s, pbKey); } - byte[] txForSend=BTCUtils.buildTXForSend(targetAddress, myAddress, unspentOutputs, amountFinal, changeFinal); + byte[] txForSend = BTCUtils.buildTXForSend(targetAddress, myAddress, unspentOutputs, amountFinal, changeFinal); notifyOnNeedSendTransaction(txForSend); return txForSend; } @@ -493,39 +503,20 @@ public class DucatusEngine extends BtcEngine { ServerApiInsight.ResponseListener responseListener = new ServerApiInsight.ResponseListener() { @Override public void onSuccess(String method, InsightResponse insightResponse) { - switch (method) { - case ServerApiInsight.INSIGHT_ADDRESS: { - try { - String walletAddress = insightResponse.getAddrStr(); - if (!walletAddress.equals(coinData.getWallet())) { - // todo - check - throw new Exception("Invalid wallet address in answer!"); - } - coinData.setBalanceReceived(true); - coinData.setBalanceConfirmed(insightResponse.getBalanceSat()); - coinData.setBalanceUnconfirmed(insightResponse.getUnconfirmedBalanceSat()); - coinData.setValidationNodeDescription(ServerApiInsight.lastNode); - } - catch (Exception e) { - e.printStackTrace(); - Log.e(TAG, "FAIL INSIGHT_ADDRESS Exception"); - } - } - break; - case ServerApiInsight.INSIGHT_TRANSACTION: { - try { - String raw = insightResponse.getRawtx(); - String txHash = new String(BTCUtils.reverse(CryptoUtil.doubleSha256(BTCUtils.fromHex(raw)))); //TODO: check - for (BtcData.UnspentTransaction tx : coinData.getUnspentTransactions()) { - if (tx.txID.equals(txHash)) - tx.script = raw; - } - } catch (Exception e) { - e.printStackTrace(); - } + try { + String walletAddress = insightResponse.getAddrStr(); + if (!walletAddress.equals(coinData.getWallet())) { + // todo - check + throw new Exception("Invalid wallet address in answer!"); } - break; + coinData.setBalanceReceived(true); + coinData.setBalanceConfirmed(insightResponse.getBalanceSat()); + coinData.setBalanceUnconfirmed(insightResponse.getUnconfirmedBalanceSat()); + coinData.setValidationNodeDescription(ServerApiInsight.lastNode); + } catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, "FAIL INSIGHT_ADDRESS Exception"); } if (serverApiInsight.isRequestsSequenceCompleted()) { @@ -535,30 +526,27 @@ public class DucatusEngine extends BtcEngine { } } - public void onSuccess(String method, List utxoList) { - // case ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS: TODO: check method - try { - coinData.getUnspentTransactions().clear(); - for (InsightResponse utxo : utxoList) { - BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction(); - trUnspent.txID = utxo.getTxid(); - trUnspent.amount = utxo.getSatoshis(); - trUnspent.outputN = utxo.getHeight(); - coinData.getUnspentTransactions().add(trUnspent); - } - - for (InsightResponse utxo : utxoList) { - //if (height != -1) { TODO: check - if (blockchainRequestsCallbacks.allowAdvance()) { - serverApiInsight.requestData(ServerApiInsight.INSIGHT_TRANSACTION, "", utxo.getTxid()); - } else { - ctx.setError("Terminated by user"); - } - } - } catch (Exception e) { - e.printStackTrace(); + public void onSuccess(String method, List utxoList) { + // case ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS: TODO: check method + try { + coinData.getUnspentTransactions().clear(); + for (InsightUtxo utxo : utxoList) { + BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction(); + trUnspent.txID = utxo.getTxid(); + trUnspent.amount = utxo.getSatoshis(); + trUnspent.outputN = utxo.getVout(); + trUnspent.script = utxo.getScriptPubKey(); + coinData.getUnspentTransactions().add(trUnspent); } + } catch (Exception e) { + e.printStackTrace(); + } + if (serverApiInsight.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError()); + } else { + blockchainRequestsCallbacks.onProgress(); + } } @Override @@ -582,65 +570,71 @@ public class DucatusEngine extends BtcEngine { public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) throws Exception { final int calcSize = calculateEstimatedTransactionSize(targetAddress, amount.toValueString()); Log.e(TAG, String.format("Estimated tx size %d", calcSize)); - coinData.minFee=null; - coinData.maxFee=null; - coinData.normalFee=null; - - final ServerApiInsight serverApiInsight = new ServerApiInsight(); - - final ServerApiInsight.ResponseListener responseListener = new ServerApiInsight.ResponseListener() { - @Override - public void onSuccess(String method, InsightResponse insightResponse) { - if ( method.equals(ServerApiInsight.INSIGHT_FEE)) { - try { - BigDecimal minFee = new BigDecimal(insightResponse.getFee2()); //fee per KB - BigDecimal normalFee = new BigDecimal(insightResponse.getFee3()); - BigDecimal maxFee = new BigDecimal(insightResponse.getFee6()); - - if (minFee.equals(BigDecimal.ZERO) || normalFee.equals(BigDecimal.ZERO) || maxFee.equals(BigDecimal.ZERO)) { - serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "",""); - } - - minFee = minFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); // (per KB -> per byte)*size - normalFee = normalFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); - maxFee = maxFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); - -// //compare fee to usual relay fee TODO: check if needed after we get access to Ducatus network -// if (fee.compareTo(relayFee) < 0) { -// fee = relayFee; +// coinData.minFee=null; +// coinData.maxFee=null; +// coinData.normalFee=null; +// +// final ServerApiInsight serverApiInsight = new ServerApiInsight(); +// +// final ServerApiInsight.ResponseListener responseListener = new ServerApiInsight.ResponseListener() { +// @Override +// public void onSuccess(String method, InsightResponse insightResponse) { +// if ( method.equals(ServerApiInsight.INSIGHT_FEE)) { +// try { +// BigDecimal minFee = new BigDecimal(insightResponse.getFee2()); //fee per KB +// BigDecimal normalFee = new BigDecimal(insightResponse.getFee3()); +// BigDecimal maxFee = new BigDecimal(insightResponse.getFee6()); +// +// if (minFee.equals(BigDecimal.ZERO) || normalFee.equals(BigDecimal.ZERO) || maxFee.equals(BigDecimal.ZERO)) { +// serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "",""); // } - minFee = minFee.setScale(8, RoundingMode.DOWN); - normalFee = normalFee.setScale(8, RoundingMode.DOWN); - maxFee = maxFee.setScale(8, RoundingMode.DOWN); +// +// minFee = minFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); // (per KB -> per byte)*size +// normalFee = normalFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); +// maxFee = maxFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); +// +//// //compare fee to usual relay fee TODO: check if needed after we get access to Ducatus network +//// if (fee.compareTo(relayFee) < 0) { +//// fee = relayFee; +//// } +// minFee = minFee.setScale(8, RoundingMode.DOWN); +// normalFee = normalFee.setScale(8, RoundingMode.DOWN); +// maxFee = maxFee.setScale(8, RoundingMode.DOWN); +// +// coinData.minFee = new Amount(minFee, ctx.getBlockchain().getCurrency()); +// coinData.normalFee = new Amount(normalFee, ctx.getBlockchain().getCurrency()); +// coinData.maxFee = new Amount(maxFee, ctx.getBlockchain().getCurrency()); +// +// blockchainRequestsCallbacks.onComplete(true); +// +// } catch (Exception e) { +// e.printStackTrace(); +// } +// } +// } +// +// @Override +// public void onSuccess (String method, List utxoList) { +// Log.e(TAG, "Wrong response body, InsightResponse expected"); +// } +// +// @Override +// public void onFail(String method, String message) { +// if (!serverApiInsight.isRequestsSequenceCompleted()) { +// ctx.setError(message); +// blockchainRequestsCallbacks.onComplete(false); +// } +// } +// }; +// serverApiInsight.setResponseListener(responseListener); +// +// serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "", ""); TODO: fee api returns -1 now - coinData.minFee = new Amount(minFee, ctx.getBlockchain().getCurrency()); - coinData.normalFee = new Amount(normalFee, ctx.getBlockchain().getCurrency()); - coinData.maxFee = new Amount(maxFee, ctx.getBlockchain().getCurrency()); + coinData.minFee = new Amount(BigDecimal.valueOf(calcSize).multiply(BigDecimal.valueOf(0.00000089)), ctx.getBlockchain().getCurrency()); //fee for byte from Ducatus wallet for android + coinData.normalFee = new Amount(BigDecimal.valueOf(calcSize).multiply(BigDecimal.valueOf(0.00000144)), ctx.getBlockchain().getCurrency()); + coinData.maxFee = new Amount(BigDecimal.valueOf(calcSize).multiply(BigDecimal.valueOf(0.00000350)), ctx.getBlockchain().getCurrency()); - blockchainRequestsCallbacks.onComplete(true); - - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - @Override - public void onSuccess (String method, List utxoList) { - Log.e(TAG, "Wrong response body, InsightResponse expected"); - } - - @Override - public void onFail(String method, String message) { - if (!serverApiInsight.isRequestsSequenceCompleted()) { - ctx.setError(message); - blockchainRequestsCallbacks.onComplete(false); - } - } - }; - serverApiInsight.setResponseListener(responseListener); - - serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "", ""); + blockchainRequestsCallbacks.onComplete(true); } @Override @@ -657,7 +651,7 @@ public class DucatusEngine extends BtcEngine { if (resultString.isEmpty()) { ctx.setError("No response from node"); blockchainRequestsCallbacks.onComplete(false); - }else { // TODO: Make check for a valid send response + } else { // TODO: Make check for a valid send response ctx.setError(null); blockchainRequestsCallbacks.onComplete(true); } @@ -676,7 +670,7 @@ public class DucatusEngine extends BtcEngine { } @Override - public void onSuccess (String method, List utxoList) { + public void onSuccess(String method, List utxoList) { Log.e(TAG, "Wrong response body, InsightResponse expected"); } diff --git a/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java b/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java index a2a6abc339..7402e658d9 100644 --- a/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java +++ b/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java @@ -31,6 +31,7 @@ import com.tangem.wallet.Transaction; import com.tangem.wallet.UnspentOutputInfo; import com.tangem.wallet.btc.BtcData; import com.tangem.wallet.btc.BtcEngine; +import com.tangem.wallet.btc.Unspents; import org.json.JSONException; @@ -389,7 +390,15 @@ public class LtcEngine extends BtcEngine { @Override public String getUnspentInputsDescription() { - return coinData.getUnspentInputsDescription(); + Unspents unspents = coinData.getUnspentInputsDescription(); + if (unspents == null) { + return ""; + } else { + return String.format( + ctx.getContext().getString(R.string.details_unspents_number), + unspents.getUnspetns(), + unspents.getGatheredUnspents()); + } } @Override diff --git a/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagData.kt b/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagData.kt new file mode 100644 index 0000000000..bdd8f4e3af --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagData.kt @@ -0,0 +1,133 @@ +package com.tangem.wallet.xlmtag + +import android.os.Bundle +import android.util.Log +import com.tangem.wallet.CoinData +import com.tangem.wallet.CoinEngine +import com.tangem.wallet.CoinEngine.InternalAmount +import org.stellar.sdk.KeyPair +import org.stellar.sdk.responses.AccountResponse +import org.stellar.sdk.responses.LedgerResponse +import java.math.BigDecimal + + +class XlmTagData : CoinData() { + class AccountResponseEx internal constructor(accountId: String?, sequenceNumber: Long?) : AccountResponse(KeyPair.fromAccountId(accountId), sequenceNumber) + + private var balance: CoinEngine.Amount? = null + private var sequenceNumber: Long? = 0L + private var baseReserve: CoinEngine.Amount? = CoinEngine.Amount("0.5", "XLM") + var baseFee: CoinEngine.Amount? = CoinEngine.Amount("0.00001", "XLM") + private set + var isError404 = false + var isTargetAccountCreated = false + var fundsFromTrustedSource = false + var fundsSentToTrustedSource = false + + + override fun clearInfo() { + super.clearInfo() + balance = null + isError404 = false + isTargetAccountCreated = false + fundsFromTrustedSource = false + fundsSentToTrustedSource = false + } + + fun getBalance(): CoinEngine.Amount? { + return if (balance != null) { + CoinEngine.Amount(balance!!.subtract(reserve), "XLM") + } else { + null + } + } + + val reserve: CoinEngine.Amount + get() = CoinEngine.Amount(baseReserve!!.multiply(BigDecimal.valueOf(2)), "XLM") + + var accountResponse: AccountResponse + get() = XlmTagData.AccountResponseEx(wallet, sequenceNumber) + set(accountResponse) { + if (accountResponse.balances.size > 0) { + val balanceResponse = accountResponse.balances[0] + balance = CoinEngine.Amount(balanceResponse.balance, "XLM") + } + sequenceNumber = accountResponse.sequenceNumber + isBalanceReceived = true + } + + fun setLedgerResponse(ledgerResponse: LedgerResponse) { + val xlmEngine = XlmTagEngine() + baseReserve = xlmEngine.convertToAmount(InternalAmount(ledgerResponse.baseReserveInStroops, "stroops")) + baseFee = xlmEngine.convertToAmount(InternalAmount(ledgerResponse.baseFeeInStroops, "stroops")) + } + + fun incSequenceNumber() { + sequenceNumber = sequenceNumber?.inc() + } + + override fun loadFromBundle(B: Bundle) { + super.loadFromBundle(B) + balance = if (B.containsKey("BalanceCurrency") && B.containsKey("BalanceDecimal")) { + CoinEngine.Amount(B.getString("BalanceDecimal"), B.getString("BalanceCurrency")) + } else { + null + } + sequenceNumber = if (B.containsKey("sequenceNumber")) { + B.getLong("sequenceNumber") + } else { + 0L + } + baseReserve = if (B.containsKey("BaseReserveCurrency") && B.containsKey("BaseReserveDecimal")) { + CoinEngine.Amount(B.getString("BaseReserveDecimal"), B.getString("BaseReserveCurrency")) + } else { + CoinEngine.Amount("0.5", "XLM") + } + baseFee = if (B.containsKey("BaseFeeCurrency") && B.containsKey("BaseFeeDecimal")) { + CoinEngine.Amount(B.getString("BaseFeeDecimal"), B.getString("BaseFeeCurrency")) + } else { + CoinEngine.Amount("0.00001", "XLM") + } + if (B.containsKey("Error404")) isError404 = B.getBoolean("Error404") else isError404 = false + if (B.containsKey("TargetAccountCreated")) isTargetAccountCreated = B.getBoolean("TargetAccountCreated") else isTargetAccountCreated = false + fundsFromTrustedSource = if (B.containsKey("FundsFromTrustedSource")) { + B.getBoolean("FundsFromTrustedSource") + } else { + false + } + fundsSentToTrustedSource = if (B.containsKey("FundsSentToTrustedSource")) { + B.getBoolean("FundsSentToTrustedSource") + } else { + false + } + + } + + override fun saveToBundle(B: Bundle) { + super.saveToBundle(B) + try { + if (balance != null) { + B.putString("BalanceCurrency", balance!!.currency) + B.putString("BalanceDecimal", balance!!.toValueString()) + } + if (sequenceNumber != null) { + B.putLong("sequenceNumber", sequenceNumber!!) + } + if (baseReserve != null) { + B.putString("BaseReserveCurrency", baseReserve!!.currency) + B.putString("BaseReserveDecimal", baseReserve!!.toValueString()) + } + if (baseFee != null) { + B.putString("BaseFeeCurrency", baseFee!!.currency) + B.putString("BaseFeeDecimal", baseFee!!.toValueString()) + } + if (isError404) B.putBoolean("Error404", true) + if (isTargetAccountCreated) B.putBoolean("TargetAccountCreated", true) + if (fundsFromTrustedSource) B.putBoolean("FundsFromTrustedSource", true) + if (fundsSentToTrustedSource) B.putBoolean("FundsSentToTrustedSource", true) + + } catch (e: Exception) { + Log.e("Can't save to bundle ", e.message) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagEngine.kt b/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagEngine.kt new file mode 100644 index 0000000000..4fe5de60ca --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/xlmtag/XlmTagEngine.kt @@ -0,0 +1,471 @@ +package com.tangem.wallet.xlmtag + +import android.net.Uri +import android.os.StrictMode +import android.text.InputFilter +import android.util.Log +import com.tangem.App +import com.tangem.data.Blockchain +import com.tangem.data.network.ServerApiStellar +import com.tangem.data.network.StellarRequest +import com.tangem.data.network.StellarRequest.Ledgers +import com.tangem.data.network.StellarRequest.SubmitTransaction +import com.tangem.tangem_card.data.TangemCard +import com.tangem.tangem_card.tasks.SignTask +import com.tangem.tangem_card.util.Util +import com.tangem.util.DecimalDigitsInputFilter +import com.tangem.wallet.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.stellar.sdk.* +import org.stellar.sdk.requests.RequestBuilder +import org.stellar.sdk.responses.operations.OperationResponse +import org.stellar.sdk.responses.operations.PaymentOperationResponse +import java.io.IOException +import java.math.BigDecimal + + +class XlmTagEngine : CoinEngine { + var coinData: XlmTagData? = null + val operations = mutableListOf() + + constructor(context: TangemContext) : super(context) { + if (context.coinData == null) { + coinData = XlmTagData() + context.coinData = coinData + } else if (context.coinData is XlmTagData) { + coinData = context.coinData as XlmTagData + } else { + throw Exception("Invalid type of Blockchain data for XlmEngine") + } + } + + constructor() : super() {} + + @Throws(Exception::class) + private fun checkBlockchainDataExists() { + if (coinData == null) throw Exception("No blockchain data") + } + + override fun awaitingConfirmation(): Boolean { + return App.pendingTransactionsStorage.hasTransactions(ctx.card) + } + + override fun getBalanceHTML(): String { + return if (hasBalanceInfo() && isBalanceNotZero && coinData!!.fundsFromTrustedSource) { + ctx.getString(R.string.tag_genuine) + } else if (coinData!!.fundsSentToTrustedSource) { + ctx.getString(R.string.tag_claimed) + } else { + ctx.getString(R.string.tag_not_genuine) + } + } + + override fun getBalanceCurrency(): String { + return "XLM" + } + + override fun isBalanceNotZero(): Boolean { + if (coinData == null) return false + return if (coinData!!.getBalance() == null) false else coinData!!.getBalance()!!.notZero() + } + + override fun hasBalanceInfo(): Boolean { + return if (coinData == null) false else coinData!!.getBalance() != null || coinData!!.isError404 + } + + override fun isExtractPossible(): Boolean { + 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 { + return true + } + return false + } + + override fun getFeeCurrency(): String { + return "XLM" + } + + override fun validateAddress(address: String): Boolean { + try { + val kp = KeyPair.fromAccountId(address) + } catch (e: Exception) { + return false + } + return true + } + + override fun isNeedCheckNode(): Boolean { + return false + } + + override fun getWalletExplorerUri(): Uri { + return Uri.parse("https://stellar.expert/explorer/public/account/" + ctx.coinData.wallet) + } + + override fun getShareWalletUri(): Uri { + return if (ctx.card.denomination != null) { + Uri.parse(ctx.coinData.wallet + "?amount=" + convertToAmount(convertToInternalAmount(ctx.card.denomination)!!).toValueString()) + } else { + Uri.parse(ctx.coinData.wallet) + } + } + + override fun getAmountInputFilters(): Array { + return arrayOf(DecimalDigitsInputFilter(decimals)) + } + + override fun checkNewTransactionAmount(amount: Amount): Boolean { + return true + } + + override fun checkNewTransactionAmountAndFee(amountValue: Amount, feeValue: Amount, isIncludeFee: Boolean): Boolean { + return true + } + + override fun validateBalance(balanceValidator: BalanceValidator): Boolean { + return try { + if (ctx.card.offlineBalance == null && !ctx.coinData.isBalanceReceived || !ctx.coinData.isBalanceReceived && ctx.card.remainingSignatures != ctx.card.maxSignatures) { + if (coinData!!.isError404) { + balanceValidator.setScore(0) + balanceValidator.firstLine = R.string.balance_validator_first_line_no_account + balanceValidator.setSecondLine(R.string.balance_validator_second_line_create_account_instruction) + } else { + balanceValidator.setScore(0) + balanceValidator.firstLine = R.string.balance_validator_first_line_unknown_balance + balanceValidator.setSecondLine(R.string.balance_validator_second_line_unverified_balance) + return false + } + } + + if (coinData!!.isBalanceReceived && coinData!!.isBalanceEqual) { + balanceValidator.setScore(100) + balanceValidator.firstLine = R.string.balance_validator_first_line_verified_balance + balanceValidator.setSecondLine(R.string.balance_validator_second_line_confirmed_in_blockchain) + if (coinData!!.getBalance()!!.isZero) { + balanceValidator.firstLine = R.string.balance_validator_first_line_empty_wallet + balanceValidator.setSecondLine(R.string.empty_string) + } + } + + if (ctx.card.offlineBalance != null && !coinData!!.isBalanceReceived && ctx.card.remainingSignatures == ctx.card.maxSignatures && coinData!!.getBalance()!!.notZero()) { + balanceValidator.setScore(80) + balanceValidator.firstLine = R.string.balance_validator_first_line_verified_offline + balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance) + } + + true + } catch (e: Exception) { + e.printStackTrace() + false + } + } + + override fun getBalance(): Amount? { + return if (!hasBalanceInfo()) null else coinData!!.getBalance()!! + } + + override fun evaluateFeeEquivalent(fee: String): String { + return if (!coinData!!.amountEquivalentDescriptionAvailable) "" else try { + val feeAmount = Amount(fee, feeCurrency) + feeAmount.toEquivalentString(coinData!!.rate.toDouble()) + } catch (e: Exception) { + "" + } + } + + override fun getBalanceEquivalent(): String { + if (coinData == null || !coinData!!.amountEquivalentDescriptionAvailable) return "" + val balance = balance ?: return "" + return balance.toEquivalentString(coinData!!.rate.toDouble()) + } + + override fun calculateAddress(pkUncompressed: ByteArray): String { + val kp = KeyPair.fromPublicKey(pkUncompressed) + return kp.accountId + } + + override fun convertToAmount(internalAmount: InternalAmount): Amount { + val d = internalAmount.divide(multiplier) + return Amount(d, balanceCurrency) + } + + override fun convertToAmount(strAmount: String, currency: String): Amount { + return Amount(strAmount, currency) + } + + override fun convertToInternalAmount(amount: Amount): InternalAmount { + val d = amount.multiply(multiplier) + return InternalAmount(d, "stroops") + } + + override fun convertToInternalAmount(bytes: ByteArray): InternalAmount? { + if (bytes == null) return null + val reversed = ByteArray(bytes.size) + for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1] + return InternalAmount(Util.byteArrayToLong(reversed), "stroops") + } + + override fun convertToByteArray(internalAmount: InternalAmount): ByteArray { + val bytes = Util.longToByteArray(internalAmount.longValueExact()) + val reversed = ByteArray(bytes.size) + for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1] + return reversed + } + + override fun createCoinData(): CoinData { + return XlmTagData() + } + + override fun getUnspentInputsDescription(): String { + return "" + } + + @Throws(Exception::class) + override fun constructTransaction(amountValue: Amount, feeValue: Amount, IncFee: Boolean, targetAddress: String): SignTask.TransactionToSign { + var amountValue = amountValue + checkBlockchainDataExists() + val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() + StrictMode.setThreadPolicy(policy) + if (IncFee) { + amountValue = Amount(amountValue.subtract(feeValue), amountValue.currency) + } + val operation: Operation + operation = if (coinData!!.isTargetAccountCreated) PaymentOperation.Builder(KeyPair.fromAccountId(targetAddress), AssetTypeNative(), amountValue.toValueString()).build() else CreateAccountOperation.Builder(KeyPair.fromAccountId(targetAddress), amountValue.toValueString()).build() + val transaction = TransactionEx.buildEx(60, coinData!!.accountResponse, operation) + if (transaction.fee != convertToInternalAmount(feeValue).intValueExact()) { + throw Exception("Invalid fee!") + } + return object : SignTask.TransactionToSign { + override fun isSigningMethodSupported(signingMethod: TangemCard.SigningMethod): Boolean { + return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw + } + + @Throws(Exception::class) + override fun getHashesToSign(): Array { + val dataForSign = arrayOf(transaction.hash()) + return dataForSign + } + + @Throws(Exception::class) + override fun getRawDataToSign(): ByteArray { + return transaction.signatureBase() + } + + override fun getHashAlgToSign(): String { + return "sha-256" + } + + @Throws(Exception::class) + override fun getIssuerTransactionSignature(dataToSignByIssuer: ByteArray): ByteArray { + throw Exception("Issuer validation not supported!") + } + + @Throws(Exception::class) + override fun onSignCompleted(signFromCard: ByteArray): ByteArray { // Sign the transaction to prove you are actually the person sending it. + transaction.setSign(signFromCard) + val txForSend = transaction.toEnvelopeXdrBase64().toByteArray() + notifyOnNeedSendTransaction(txForSend) + return txForSend + } + } + } + + private fun checkTargetAccountCreated(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, targetAddress: String, amount: Amount) { + val serverApi = ServerApiStellar(ctx.blockchain) + val listener: ServerApiStellar.Listener = object : ServerApiStellar.Listener { + override fun onSuccess(request: StellarRequest.Base) { + coinData!!.isTargetAccountCreated = true + blockchainRequestsCallbacks.onComplete(true) + } + + override fun onFail(request: StellarRequest.Base) { + Log.i(TAG, "onFail: " + request.javaClass.simpleName + " " + request.error) + if (request.errorResponse != null && request.errorResponse.code == 404) { + coinData!!.isTargetAccountCreated = false + if (amount.compareTo(coinData!!.reserve) >= 0) { + blockchainRequestsCallbacks.onComplete(true) + } else { + ctx.setError(R.string.confirm_transaction_error_not_enough_xlm_for_create) + blockchainRequestsCallbacks.onComplete(false) + } + } else { // suppose account is created if anything goes wrong + coinData!!.isTargetAccountCreated = true + blockchainRequestsCallbacks.onComplete(true) + } + } + } + serverApi.setListener(listener) + serverApi.requestData(ctx, StellarRequest.Balance(targetAddress)) + } + + private fun requestPayments(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) { + val server = Server("https://horizon.stellar.org/") + val accountKeyPair = KeyPair.fromAccountId(coinData!!.wallet) + + try { + var operationsPage = server.payments().forAccount(accountKeyPair).order(RequestBuilder.Order.DESC).execute() + Log.e("Stellar", operationsPage.records.toString()) + + operations.addAll(operationsPage.records) + + while (operations.size < OPERATIONS_LIMIT) { + operationsPage = operationsPage.getNextPage(server.httpClient) + operations.addAll(operationsPage.records) + Log.e("Stellar", operationsPage.records.toString()) + Log.e("Stellar", "Operations downloaded: " + operationsPage.records.count()) + Log.e("Stellar", "Operations overall: " + operations.count()) + } + + parsePayments(blockchainRequestsCallbacks) + } catch (e: Exception) { + if (e.message != null) { + ctx.error = e.message + blockchainRequestsCallbacks.onComplete(false) + } else { + ctx.error = e.javaClass.name + blockchainRequestsCallbacks.onComplete(false) + } + } + } + + private fun parsePayments(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) { + coinData!!.fundsFromTrustedSource = operations.find { it is PaymentOperationResponse && it.from.accountId == TRUSTED_SOURCE } != null + coinData!!.fundsSentToTrustedSource = + (operations.find { it is PaymentOperationResponse && it.to.accountId == TRUSTED_DESTINATION } != null) && coinData!!.fundsFromTrustedSource + Log.e("Stellar", + "fundsFromTrustedSource $coinData!!.fundsFromTrustedSource, " + + "fundsSentToTrustedSource $coinData!!.fundsSentToTrustedSource") + blockchainRequestsCallbacks.onComplete(!ctx.hasError()) + } + + override fun requestBalanceAndUnspentTransactions(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) { + CoroutineScope(Dispatchers.IO).launch { requestPayments(blockchainRequestsCallbacks) } + val serverApi = ServerApiStellar(Blockchain.Stellar) + val listener: ServerApiStellar.Listener = object : ServerApiStellar.Listener { + override fun onSuccess(request: StellarRequest.Base) { + Log.i(TAG, "onSuccess: " + request.javaClass.simpleName) + if (request is StellarRequest.Balance) { + coinData!!.accountResponse = request.accountResponse + coinData!!.validationNodeDescription = serverApi.currentURL + + blockchainRequestsCallbacks.onProgress() + } else if (request is Ledgers) { + coinData!!.setLedgerResponse(request.ledgerResponse) + if (serverApi.isRequestsSequenceCompleted) { + } else { + blockchainRequestsCallbacks.onProgress() + } + } else { + ctx.error = "Invalid request logic" + blockchainRequestsCallbacks.onComplete(false) + } + } + + override fun onFail(request: StellarRequest.Base) { + Log.i(TAG, "onFail: " + request.javaClass.simpleName + " " + request.error) + if (request.errorResponse != null && request.errorResponse.code == 404) { + coinData!!.isError404 = true + } else { + ctx.error = request.error + } + if (serverApi.isRequestsSequenceCompleted) { + if (ctx.hasError()) { + blockchainRequestsCallbacks.onComplete(false) + } else { + blockchainRequestsCallbacks.onComplete(true) + } + } else { + blockchainRequestsCallbacks.onProgress() + } + } + } + serverApi.setListener(listener) + serverApi.requestData(ctx, StellarRequest.Balance(coinData!!.wallet)) + serverApi.requestData(ctx, Ledgers()) + } + + @Throws(Exception::class) + override fun requestFee(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, targetAddress: String, amount: Amount) { + coinData!!.maxFee = coinData!!.baseFee + coinData!!.normalFee = coinData!!.maxFee + coinData!!.minFee = coinData!!.normalFee + checkTargetAccountCreated(blockchainRequestsCallbacks, targetAddress, amount) + } + + @Throws(IOException::class) + override fun requestSendTransaction(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, txForSend: ByteArray) { + val serverApi = ServerApiStellar(ctx.blockchain) + val listener: ServerApiStellar.Listener = object : ServerApiStellar.Listener { + override fun onSuccess(request: StellarRequest.Base) { + try { + if (!SubmitTransaction::class.java.isInstance(request)) throw Exception("Invalid request logic") + val submitTransactionRequest = request as SubmitTransaction + if (submitTransactionRequest.response.isSuccess) { + ctx.error = null + blockchainRequestsCallbacks.onComplete(true) + } else { + if (submitTransactionRequest.response.extras != null && submitTransactionRequest.response.extras.resultCodes != null) { + var trResult = submitTransactionRequest.response.extras.resultCodes.transactionResultCode + if (submitTransactionRequest.response.extras.resultCodes.operationsResultCodes != null && submitTransactionRequest.response.extras.resultCodes.operationsResultCodes.size > 0) { + trResult += "/" + submitTransactionRequest.response.extras.resultCodes.operationsResultCodes[0] + } + ctx.error = trResult + } else { + ctx.error = "transaction failed" + } + blockchainRequestsCallbacks.onComplete(false) + } + } catch (e: Exception) { + if (e.message != null) { + ctx.error = e.message + blockchainRequestsCallbacks.onComplete(false) + } else { + ctx.error = e.javaClass.name + blockchainRequestsCallbacks.onComplete(false) + } + } + } + + override fun onFail(request: StellarRequest.Base) { + ctx.error = request.error + blockchainRequestsCallbacks.onComplete(false) + } + } + serverApi.setListener(listener) + val transaction = TransactionEx.fromEnvelopeXdr(String(txForSend)) + coinData!!.incSequenceNumber() + serverApi.requestData(ctx, SubmitTransaction(transaction)) + } + + override fun needMultipleLinesForBalance(): Boolean { + return true + } + + override fun allowSelectFeeLevel(): Boolean { + return false + } + + override fun pendingTransactionTimeoutInSeconds(): Int { + return 10 + } + + companion object { + private val TAG = XlmTagEngine::class.java.simpleName + private val decimals: Int + private get() = 7 + + private val multiplier = BigDecimal("10000000") + + const val OPERATIONS_LIMIT = 50 + private val TRUSTED_DESTINATION = "GAYPZMHFZERB42ONEJ4CY6ADDVTINEXMY6OZ5G6CLR4HHVKOSNJSZGMM" + private val TRUSTED_SOURCE = "GAZY7H4BWWEVB6QGB4RV3LW7DH5NO5CD5O6JCEQXA7N2UCGZSAPJFYW2" + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/fr_verify_card.xml b/app/src/main/res/layout/fr_verify_card.xml index 09cbf321ca..189186e5f5 100644 --- a/app/src/main/res/layout/fr_verify_card.xml +++ b/app/src/main/res/layout/fr_verify_card.xml @@ -198,7 +198,7 @@ android:layout_height="wrap_content" android:layout_marginStart="20dp" android:fontFamily="@font/maax" - android:text="The card is linked" + android:text="@string/details_linked_card_title" android:textSize="@dimen/text_size_1_small" /> + + + + diff --git a/app/src/main/res/navigation/main_graph.xml b/app/src/main/res/navigation/main_graph.xml index 7409283746..a65f23db22 100644 --- a/app/src/main/res/navigation/main_graph.xml +++ b/app/src/main/res/navigation/main_graph.xml @@ -28,6 +28,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0bd711498a..70870483ed 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -9,9 +9,9 @@ Continuer Le code PIN est vide Blockchain - Réessayez de numériser + Réessayez de scanner la carte Impossible d\'effacer le porte-monnaie avec un solde différent de zéro - Envoyer le paiement + Transférer les fonds Depuis la carte sur la carte avec solde @@ -49,9 +49,9 @@ Oups .. Il semble que votre smartphone ne supporte pas de tels paquets NFC. Essayez d\'envoyer une quantité inférieure ou utilisez un smartphone avec prise en charge complète de NFC. Votre argent est en jeu! - Cette carte a imposé un délai de sécurité - Veuillez tenir la carte fermement \ n jusqu\'à ce que l\'opération soit terminée… - Vous devrez peut-être répéter cette opération plusieurs fois en fonction des performances NFC de votre smartphone. \ n Ceci est fait pour sécuriser vos fonds. + Cette carte impose un délai de sécurité pour confirmer le transfert des fonds + Veuillez maintenir la carte dans cette position jusqu\'à ce que l\'opération soit terminée… + Vous devrez peut-être répéter cette opération plusieurs fois en fonction des performances NFC de votre smartphone.\nCeci est fait pour sécuriser vos fonds. Gérer le code PIN de l\'utilisateur… @@ -74,12 +74,14 @@ Mettez en contact la carte avec votre smartphone - Scanner une carte avec votre \n %1$s \n comme indiqué ci-dessus + Scanner votre carte avec votre %1$s comme indiqué ci-dessus phone Le porte-monnaie a été effacé Non personnalisé Il existe une nouvelle version de l\'application: %1$s Mettre à jour + Vous n’avez pas de carte?\nVisitez notre boutique sur %1$s + Aucun porte-monnaie compatible installé @@ -146,10 +148,10 @@ L\'authenticité ne peut pas être vérifiée. Balayez vers le bas pour actualiser.      - Maintenant, touchez la carte avec l\'ID + Maintenir la carte avec l\’ID pour créer le porte-monnaie pour effacer le porte-monnaie - pour signer le paiement + pour confirmer le transfert des fonds pour changer les codes PIN / PIN2 En mettant la carte en contact avec votre smartphone, vous retirerez définitivement le porte-monnaie de la blockchain. Assurez-vous qu\'il n\'y aura pas d\'autres transactions entrantes Impossible d\'effacer le porte-monnaie. Veillez à entrer le code PIN2 correct! @@ -178,7 +180,7 @@ entrez l\'adresse du porte-monnaie entrer le montant - Vérifiez + Vérifier Pas assez de fonds Format de montant inconnu Adresse du porte-monnaie de destination incorrecte @@ -223,12 +225,12 @@ Fabricant Porte-monnaie Identité de la carte - Attested + Attestée Non confirmé Réutilisable None Le dernier! - Unlimited + illimitées Carte ponctuelle Micrologiciel Date d\'enregistrement @@ -238,7 +240,7 @@ Émetteur Clé privée Noeud de validation - Unspents + Transferts Cette carte est protégée par le code PIN1 par défaut Cette carte est protégée par le code PIN1 de l’utilisateur Cette carte est protégée par le code PIN2 par défaut @@ -255,8 +257,9 @@ Bloquable\n Mode de commande atomique\n La liaison au terminal est prise en charge\n - La carte est liée + La carte est associée à ce téléphone + %1$d envoyé(s), %2$d reçu(s) Enregistrer diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ebf79111a2..645ff3bfac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,7 +82,7 @@ There is a new application version: %1$s Update Don\'t have a card?\n Visit our store at %1$s - tangemcards.com + tangemcards.com No compatible wallets installed @@ -262,6 +262,13 @@ Linking to the terminal is supported\n The card is linked To this phone + %1$d unspents (%2$d received) + + + Claim + GENUINE + NOT GENUINE + ALREADY CLAIMED Save diff --git a/app/src/main/res/xml/nfc_tech_filter.xml b/app/src/main/res/xml/nfc_tech_filter.xml index c10d3b1038..e2921c8701 100644 --- a/app/src/main/res/xml/nfc_tech_filter.xml +++ b/app/src/main/res/xml/nfc_tech_filter.xml @@ -3,5 +3,6 @@ android.nfc.tech.IsoDep android.nfc.tech.Ndef + android.nfc.tech.NfcV \ No newline at end of file diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java index 4fd4cc1b0f..8fe19575d6 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java @@ -29,6 +29,7 @@ public class TangemCard { private byte[] terminalPrivateKey; private byte[] terminalPublicKey; private boolean terminalIsLinked = false; + private byte[] tagSignature = null; public String getBlockchainID() { return blockchainID; @@ -629,6 +630,14 @@ public class TangemCard { DenominationText = null; } + public byte[] getTagSignature() { + return tagSignature; + } + + public void setTagSignature(byte[] tagSignature) { + this.tagSignature = tagSignature; + } + // public Bundle getAsBundle() { // Bundle B = new Bundle(); // saveToBundle(B); diff --git a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt index b59167f216..dffc588721 100644 --- a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt +++ b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/android/reader/NfcManager.kt @@ -24,7 +24,8 @@ class NfcManager(private val activity: FragmentActivity, private val readerCallb val TAG: String = NfcManager::class.java.simpleName // reader mode flags: listen for type A (not B), skipping ndef check - private const val READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK or NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS + private const val READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_V or + NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS private const val DELAY_PRESENCE = 1500 private const val REQUEST_NFC_PERMISSIONS = 1 diff --git a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt index f6e825319a..9c6356dd4e 100644 --- a/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt +++ b/tangem-sdk-old/src/main/java/com/tangem/tangem_sdk/data/TangemCardExtensions.kt @@ -85,6 +85,8 @@ fun TangemCard.loadFromBundle(B: Bundle) { if (B.containsKey("terminalPublicKey")) terminalPublicKey = B.getByteArray("terminalPublicKey") terminalIsLinked = B.getBoolean("terminalIsLinked") + if (B.containsKey("tagSignature")) tagSignature = B.getByteArray("tagSignature") + } val TangemCard.asBundle: Bundle @@ -112,11 +114,10 @@ fun TangemCard.saveToBundle(B: Bundle) { B.putInt("Health", health) if (settingsMask != null) B.putInt("settingsMask", settingsMask) B.putInt("pauseBeforePIN2", pauseBeforePIN2) - if( allowedSigningMethod!=null ){ - var iSigningMethod=0x80 - for(sM in allowedSigningMethod) - { - iSigningMethod=iSigningMethod.or(0x01.shl(sM.ID)) + if (allowedSigningMethod != null) { + var iSigningMethod = 0x80 + for (sM in allowedSigningMethod) { + iSigningMethod = iSigningMethod.or(0x01.shl(sM.ID)) } B.putInt("signingMethod", iSigningMethod) } @@ -163,6 +164,8 @@ fun TangemCard.saveToBundle(B: Bundle) { B.putByteArray("terminalPublicKey", terminalPublicKey) B.putBoolean("terminalIsLinked", terminalIsLinked) + if (tagSignature != null) B.putByteArray("tagSignature", tagSignature) + } catch (e: Exception) { Log.e("Can't save to bundle ", e.message) }