diff --git a/app/build.gradle b/app/build.gradle index af2ce223fa..e80024ee8a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -59,13 +59,13 @@ android { } packagingOptions { pickFirst('META-INF/proguard/okhttp3.pro') - // for one.block:eosiojava:0.1.0 +// for one.block:eosiojava:0.1.0 exclude 'lib/x86_64/darwin/libscrypt.dylib' exclude 'lib/x86_64/freebsd/libscrypt.so' exclude 'lib/x86_64/linux/libscrypt.so' exclude 'org.slf4j:slf4j-jdk14:1.7.25' } - buildToolsVersion '28.0.3' + buildToolsVersion '29.0.3' } repositories { @@ -85,15 +85,17 @@ dependencies { implementation 'androidx.navigation:navigation-ui-ktx:2.2.2' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation "androidx.lifecycle:lifecycle-runtime:2.2.0" implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" + implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.fragment:fragment:1.2.4' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "androidx.preference:preference:1.1.1" implementation 'androidx.biometric:biometric:1.0.1' - implementation 'androidx.core:core-ktx:1.2.0' + implementation 'androidx.core:core-ktx:1.3.0' implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.dagger:dagger:2.24' @@ -132,6 +134,7 @@ dependencies { implementation 'com.google.firebase:firebase-crashlytics:17.0.0-beta04' implementation 'com.google.firebase:firebase-perf:19.0.6' + //dependencies for XRP implementation files('libs/ripple-core-0.0.1.jar') //4 dependencies for ripple-core TODO: move to module? implementation 'net.i2p.crypto:eddsa:0.3.0' diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index b2771a6bb2..25117cc331 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -2,6 +2,8 @@ package com.tangem.data; import com.tangem.tangem_sdk.R; +import java.util.EnumSet; + /** * Created by dvol on 06.08.2017. */ @@ -20,7 +22,7 @@ public enum Blockchain { Rootstock("RSK", "RBTC", 1.0, R.drawable.tangem2, "RSK"), RootstockToken("RskToken", "RBTC", 1.0, R.drawable.tangem2, "RSK"), Cardano("CARDANO", "ADA", 1000000.0, R.drawable.tangem2, "Cardano"), - Ripple("XRP", "XRP", 1000000.0, R.drawable.ic_logo_xrp, "XRP"), + Ripple("XRP", "XRP", 1000000.0, R.drawable.ic_logo_xrp, "XRP Ledger"), 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"), @@ -36,6 +38,8 @@ public enum Blockchain { FlowDemo("FLOW/demo", "", 1.0, R.drawable.tangem2, "Flow demo"), TokenEmv("TTW", "ETH", 1.0, R.drawable.ic_logo_ethereum, "Ethereum"); + static private EnumSet payIdSupported = EnumSet.of(Blockchain.Ripple, Blockchain.Ethereum, Blockchain.Bitcoin); + Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) { mID = ID; mCurrency = currency; @@ -126,4 +130,8 @@ public enum Blockchain { return scheme; } + public Boolean isPayIdSupported() { + return payIdSupported.contains(this); + } + } diff --git a/app/src/main/java/com/tangem/data/network/PayIdApi.java b/app/src/main/java/com/tangem/data/network/PayIdApi.java new file mode 100644 index 0000000000..4616c01240 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/PayIdApi.java @@ -0,0 +1,15 @@ +package com.tangem.data.network; + +import com.tangem.data.network.model.PayIdResponse; + +import io.reactivex.Single; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.Headers; +import retrofit2.http.Path; + +public interface PayIdApi { + @Headers("PayID-Version: 1.0") + @GET("{user}") + Single getAddress(@Path("user") String user, @Header("Accept") String acceptHeader); +} diff --git a/app/src/main/java/com/tangem/data/network/ServerApiPayId.java b/app/src/main/java/com/tangem/data/network/ServerApiPayId.java new file mode 100644 index 0000000000..a2f545b580 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/ServerApiPayId.java @@ -0,0 +1,59 @@ +package com.tangem.data.network; + +import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import com.tangem.data.Blockchain; +import com.tangem.data.network.model.PayIdResponse; +import com.tangem.tangem_card.util.Log; + +import java.security.InvalidParameterException; + +import io.reactivex.Single; +import io.reactivex.SingleObserver; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class ServerApiPayId { + private static String TAG = ServerApiPayId.class.getSimpleName(); + + private int requestsCount = 0; + + public String getAcceptHeader(Blockchain blockchain) throws InvalidParameterException { + switch (blockchain) { + case Ripple: return "application/xrpl-mainnet+json"; + case Bitcoin: return "application/btc-mainnet+json"; + case Ethereum: return "application/eth-mainnet+json"; + default: throw new InvalidParameterException("PayID is not supported for " + blockchain.getOfficialName()); + } + } + + public boolean isRequestsSequenceCompleted() { + Log.i(TAG, String.format("isRequestsSequenceCompleted: %s (%d requests left)", String.valueOf(requestsCount <= 0), requestsCount)); + return requestsCount <= 0; + } + + public void getAddress(String payID, Blockchain blockchain, SingleObserver addressObserver) throws InvalidParameterException { + requestsCount++; + Log.i(TAG, "new getAddress request"); + + String[] addressParts = payID.split("\\$"); + String user = addressParts[0]; + String domain = addressParts[1]; + + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://" + domain + "/") + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .build(); + + PayIdApi api = retrofit.create(PayIdApi.class); + + Single addressSingle = api.getAddress(user, getAcceptHeader(blockchain)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnEvent((object, throwable) -> requestsCount--); + + addressSingle.subscribe(addressObserver); + } +} diff --git a/app/src/main/java/com/tangem/data/network/model/PayIdResponse.kt b/app/src/main/java/com/tangem/data/network/model/PayIdResponse.kt new file mode 100644 index 0000000000..583ae73462 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/PayIdResponse.kt @@ -0,0 +1,24 @@ +package com.tangem.data.network.model + +import com.google.gson.annotations.SerializedName + +data class PayIdResponse( + @SerializedName("addresses") + var addresses: List? = null +) + +data class PayIdAddress( + @SerializedName("paymentNetwork") + var paymentNetwork: String? = null, + + @SerializedName("environment") + var environment: String? = null, + + @SerializedName("addressDetails") + var addressDetails: PayIdAddressDetails? = null +) + +data class PayIdAddressDetails( + @SerializedName("address") + var address: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/ui/fragment/BaseFragment.kt b/app/src/main/java/com/tangem/ui/fragment/BaseFragment.kt index 43065af0c1..ed903c4d47 100644 --- a/app/src/main/java/com/tangem/ui/fragment/BaseFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/BaseFragment.kt @@ -91,9 +91,15 @@ abstract class BaseFragment : Fragment() { protected fun navigateBackWithResult(resultCode: Int, data: Bundle? = null, @IdRes destination: Int = DESTINATION_NOT_SET) { - (requireActivity() as MainActivity).viewModel.navigationResult = + try { + (requireActivity() as MainActivity).viewModel.navigationResult = NavigationResult(requestCode, resultCode, data) - return navigateUp(destination) + return navigateUp(destination) + } catch (e: IllegalArgumentException) { + Log.w(this::class.java.simpleName, e.message) + } catch (e: IllegalStateException) { + Log.w(this::class.java.simpleName, e.message) + } } companion object { 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 d380de4fc2..bd386f97c4 100644 --- a/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt @@ -64,7 +64,6 @@ 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, @@ -74,6 +73,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader companion object { fun newInstance() = MainFragment() val TAG: String = MainFragment::class.java.simpleName + const val CARD_SHOP_URI = "https://shop.tangem.com/?afmc=1i&utm_campaign=1i&utm_source=leaddyno&utm_medium=affiliate" } override val layoutId = R.layout.fragment_main @@ -102,7 +102,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader // navigateToDestination(R.id.action_main_to_emptyIdFragment) // init NFC Antenna - nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc) + nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc) nfcDeviceAntenna.init() // set phone name @@ -136,7 +136,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader tvBuyCards?.setText(spannable, TextView.BufferType.SPANNABLE) llShoppingView?.setOnClickListener { - val uri = Uri.parse("https://www.tangemcards.com") + val uri = Uri.parse(CARD_SHOP_URI) val intent = Intent(Intent.ACTION_VIEW, uri) startActivity(intent) } @@ -342,7 +342,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader } else { if (cardProtocol.error is CardProtocol.TangemException_ExtendedLengthNotSupported) if (!NoExtendedLengthSupportDialog.allReadyShowed) - NoExtendedLengthSupportDialog().show(activity!!.supportFragmentManager, NoExtendedLengthSupportDialog.TAG) + NoExtendedLengthSupportDialog().show(requireActivity().supportFragmentManager, NoExtendedLengthSupportDialog.TAG) lastTag = null ReadCardInfoTask.resetLastReadInfo() @@ -444,15 +444,15 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader } override fun onReadWait(msec: Int) { - WaitSecurityDelayDialog.onReadWait(Objects.requireNonNull(activity) as AppCompatActivity?, msec) + WaitSecurityDelayDialog.onReadWait(requireActivity() as AppCompatActivity?, msec) } override fun onReadBeforeRequest(timeout: Int) { - WaitSecurityDelayDialog.onReadBeforeRequest(Objects.requireNonNull(activity) as AppCompatActivity?, timeout) + WaitSecurityDelayDialog.onReadBeforeRequest(requireActivity() as AppCompatActivity?, timeout) } override fun onReadAfterRequest() { - WaitSecurityDelayDialog.onReadAfterRequest(Objects.requireNonNull(activity)) + WaitSecurityDelayDialog.onReadAfterRequest(requireActivity()) } override fun onMenuItemClick(item: MenuItem?): Boolean { diff --git a/app/src/main/java/com/tangem/ui/fragment/PurgeFragment.kt b/app/src/main/java/com/tangem/ui/fragment/PurgeFragment.kt index 76d8d97dc6..2130c741a9 100644 --- a/app/src/main/java/com/tangem/ui/fragment/PurgeFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/PurgeFragment.kt @@ -66,7 +66,7 @@ class PurgeFragment : BaseFragment(), NfcAdapter.ReaderCallback, CardProtocol.No super.onViewCreated(view, savedInstanceState) // init NFC Antenna - nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc) + nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc) nfcDeviceAntenna.init() tvCardID.text = ctx.card.cidDescription @@ -99,7 +99,7 @@ class PurgeFragment : BaseFragment(), NfcAdapter.ReaderCallback, CardProtocol.No } override fun onReadWait(msec: Int) { - WaitSecurityDelayDialog.onReadWait(activity, msec) + activity?.let { WaitSecurityDelayDialog.onReadWait(it, msec) } // val readWait = ReadWait() // readWait.msec = msec @@ -107,7 +107,7 @@ class PurgeFragment : BaseFragment(), NfcAdapter.ReaderCallback, CardProtocol.No } override fun onReadBeforeRequest(timeout: Int) { - WaitSecurityDelayDialog.onReadBeforeRequest(activity, timeout) + activity?.let { WaitSecurityDelayDialog.onReadBeforeRequest(it, timeout) } // if (!waitSecurityDelayDialogNew.isAdded) // waitSecurityDelayDialogNew.show(supportFragmentManager, WaitSecurityDelayDialogNew.TAG) @@ -119,16 +119,16 @@ class PurgeFragment : BaseFragment(), NfcAdapter.ReaderCallback, CardProtocol.No } override fun onReadAfterRequest() { - WaitSecurityDelayDialog.onReadAfterRequest(activity) + activity?.let { WaitSecurityDelayDialog.onReadAfterRequest(it) } // val readAfterRequest = ReadAfterRequest() // EventBus.getDefault().post(readAfterRequest) } override fun onReadStart(cardProtocol: CardProtocol) { - rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE } + rlProgressBar?.post { rlProgressBar.visibility = View.VISIBLE } - progressBar.post { + progressBar?.post { progressBar.visibility = View.VISIBLE progressBar.progress = 5 } @@ -179,7 +179,7 @@ class PurgeFragment : BaseFragment(), NfcAdapter.ReaderCallback, CardProtocol.No progressBar?.post { if (cardProtocol.error is CardProtocol.TangemException_ExtendedLengthNotSupported) { if (!NoExtendedLengthSupportDialog.allReadyShowed) - NoExtendedLengthSupportDialog().show(activity!!.supportFragmentManager, NoExtendedLengthSupportDialog.TAG) + NoExtendedLengthSupportDialog().show(requireActivity().supportFragmentManager, NoExtendedLengthSupportDialog.TAG) } else Toast.makeText(context, R.string.general_notification_scan_again_to_verify, Toast.LENGTH_LONG).show() diff --git a/app/src/main/java/com/tangem/ui/fragment/id/ValidateIdFragment.kt b/app/src/main/java/com/tangem/ui/fragment/id/ValidateIdFragment.kt index 61019f8ab9..ebbc5c2191 100644 --- a/app/src/main/java/com/tangem/ui/fragment/id/ValidateIdFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/id/ValidateIdFragment.kt @@ -77,7 +77,7 @@ class ValidateIdFragment : BaseFragment(), NavigationResultListener, mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound) // init NFC Antenna - nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc) + nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc) nfcDeviceAntenna.init() progressBar.progressTintList = ColorStateList.valueOf(Color.DKGRAY) @@ -198,7 +198,7 @@ class ValidateIdFragment : BaseFragment(), NavigationResultListener, } override fun onReadStart(cardProtocol: CardProtocol) { - rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE } + rlProgressBar?.post { rlProgressBar.visibility = View.VISIBLE } progressBar?.post { progressBar?.visibility = View.VISIBLE @@ -265,7 +265,7 @@ class ValidateIdFragment : BaseFragment(), NavigationResultListener, NoExtendedLengthSupportDialog().show(requireFragmentManager(), NoExtendedLengthSupportDialog.TAG) } } else { - (activity as MainActivity).toastHelper.showSingleToast( + (activity as? MainActivity)?.toastHelper?.showSingleToast( context, getString(R.string.general_notification_scan_again) ) @@ -313,17 +313,17 @@ class ValidateIdFragment : BaseFragment(), NavigationResultListener, override fun onReadBeforeRequest(timeout: Int) { LOG.i(TAG, "onReadBeforeRequest timeout $timeout") - WaitSecurityDelayDialog.onReadBeforeRequest(activity!!, timeout) + activity?.let { WaitSecurityDelayDialog.onReadBeforeRequest(it, timeout) } } override fun onReadAfterRequest() { LOG.i(TAG, "onReadAfterRequest") - WaitSecurityDelayDialog.onReadAfterRequest(activity!!) + activity?.let { WaitSecurityDelayDialog.onReadAfterRequest(it) } } override fun onReadWait(msec: Int) { LOG.i(TAG, "onReadWait msec $msec") - WaitSecurityDelayDialog.onReadWait(activity!!, msec) + activity?.let { WaitSecurityDelayDialog.onReadWait(it, msec) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/ui/fragment/id/WriteIdFragment.kt b/app/src/main/java/com/tangem/ui/fragment/id/WriteIdFragment.kt index 03e96331c8..e0baa7b79d 100644 --- a/app/src/main/java/com/tangem/ui/fragment/id/WriteIdFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/id/WriteIdFragment.kt @@ -77,7 +77,7 @@ class WriteIdFragment : BaseFragment(), NavigationResultListener, mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound) // init NFC Antenna - nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc) + nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc) nfcDeviceAntenna.init() tvCardID.text = ctx.card!!.cidDescription @@ -137,7 +137,7 @@ class WriteIdFragment : BaseFragment(), NavigationResultListener, } override fun onReadStart(cardProtocol: CardProtocol) { - rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE } + rlProgressBar?.post { rlProgressBar.visibility = View.VISIBLE } progressBar?.post { progressBar?.visibility = View.VISIBLE @@ -279,17 +279,17 @@ class WriteIdFragment : BaseFragment(), NavigationResultListener, override fun onReadBeforeRequest(timeout: Int) { LOG.i(TAG, "onReadBeforeRequest timeout $timeout") - WaitSecurityDelayDialog.onReadBeforeRequest(activity!!, timeout) + activity?.let { WaitSecurityDelayDialog.onReadBeforeRequest(it, timeout) } } override fun onReadAfterRequest() { LOG.i(TAG, "onReadAfterRequest") - WaitSecurityDelayDialog.onReadAfterRequest(activity!!) + activity?.let { WaitSecurityDelayDialog.onReadAfterRequest(it) } } override fun onReadWait(msec: Int) { LOG.i(TAG, "onReadWait msec $msec") - WaitSecurityDelayDialog.onReadWait(activity!!, msec) + activity?.let { WaitSecurityDelayDialog.onReadWait(it, msec) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt b/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt index e1055fbfce..e445b52dd1 100644 --- a/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt @@ -14,6 +14,7 @@ import android.os.Build import android.os.Bundle import android.text.Html import android.view.Gravity +import android.view.LayoutInflater import android.view.View import android.widget.Toast import androidx.appcompat.app.AppCompatActivity @@ -24,13 +25,12 @@ import com.tangem.App import com.tangem.Constant import com.tangem.data.Blockchain import com.tangem.data.dp.PrefsManager -import com.tangem.data.network.ServerApiCommon +import com.tangem.server_android.Result import com.tangem.server_android.ServerApiTangem import com.tangem.server_android.model.CardVerifyAndGetInfo import com.tangem.tangem_card.data.TangemCard import com.tangem.tangem_card.reader.CardProtocol import com.tangem.tangem_card.tasks.VerifyCardTask -import com.tangem.tangem_card.tasks.WriteIDCardTask import com.tangem.tangem_card.util.Util import com.tangem.tangem_sdk.android.reader.NfcReader import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD @@ -50,13 +50,14 @@ import com.tangem.ui.navigation.NavigationResultListener import com.tangem.util.LOG import com.tangem.util.UtilHelper import com.tangem.wallet.* +import kotlinx.android.synthetic.main.dialog_pay_id.view.* import kotlinx.android.synthetic.main.fr_loaded_wallet.* import kotlinx.android.synthetic.main.layout_btn_details.* import kotlinx.android.synthetic.main.layout_tangem_card.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe +import retrofit2.HttpException import java.io.InputStream -import java.text.SimpleDateFormat import java.util.* import kotlin.concurrent.timerTask @@ -64,6 +65,7 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte CardProtocol.Notifications, SharedPreferences.OnSharedPreferenceChangeListener { companion object { val TAG: String = LoadedWalletFragment::class.java.simpleName + const val PAY_ID_TANGEM = "\$payid.tangem.com" } override val layoutId = R.layout.fr_loaded_wallet @@ -71,7 +73,6 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte private lateinit var viewModel: LoadedWalletViewModel private lateinit var ctx: TangemContext private lateinit var mpSecondScanSound: MediaPlayer - private var serverApiCommon: ServerApiCommon = ServerApiCommon() private var serverApiTangem: ServerApiTangem = ServerApiTangem() private var lastTag: Tag? = null private var lastReadSuccess = true @@ -113,6 +114,10 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte srl.isRefreshing = true } + private var payId: String? = null + private lateinit var btnLoadItems: MutableList + + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -121,7 +126,7 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte lastTag = activity?.intent?.getParcelableExtra(Constant.EXTRA_LAST_DISCOVERED_TAG) if (arguments?.containsKey(NfcAdapter.EXTRA_TAG) == true) { - val tag = arguments!!.getParcelable(NfcAdapter.EXTRA_TAG) + val tag = requireArguments().getParcelable(NfcAdapter.EXTRA_TAG) if (tag != null) onTagDiscovered(tag) } } @@ -129,6 +134,8 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + viewModel = ViewModelProviders.of(this).get(LoadedWalletViewModel::class.java) + mpSecondScanSound = MediaPlayer.create(activity, R.raw.scan_card_sound) val engine = CoinEngineFactory.create(ctx) @@ -142,6 +149,21 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte tvWallet.text = ctx.coinData.wallet + btnLoadItems = mutableListOf( + getString(R.string.loaded_wallet_load_via_app), + getString(R.string.loaded_wallet_load_via_share_address), + getString(R.string.loaded_wallet_load_via_qr) + ) + + if (ctx.blockchain.isPayIdSupported) { + ivPayId.visibility = View.VISIBLE + ivPayId.imageAlpha = 100 + ivPayId.setOnClickListener { createPayIdDialog() } + + getPayIdIfApplicable() + } + + // set listeners srl.setOnRefreshListener { refresh() } @@ -151,16 +173,17 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte btnCopy.setOnClickListener { doShareWallet(false) } + + if (Util.bytesToHex(ctx.card?.cid)?.startsWith("10") == true) { btnLoad?.visibility = View.GONE } btnLoad.setOnClickListener { - val items = arrayOf(getString(R.string.loaded_wallet_load_via_app), getString(R.string.loaded_wallet_load_via_share_address), getString(R.string.loaded_wallet_load_via_qr))//, getString(R.string.via_cryptonit), getString(R.string.via_kraken)) val cw = android.view.ContextThemeWrapper(activity, R.style.AlertDialogTheme) - val dialog = AlertDialog.Builder(cw).setItems(items + val dialog = AlertDialog.Builder(cw).setItems(btnLoadItems.toTypedArray() ) { _, which -> - when (items[which]) { + when (btnLoadItems[which]) { getString(R.string.loaded_wallet_load_via_app) -> { try { val intent = Intent(Intent.ACTION_VIEW, engine.shareWalletUriEx) @@ -178,22 +201,14 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte getString(R.string.loaded_wallet_load_via_qr) -> { ShowQRCodeDialog.show(activity as AppCompatActivity?, engine.shareWalletUriEx.toString()) } - - getString(R.string.loaded_wallet_load_via_cryptonit) -> { - navigateForResult( - Constant.REQUEST_CODE_RECEIVE_TRANSACTION, - R.id.action_loadedWalletFragment_to_prepareCryptonitWithdrawalFragment, - Bundle().apply { ctx.saveToBundle(this) } - ) + payId -> { + if (payId == getString(R.string.loaded_wallet_create_pay_id)) { + createPayIdDialog() + } else { + payId?.let { copyText(it) } + } } - getString(R.string.loaded_wallet_load_via_kraken) -> { - navigateForResult( - Constant.REQUEST_CODE_RECEIVE_TRANSACTION, - R.id.action_loadedWalletFragment_to_prepareKrakenWithdrawalFragment, - Bundle().apply { ctx.saveToBundle(this) } - ) - } else -> { } } @@ -299,10 +314,9 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte startVerify(lastTag) - viewModel = ViewModelProviders.of(this).get(LoadedWalletViewModel::class.java) // set rate info to CoinData - viewModel.getRateInfo().observe(this, Observer { rate -> + viewModel.getRateInfo().observe(viewLifecycleOwner, Observer { rate -> ctx.coinData.rate = rate ctx.coinData.rateAlter = rate updateViews() @@ -310,6 +324,92 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte viewModel.requestRateInfo(ctx) } + private fun getPayIdIfApplicable() { + if (ctx.blockchain.isPayIdSupported) { + viewModel.getPayId( + Util.byteArrayToHexString(ctx.card.cid!!), + Util.byteArrayToHexString(ctx.card.cardPublicKey!!)) + .observe( + viewLifecycleOwner, Observer { result -> + when (result) { + is Result.Success -> { + onPayIdFound(result.data.payId) + } + is Result.Failure -> { + (result.error as? HttpException)?.let { + if (it.code() == 404) { + val createPayId = getString(R.string.loaded_wallet_create_pay_id) + payId = createPayId + if (!btnLoadItems.contains(createPayId)) { + btnLoadItems.add(createPayId) + } + } + } + } + } + }) + } + } + + private fun createPayIdDialog() { + val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_pay_id, null); + val alertDialogBuilderUserInput = AlertDialog.Builder(context); + alertDialogBuilderUserInput.setView(dialogView); + alertDialogBuilderUserInput + .setCancelable(true) + .setPositiveButton(getString(R.string.create_pay_id_create)) { dialogBox, _ -> + if (dialogView.etPayId.text.isNullOrBlank()) { + Toast.makeText(context, getString(R.string.create_pay_id_empty), Toast.LENGTH_LONG).show() + return@setPositiveButton + } + + dialogBox.cancel() + createPayId("${dialogView.etPayId.text}$PAY_ID_TANGEM") + } + + .setNegativeButton(getString(R.string.general_cancel)){ dialogBox, _ -> + dialogBox.cancel() + } + + alertDialogBuilderUserInput.create().show() + } + + private fun createPayId(payId: String) { + val network = if (ctx.blockchain == Blockchain.Ripple) "XRPL" else ctx.blockchain.id + viewModel.setPayId( + Util.byteArrayToHexString(ctx.card!!.cid!!), + Util.byteArrayToHexString(ctx.card!!.cardPublicKey!!), + payId, + ctx.coinData.wallet, + network + ) + .observe(viewLifecycleOwner, Observer { result -> + when (result) { + is Result.Success -> { + onPayIdFound(payId) + Toast.makeText(context, getString(R.string.create_pay_id_success), Toast.LENGTH_LONG).show() + } + is Result.Failure -> { + Toast.makeText(context, getString(R.string.create_pay_id_error), Toast.LENGTH_LONG).show() + } + } + }) + } + + private fun onPayIdFound(payId: String) { + updateLoadButtonItemsWithNewPayId(payId) + ivPayId.imageAlpha = 255 + this.payId = payId + ivPayId.setOnClickListener { copyText(payId) } + } + + private fun updateLoadButtonItemsWithNewPayId(payId: String) { + val oldPayId = btnLoadItems.firstOrNull { it.contains(PAY_ID_TANGEM) } + btnLoadItems.remove(oldPayId) + btnLoadItems.remove(getString(R.string.loaded_wallet_create_pay_id)) + btnLoadItems.add(payId) + } + override fun onPause() { super.onPause() if (timerHideErrorAndMessage != null) { @@ -705,6 +805,10 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte requestCounter = 0 srl?.isRefreshing = true + if (payId == null || payId == getString(R.string.loaded_wallet_create_pay_id)) { + getPayIdIfApplicable() + } + updateViews() // Bitcoin, Litecoin, BitcoinCash, Stellar @@ -804,71 +908,7 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte val uid = tag?.id val sUID = Util.byteArrayToHexString(uid) if (ctx.card.uid != sUID || cardProtocol != null) { - - //TODO - remove, only for test - //TODO - begin - if (ctx.card.isIDCard && !ctx.card.hasIDCardData()) { - val idCardData = TangemCard.IDCardData() - idCardData.fullName = "John Doe" - idCardData.birthday = "1980-1-1" - idCardData.gender = "M" - val dt = Date() - idCardData.issueDate = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(dt) - dt.year = dt.year + 10 - idCardData.expireDate = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(dt) - idCardData.trustedAddress = "GCOQLQT32RVH2MTWQGK5K3GIGT3STYG5323KMNTTFHIU2T5TFOGR4KDT" // ask D.Baturin - idCardData.photo = Util.hexToBytes("FFD8FFE000104A46494600010101012C012C0000FFDB0043000A07070807060A0808080B0A0A0B0E18100E0D0D0E1D15161118231F2524221F2221262B372F26293429212230413134393B3E3E3E252E4449433C48373D3E3BFFDB0043010A0B0B0E0D0E1C10101C3B2822283B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3B3BFFC000110800C8007903012200021101031101FFC4001C0000020203010100000000000000000000000706080103050402FFC4003F100001030301050505060406020300000001020304000511060712213141135161718114223291A115235262B1C1428292D10816333472A2175393D2E1FFC4001801000301010000000000000000000000000002030104FFC4002111000202030100020301000000000000000001021103213112132232415142FFDA000C03010002110311003F007351451400562B35A83ED29F53097505D424294807DE4839C123B8E0FCA8016BB4CD712AC97FB642B72FDF8AA12A4273C179C84A0F811BC7D45306CF768B7BB547B9425EFB0FA3793DE3BC1F10720D579D75297335C5E1D5924A64A9B19EE4FBA3F4AECECDB5B7F96AE2604E708B64B57BC4F2617C82FC8F23E87A54164FB6C771D0FBA2BE50B4AD0149214923208E44566AE219A28A2800A28A2800A28A2800A28A2800A28A2803E5440049E02ABEFF009F26C5DA0C9D44CA94B69C74B6A673C16C03809F900478D580791DAB2B6F38DF494E7CEAA9BAD165E5B4AF89B5149F307151CADAAA1A28ECEB35B0FEAB9D322381C8D31424B4B1D52B483F30720F8835C4A3248009381CBC28AE76ED9444C7496D2AEDA65B44375227C04F04B2E2B0A6C7E45741E07879533AD7B55D2B7040ED662A0B879A2520A47F50C8FAD2028A78E468571459E6F53D85E46FB77AB7A93DE24A3FBD706EBB54D2D6DCA5B98A9CE0FE18A8DE1FD4709FAD57EC03D07CAB34CF3333C967B4E5F19D47638D7561B53687C1FBB5104A48241071E55EE932E3C3614FCA7DB61A4FC4E38A0948F5355F6C1B43BBE9BB02ED36F6D8CA9E538979D05451903202797319E3DFCAB8574BD5CEF6FF006D739CF4B5E78768AC84F90E43D053FCAA8CF2588B36AFB46A0B9C8856A7952BD99016E3C9410D8C9C0009E679F2E1C2BBB4BED8EDA7D8B4A393D69C393DE2A07F227DD4FD778FAD306A916DAB6630A28A298C31466B54A694FC575943CB654B414A5C47C4824731E2290978D5FAE2C976936C977B901E8EB2927711850E8A1EEF22307D69252F26A564CEFF00B49BCE92D48FDB6E36D8F2E383DA30EB6A2D296D9E5DE091C41F2A52DD65353AEF32630D29A6A43EB752DA8E4A428938FAD6FBBDFEEB7E5B4BBACD5CA532084296948290798E0077573AB9A53B2895051582A006490078D7C7B4320E3B44FCE9526CD3651520B268ABA6A0483024DB5448CEE19C82BF54A72457A6E9B37D51698AECA760B6FB2CA0AD6A61E4A8800649C7034530B22D45789570E3EEB7F335F22E0BEADA4F91A7F8A467A47BE83E15E76A634E1C1F70F8D7A291C5AE9A9D8D7B0ED76D36BB544B6AACB2D0DC6692D02D3885E70319C1C73A6258AFE9BEB1DBB76DB8446F190A96C86F7BCB8E4D56C813E55B26B53613CA62432ADE42D38C83EB4E2D15B556EF1218B5DE582CCD748436F3292A43A7C40E293F4F2ABC277A62490C8A2B159AB8862951B68B75B0B712E224B2D5C53F765927DF79BEFC7E53D4F426999738D2E5C25330E7182EAB876C9682D491E00F0CF8F1A40EBEB55AECB75EC5ABFBD75B82944CB5BC527B33D0139E2AF0E953C9C1A3D22F5A24C8EC123032A3CBBAB7020F2391E15BA2C11739B1E0938F68790D820711BCA033F5AE68D5EC77C3970E15C2F32D31E1467A5BEAE4DB482A3F21D2A52C6C8F5A3CD05FD9496F3C92E486D27E59A7BE9ED3569D236AF6580D25A42465E7DC237DC3F894AFDB90AC2F58E986DEEC57A86D895E71BA6523FBD59E47FE5095FD10917671AA23DFE0439B6A931DB7E4210A7D1EF21033C4EF24903866AC9169B2D762520B653BBBA78E538C63E55861F664B21E8EF21D695C96DA82927D452FB5D6D43EC49E6C7608E275D490859DD2A4B4A3FC21238A95E1C878F2A46DCDD1BA479E06C32C2D2D6B9B3A649CA894A1B21B4A46780EA4FCC56ABEEC36D521ADFB24C7A23C07FA7215DA215EB8C8FAD781AD21B56D449126E17E55B82F88695254D91FC8D8C0A1CD27B58D3A3B7817D55C528E3D9A649709FE57471F4AAF99FF004CB42E351E8FBDE977B72E509686C9C21E4FBCDABC94387EF5E78A9712C80E1C9E80F414D381B4C45E62CBD2FACA10B7497DB5305F282942564606FA4F141CE0E797952CD68536B5217F12490AF31CEA7372AA66C523E69F7B33B169F896166E76B57B5497D387A4B806FA55D518FE1C1E9D79E4D2523E9BD473612AE10EC52DF86919ED529F8C0E6523991E40D31F64F11C5155CAC3770EC65283770B74B6F716D9E8A041209EE381919071D371C5A7612636EB358159AE810876D3F53BDA6748BAEC3514CD96B11E3A8734920E543C40071E24579F44681B6E9EB434E4B88D49BA3C90B92FBC80B2147894A73C80E5E278D7236C984CCD2AE3BFEDD370FBDCF2E68E7E80D320F33E751CADF068A10FB53B4C5B56AF262212DA2530878B6848012AC949C01DFBB9F9D462D17362CD798572928538DC5792E96D3CD653C401EB8A97ED817BDACDB4FE086D8FAA8D426CD6A3A8355DBACFBC52890F250B23984F3511E80D4A11F5219BA44D615A358ED764AA6CE99EC1670B3B8307B3183C908CFBE47551F9F4AF5DE767DB39D33B912F5A9673531C00E13BAA207794A50703CE9CD162B16F84DC68ACA5A61840436DA0704A40E005548BD5CA45DEF32EE129654F48754B513D38F01E4070F4AEBAA264F275B6FDB305C7BEE9DBBA6E165998DD71232DAB23805A738F250EA3A72AEF6C36CAD4D72E5A9660EDE507BB1696BE25248DE5ABCCEF019F3EFAD5616C9FF0F574F6FE2CEF3AA8FBDD3DF4EEE3F9F35AF613A8988F26769F7D610B92A1223E7F89406149F3C007D0D0074F52EDBBEC8D46F5BADF6B6E5468AE169D756E9495A81C2B7703000391939CD31B4E5FE16A7B247BAC1512D3C38A55F121438149F106AB56BBB1CAB06AFB846908210E3CA759591C16DA89208EFE783E20D3736131E533A3E53AF052587A6294CE7AE1290A23C3231E8680241AFB41C1D636A5E1B437736924C6918C1CFE051EA93F4E755CD80F34F3B0DE4143CD28A0A55CC107047A1AB7279555FD6FD89DA4DE7D9F1B9ED4BCE3F163DEFF00B66926AE2CD5D2C6DAA208369870C0C7B3B086FE490297D15B1A676EC23C61D9C5BEC52B5B63E10BC28938FF009209FE6358D9AEBF125B458AF2F80F21388D21C57FA807F0289EA0723D478F3F88529BD5FB6F6675BD41E83648A52A7D1C50A5614381EBEF2CE3BF74D47174690D8159AC0ACD748847B5B694635869D76D8EB9D93A141C61DC6771C1C8F910483E06A0D17556BDD22C2605F74BBD7669801089914925491C8920107D403DF4DAAC1AC693E8595A3575FD7A9750BD725C572265296C30E1CA91BA3183E39CD78ECD25FD35A82D1A95D654A862491BC3AEEE02C79EEAABEF51C77226A3B8B0F496E53A990BED1D6F3BAA51393CFC4E3D29A7A2B4BDBB516CB99B7DC992B69F7DD752A4F0536ADE202927A1E15CD19799146AD0C5833A2DCA135321BE87E3BC90A6DC41C850A5DDD7629619B7B76E7EDF222C57165D7632027033C4E147E11E87151FFF00C6DAEF4BBEBFF2B5F7B48CA39DD4BDD913FF00242BDD27C73515D6E35D4211A2EA6B9BCEFB58514474C90A07040C94A78733C2BA1493E13A247AF351C7BF983A0B46321D8ADAD285167E070A79241EA91CCABA919E993D0D47B227A1DAA04ED32E94DD203490E84AB74BEB1C77D27A2B3D3A8C75E732D13A12D9A420A16D35DA5C1D6C07E439C559C64A53DC9CFCFAD4AAA32C8EF43A889DB6ED3AD5706D169DA1D8D0B9118E0BEE460BC1EF5208CA4F7E39F753021EBED12DC34262DF6DECB084E10D85767BA3B827031F2AF6DE74C593502026ED6D625103016A4E163C943047CE92BB50D176BD2371B649B7B0E88127783AD29D2AC292412013C46527E9548E452D18D5135D5BB67B6468AB89A68AA7CD706EA1EECC86DB27A8CF151EE0063F4A80CDD0D3ECFA3D3A8AEFBE89F2E5A53D8B87DE4A141472AFCC4E0E3A0F3A72E9DD17A5ECA96A65A6D8D071690B4487097178232082ACE3D315AF5E69995AAACCCC18D25A8FD9BE1E5A9C04E40491818F3A9CF25E91A908BB346B5CC9E98F769CEC261CE01F4341C4A4FE6191C3C45582D17A6E1699B0B70E1A98782CEFAA4328DDEDF3C94789CF0F1C776290B61F6190B7ED53FB3684C012CCB50FF6EF0CEE927F01CEEABCC1E94E6D9DE96BE69A82A6AE972438CAC6510D037C327A90B3FA018ADC46489AD158ACD5C50A8D6BBD4A9D33A69F94850F6B7BEE62A7BD67AFA0C9F4A92D41B6A3A818B1D89296D0DAAE52829A8CB29054D24FC6B07A70C0F323BA964E91A8439254A2A512493924F326AC2ECF59EC7425A138C6F31BFF00D4A27F7AAF237411BD9DDEB8EEAB31A762A6169CB6C54AD2B0D456D2149E4AF747115C654E8D26F5AA7EDADB7D9ADABE2DB1D82549F0C9715F4A7252727FDCFF0088C8CB74E12B537BB9F16303EB54C7D62C871F3E3DF59AC0E559A98C14BADB6C44BFA25A7F1EF4798820F8282927F6A62D2EB6D935B8FA25B8C4FBF265A02478241513FA7CE9E1F9215F092E8394A99A12CAF2CE55EC88493DFBBEEFED5DD75412DA94790493F4AE0E828AB85A12CCCB80857B225641E9BDEF7EF5EBD513D36BD2F729AA38ECA32F77FE44607D48A57D3570AD8A3BCA51E849A766CAB5A7DAF6FF00B127BB99B111F74A51E2F343F529E47C307BE92206001DD5E9B7CF936AB8313E1BA5A911D616DA8743FD8F235B097960D596A6B35C4D29A9236A8B1B370630959F75E6B3C5A58E63F71E04576EBAD3B247CA88032780155B3596A273536A491394486527B28E83FC2D83C3D4F127CE9F7AC2E3F65692BA4D070A6E32820FE623753F522AB3018E1DD51CCFF43C4CD3E76617B4DDB4830C2D797E01F677013C7747141FE9E1E8690D5DDD21AAA4E93BB898D23B561C1B9219CE37D3E1DC474FFF006B9C72C6528B6C76D936BBCDA757C2202D85A1A59CF25A49520F8E4647A577646D92C0DC7DE8F1273CEE3820A12819F1564FE94B2D57AB2E1ABA6076684B6C3790CC647C280799F127BE9E32A7663563CB4BEAEB5EACB737260BE80F1482EC62A1DA34AEA08EEEE3C8D77307B8FCAAA608AA69C0E47794D287220E08F222BD2A9B7B5A77177896A477190B23E59A6A8BE3336593BDEA8B2E9D8EA76E97165820706F7B79C578048E26945DA4FDB16B76886571EC9031BC09E286C9C9C9FC6BC63872C7852FC420A5153AE2964F3F1F5A9468BD52EE8FBA990D33DAC6753B8FB20E37873041EF1FDE8B8C79D0A6CB148425B42508484A523000E400E94ABDAF6A76D486F4EC570294141D9641F871F0A3CFA9F4AF8BFED8FB786A62C509D61D58C191237728FF008A46727C4FCA960E38E3CEADD756A71C5A8A94B51C9513CC935318F9A28A2B00636CD6DFAAECD3A3DCE2DB1C91699C901EDD751EF273C16015734FE9914EAE3E1503D8F5C7DAF46988A56550A42DBC7E557BC3F53F2A9ED76415224FA2F76CD70F66D28C4249C2A6494823BD281BC7EBBB490A636DA6E3DBEA285012ACA62C6DF50EE52CFF00648A5CD73E4772291E05145153342B5B921B68E16AC1EEC56CAF9536851CA90924778AD55FB035A65B0A380B03CC62B766B52E3B2B182D8F4E15AA3EF32FA982494E32934F49AD196CF551451533428A28A0028A283C050032B62971EC6F770B7295812180EA47E641C1FA2BE94E7CD577D1E6569BD79685CB41692FA90013F0ADB75380A07A8E23E5561BDEEEAEAC4FEB44E5D14ACE8E7B5DEB6B85F26A94DD9848DC694382A4A5184809EE4F0E2AF9782BA404894E840C2438A091DC327156A5C09663AB7404A508380060000555351DE515779CD4F2248D8B31451454470A28A2800A31451400514514005145140051451400E6D2F6585ADB6690187CF6732DEA5B6C4948CA995A5594F98C6EE454DF7F50FF00E9B77FF22FFF00AD2F762170CB374B6A95F0A90FA0798293FA0A6BD76436AC93E9E4B9AFB3B5CB5FE16167FEA6AAEC3892273A962334A75D282ADD4F3C049513E8013566F512FB3D37735E71BB0DD3FF0043506D9368F4C1B3AAF539A1ED13DBDD692A1F0327F7573F2C52CE3E9A46A7484C03919A2BD57582AB65DA64058C18CFADAF92881F4C5796B95940A28A2800A28A2800A28A2800A28A2803D56CB6C8BBDC1B811021521EC86D2B504EF9033BA09E1938E15F1360CBB74A5459B19D8CFA3E26DD4949AF88D25E8729A951D5B8F32B0E36A1D140E47E95635B8D65D75A6A24B9B09A92CC9682D3BC3DE6D5D40573041C8E1DD548414918DD0A2D935C3D8B5CB0D156133195B27CF1BC3EA9FAD3F37877D2AA4ECAE6586FB0EF1A7A47B5371A421D319E2038003C40572570CF3C7AD357753DD57C69A54C9B354B8ACCE88EC5909DF65E414389CFC493C08AD8942508094A425206001C80AFBAC550C2BFED56DDEC1AEA4B813BA898DA1F1E646E9FAA6A1D4DADB75BB2D5AEE694FC2A5C759F31BC9FD15503D21A4A66ADBA88AC65B8CDE15224638369F0EF51E83F6AE49C7ED48A27A38C224930CCC0C39ECC970345EDD3BA16464273DF8AD5565D5A4ED074C2B4EA6304C12DEE6E8F8B3F8F3F8B3C73DF55EF50D865E9BBD3F6C9832A6CE50B0301C41E4A1E7F439144F1F904ECE6515BE14291719CCC288D175F7D610DA075269FB6ED9D5958D22DD866C74C827EF1D7C7059748E2A49E631C8780AC841C8D6E8AF4738E0327A0A91EAAD1772D2A63BB2125D8B21092979238256464A15DC471C778AEF3BB31B95A758DB23A92A976C7E52712529F8520EF10B1D0E0791FA53A2E16F897582EC29CC25F8EF2775685F223F63E34F1C769D8AE4557A9AECE34546D5AEDC173CBA88CC361085B6AC10E2B883E3800F0F115E4D73A1A56919BDA2379FB6BCAFB97C8E293F817DC7C7AD37B67164FB1346C36DC4EEBF247B43BDF957103D13814421F6A66B7A14FAAB66B7AD39BF21A419F053C7B7653EF207E74F31E6323CAA5BB16BF769165D89D5E4B47DA18C9FE13C163D0E0FF0035353151D73455ADBD431EFB6F47B0CD6944B8591843E9230A0A4F2E3DE3AF7D51429DA16F448AB358159AA8A1451450047B5BE9B56A9D3ABB6B4E21A77B66DC42D7C9385713FD2555EDD3F6083A6ED4D5BA037BADA38A967E2715D54A3D49A28ACA5760752A21B43D1C9D5566DF8E948B8C50551D5CB7FBD04F71E9DC71E34514356A80E1ECA345AADB14DFAE2C944B7D2531DB58C169BEA48E855FA79D32A8A2B22A91AC3145145318689B062DC62391263087D874616DAC642856F4A424000600E94514019A28A2800A28A2803FFFD9") - - val writeTask = WriteIDCardTask(ctx.card, cardProtocol!!.reader, App.localStorage, App.pinStorage, idCardData, ctx.card.issuer.privateDataKey, - object : CardProtocol.Notifications { - override fun onReadProgress(cardProtocol: CardProtocol?, progress: Int) { - LOG.i("WriteIDCardTask", "onReadProgress: ${progress}%") - } - - override fun onReadWait(msec: Int) { - } - - override fun onReadStart(cardProtocol: CardProtocol?) { - LOG.i("WriteIDCardTask", "onReadStart") - rlProgressBar?.post { rlProgressBar?.visibility = View.VISIBLE } - - } - - override fun onReadCancel() { - LOG.i("WriteIDCardTask", "onReadCancel") - rlProgressBar?.postDelayed({ - try { - rlProgressBar?.visibility = View.GONE - } catch (e: Exception) { - e.printStackTrace() - } - }, 500) - - } - - override fun onReadFinish(cardProtocol: CardProtocol?) { - LOG.i("WriteIDCardTask", "onReadFinish") - rlProgressBar?.postDelayed({ - try { - rlProgressBar?.visibility = View.GONE - } catch (e: Exception) { - e.printStackTrace() - } - }, 500) - - } - - override fun onReadAfterRequest() { - } - - override fun onReadBeforeRequest(timeout: Int) { - } - }) - writeTask.start() - return - } - //TODO end - (activity as MainActivity).nfcManager.ignoreTag(isoDep.tag) + (activity as? MainActivity)?.nfcManager?.ignoreTag(isoDep.tag) return } @@ -902,7 +942,7 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte val chooser = Intent.createChooser(intent, getString(R.string.loaded_wallet_chooser_share)) // verify the intent will resolve to at least one activity - if (intent.resolveActivity(activity!!.packageManager) != null) { + if (intent.resolveActivity(requireActivity().packageManager) != null) { startActivity(chooser) } } else { @@ -911,11 +951,14 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte Toast.makeText(activity, R.string.loaded_wallet_toast_copied, Toast.LENGTH_LONG).show() } } else { - val txtShare = ctx.coinData.wallet - val clipboard = activity?.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager - clipboard.primaryClip = ClipData.newPlainText(txtShare, txtShare) - Toast.makeText(activity, R.string.loaded_wallet_toast_copied, Toast.LENGTH_LONG).show() + copyText(ctx.coinData.wallet) } } + private fun copyText(text: String) { + val clipboard = activity?.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + clipboard.primaryClip = ClipData.newPlainText(text, text) + Toast.makeText(activity, R.string.loaded_wallet_toast_copied, Toast.LENGTH_LONG).show() + } + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletViewModel.kt b/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletViewModel.kt index fe020d4831..fb1191f6b1 100644 --- a/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletViewModel.kt +++ b/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletViewModel.kt @@ -1,15 +1,19 @@ package com.tangem.ui.fragment.wallet -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel +import androidx.lifecycle.* import com.tangem.data.network.ServerApiCommon import com.tangem.data.network.ServerApiCommon.RateInfoListener import com.tangem.data.network.model.RateInfoResponse +import com.tangem.server_android.PayIdResponse +import com.tangem.server_android.PayIdService +import com.tangem.server_android.Result +import com.tangem.server_android.SetPayIdResponse import com.tangem.wallet.TangemContext +import kotlinx.coroutines.CoroutineExceptionHandler class LoadedWalletViewModel : ViewModel() { - private var serverApiCommon: ServerApiCommon = ServerApiCommon() + private val serverApiCommon: ServerApiCommon = ServerApiCommon() + private val payIdService = PayIdService() private lateinit var ctx: TangemContext private val rate: MutableLiveData by lazy { MutableLiveData().also { @@ -17,6 +21,10 @@ class LoadedWalletViewModel : ViewModel() { } } + private val scope = viewModelScope.coroutineContext + CoroutineExceptionHandler { _, ex -> + throw ex + } + fun getRateInfo(): LiveData { return rate } @@ -28,7 +36,7 @@ class LoadedWalletViewModel : ViewModel() { } private fun loadRateInfo() { - serverApiCommon.setRateInfoListener(object: RateInfoListener{ + serverApiCommon.setRateInfoListener(object : RateInfoListener { override fun onSuccess(rateInfoResponse: RateInfoResponse?) { rate.value = rateInfoResponse!!.data!!.quote!!.usd!!.price } @@ -38,4 +46,31 @@ class LoadedWalletViewModel : ViewModel() { } }) } + + fun getPayId(cardId: String, publicKey: String): LiveData> { + return liveData(viewModelScope.coroutineContext) { + emit( + payIdService.getPayId( + cardId, + publicKey + ) + ) + } + } + + fun setPayId( + cardId: String, + publicKey: String, + payId: String, + address: String, + network: String + ): LiveData> { + return liveData(scope) { + emit( + payIdService.setPayId( + cardId, publicKey, payId, address, network + ) + ) + } + } } \ No newline at end of file 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 28648010df..1a892adee9 100644 --- a/app/src/main/java/com/tangem/wallet/btc/BtcData.java +++ b/app/src/main/java/com/tangem/wallet/btc/BtcData.java @@ -22,6 +22,8 @@ public class BtcData extends CoinData { //for blockchain.info private boolean hasUnconfirmed = false; + private String resolvedPayIdAddress = null; + public Unspents getUnspentInputsDescription() { try { int gatheredUnspents = 0; @@ -90,6 +92,8 @@ public class BtcData extends CoinData { } if (B.containsKey("UseBlockcypher")) useBlockcypher = B.getBoolean("UseBlockcypher"); if (B.containsKey("HasUnconfirmed")) useBlockcypher = B.getBoolean("HasUnconfirmed"); + if (B.containsKey("ResolvedPayIdAddress")) resolvedPayIdAddress = B.getString("ResolvedPayIdAddress"); + else resolvedPayIdAddress = null; } @Override @@ -107,6 +111,7 @@ public class BtcData extends CoinData { if (balanceUnconfirmed != null) B.putLong("BalanceUnconfirmed", balanceUnconfirmed); if (useBlockcypher) B.putBoolean("UseBlockcypher", true); if (hasUnconfirmed) B.putBoolean("HasUnconfirmed", true); + if (resolvedPayIdAddress != null) B.putString("ResolvedPayIdAddress", resolvedPayIdAddress); } catch (Exception e) { Log.e("Can't save to bundle ", e.getMessage()); } @@ -119,6 +124,7 @@ public class BtcData extends CoinData { balanceUnconfirmed = null; unspentTransactions = null; hasUnconfirmed = false; + resolvedPayIdAddress = null; } public CoinEngine.InternalAmount getBalanceInInternalUnits() { @@ -160,4 +166,12 @@ public class BtcData extends CoinData { public void setHasUnconfirmed(boolean hasUnconfirmed) { this.hasUnconfirmed = hasUnconfirmed; } + + public String getResolvedPayIdAddress() { + return resolvedPayIdAddress; + } + + public void setResolvedPayIdAddress(String resolvedPayIdAddress) { + this.resolvedPayIdAddress = resolvedPayIdAddress; + } } 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 e9782bcf5d..df6b877f98 100644 --- a/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java +++ b/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java @@ -11,6 +11,7 @@ import com.tangem.data.network.Server; import com.tangem.data.network.ServerApiBlockchainInfo; import com.tangem.data.network.ServerApiBlockcypher; import com.tangem.data.network.ServerApiCommon; +import com.tangem.data.network.ServerApiPayId; import com.tangem.data.network.model.BlockchainInfoAddress; import com.tangem.data.network.model.BlockchainInfoAddressAndUnspents; import com.tangem.data.network.model.BlockchainInfoInput; @@ -21,6 +22,8 @@ import com.tangem.data.network.model.BlockcypherFee; import com.tangem.data.network.model.BlockcypherResponse; import com.tangem.data.network.model.BlockcypherTx; import com.tangem.data.network.model.BlockcypherTxref; +import com.tangem.data.network.model.PayIdAddress; +import com.tangem.data.network.model.PayIdResponse; import com.tangem.tangem_card.data.TangemCard; import com.tangem.tangem_card.reader.CardProtocol; import com.tangem.tangem_card.tasks.SignTask; @@ -47,6 +50,7 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; +import java.net.URL; import java.nio.ByteBuffer; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; @@ -54,8 +58,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import io.reactivex.CompletableObserver; import io.reactivex.Single; import io.reactivex.SingleObserver; +import io.reactivex.observers.DisposableCompletableObserver; import io.reactivex.observers.DisposableSingleObserver; import okhttp3.ResponseBody; @@ -154,6 +160,21 @@ public class BtcEngine extends CoinEngine { return false; } + if (address.contains("$")) { // PayID + String[] addressParts = address.split("\\$"); + + if (addressParts.length != 2) { + return false; + } + String addressURL = "https://" + addressParts[1] + "/" + addressParts[0]; + try { + new URL(addressURL).toURI(); + return true; + } catch (Exception e) { + return false; + } + } + if (address.startsWith("1") || address.startsWith("2") || address.startsWith("3") || address.startsWith("n") || address.startsWith("m")) { if (address.length() < 25) { return false; @@ -458,6 +479,14 @@ public class BtcEngine extends CoinEngine { String myAddress = ctx.getCoinData().getWallet(); byte[] pbKey = ctx.getCard().getWalletPublicKey(); + String destination; + //PayID + if (coinData.getResolvedPayIdAddress() != null) { + destination = coinData.getResolvedPayIdAddress(); + } else { + destination = targetAddress; + } + // /blockcypher/ // 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)); @@ -502,7 +531,7 @@ public class BtcEngine extends CoinEngine { 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); + txForSign[i] = BTCUtils.buildTXForSign(myAddress, destination, myAddress, unspentOutputs, i, amount, change); bodyHash[i] = Util.calculateSHA256(txForSign[i]); bodyDoubleHash[i] = Util.calculateSHA256(bodyHash[i]); } @@ -555,7 +584,7 @@ public class BtcEngine extends CoinEngine { unspentOutputs.get(i).scriptForBuild = DerEncodingUtil.packSignDer(r, s, pbKey); } - byte[] txForSend = BTCUtils.buildTXForSend(targetAddress, myAddress, unspentOutputs, amountFinal, changeFinal); + byte[] txForSend = BTCUtils.buildTXForSend(destination, myAddress, unspentOutputs, amountFinal, changeFinal); notifyOnNeedSendTransaction(txForSend); return txForSend; } @@ -877,8 +906,6 @@ public class BtcEngine extends CoinEngine { @Override 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; @@ -886,6 +913,26 @@ public class BtcEngine extends CoinEngine { if (!coinData.isUseBlockcypher()) { final ServerApiCommon serverApiCommon = new ServerApiCommon(); + final Integer[] calcSize = new Integer[1]; + + CompletableObserver payIdObserver = new DisposableCompletableObserver() { + @Override + public void onComplete() { + calcSize[0] = calculateEstimatedTransactionSize(coinData.getResolvedPayIdAddress(), amount.toValueString()); + Log.e(TAG, String.format("Estimated tx size %d", calcSize[0])); + + serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_PRIORITY); + serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_NORMAL); + serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_MINIMAL); + } + + @Override + public void onError(Throwable e) { + ctx.setError(e.getMessage()); + blockchainRequestsCallbacks.onComplete(false); + } + }; + final ServerApiCommon.EstimatedFeeListener estimatedFeeListener = new ServerApiCommon.EstimatedFeeListener() { @Override public void onSuccess(int blockCount, String estimateFeeResponse) { @@ -898,8 +945,8 @@ public class BtcEngine extends CoinEngine { return; } - if (calcSize != 0) { - fee = fee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024), BigDecimal.ROUND_DOWN); // per Kb -> per byte + if (calcSize[0] != 0) { + fee = fee.multiply(new BigDecimal(calcSize[0])).divide(new BigDecimal(1024), BigDecimal.ROUND_DOWN); // per Kb -> per byte } else { if (blockchainRequestsCallbacks.allowAdvance()) { serverApiCommon.requestBtcEstimatedFee(blockCount); @@ -948,13 +995,38 @@ public class BtcEngine extends CoinEngine { }; serverApiCommon.setBtcEstimatedFeeListener(estimatedFeeListener); - serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_PRIORITY); - serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_NORMAL); - serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_MINIMAL); + if (targetAddress.contains("$")) { // PayID + resolvePayID(targetAddress, payIdObserver); + } else { + calcSize[0] = calculateEstimatedTransactionSize(targetAddress, amount.toValueString()); + Log.e(TAG, String.format("Estimated tx size %d", calcSize[0])); + + serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_PRIORITY); + serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_NORMAL); + serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_MINIMAL); + } } else { final ServerApiBlockcypher serverApiBlockcypher = new ServerApiBlockcypher(); + final Integer[] calcSize = new Integer[1]; + + CompletableObserver payIdObserver = new DisposableCompletableObserver() { + @Override + public void onComplete() { + calcSize[0] = calculateEstimatedTransactionSize(coinData.getResolvedPayIdAddress(), amount.toValueString()); + Log.e(TAG, String.format("Estimated tx size %d", calcSize[0])); + + serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_FEE, "", ""); + } + + @Override + public void onError(Throwable e) { + ctx.setError(e.getMessage()); + blockchainRequestsCallbacks.onComplete(false); + } + }; + ServerApiBlockcypher.ResponseListener blockcypherListener = new ServerApiBlockcypher.ResponseListener() { @Override public void onSuccess(String method, BlockcypherResponse blockcypherResponse) { @@ -970,9 +1042,9 @@ public class BtcEngine extends CoinEngine { BigDecimal normalByteFee = new BigDecimal(blockcypherFee.getMedium_fee_per_kb()).divide(BigDecimal.valueOf(1024)); BigDecimal maxByteFee = new BigDecimal(blockcypherFee.getHigh_fee_per_kb()).divide(BigDecimal.valueOf(1024)); - CoinEngine.InternalAmount minIntAmount = new CoinEngine.InternalAmount(minByteFee.multiply(BigDecimal.valueOf(calcSize)), "satoshi"); - CoinEngine.InternalAmount normalIntAmount = new CoinEngine.InternalAmount(normalByteFee.multiply(BigDecimal.valueOf(calcSize)), "satoshi"); - CoinEngine.InternalAmount maxIntAmount = new CoinEngine.InternalAmount(maxByteFee.multiply(BigDecimal.valueOf(calcSize)), "satoshi"); + CoinEngine.InternalAmount minIntAmount = new CoinEngine.InternalAmount(minByteFee.multiply(BigDecimal.valueOf(calcSize[0])), "satoshi"); + CoinEngine.InternalAmount normalIntAmount = new CoinEngine.InternalAmount(normalByteFee.multiply(BigDecimal.valueOf(calcSize[0])), "satoshi"); + CoinEngine.InternalAmount maxIntAmount = new CoinEngine.InternalAmount(maxByteFee.multiply(BigDecimal.valueOf(calcSize[0])), "satoshi"); coinData.minFee = convertToAmount(minIntAmount); coinData.normalFee = convertToAmount(normalIntAmount); @@ -999,10 +1071,57 @@ public class BtcEngine extends CoinEngine { serverApiBlockcypher.setResponseListener(blockcypherListener); - serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_FEE, "", ""); + if (targetAddress.contains("$")) { // PayID + resolvePayID(targetAddress, payIdObserver); + } else { + calcSize[0] = calculateEstimatedTransactionSize(targetAddress, amount.toValueString()); + Log.e(TAG, String.format("Estimated tx size %d", calcSize[0])); + + serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_FEE, "", ""); + } } } + private void resolvePayID(String targetAddress, CompletableObserver observer) { + final ServerApiPayId serverApiPayId = new ServerApiPayId(); + + SingleObserver payIdObserver = new DisposableSingleObserver() { + @Override + public void onSuccess(PayIdResponse payIdResponse) { + try { + String resolvedAddress = null; + for (PayIdAddress address : payIdResponse.getAddresses()) { + if (address.getPaymentNetwork().equals("BTC") && + address.getEnvironment().equals("MAINNET")) { + resolvedAddress = address.getAddressDetails().getAddress(); + break; + } + } + if (validateAddress(resolvedAddress)) { + if (resolvedAddress.equals(coinData.getWallet())) { + observer.onError(new Exception(ctx.getString(R.string.prepare_transaction_error_same_address))); + } else { + coinData.setResolvedPayIdAddress(resolvedAddress); + observer.onComplete(); + } + } else { + observer.onError(new Exception("Unknown address format in PayID response")); + } + } catch (Exception e) { + observer.onError(new Exception("Unknown response format on PayID request")); + } + } + + @Override + public void onError(Throwable e) { + Log.i(TAG, "onFail: " + "payID" + " " + e.getMessage()); + observer.onError(new Exception("PayID error:" + e.getMessage())); + } + }; + + serverApiPayId.getAddress(targetAddress, ctx.getBlockchain(), payIdObserver); + } + @Override public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) throws Exception { diff --git a/app/src/main/java/com/tangem/wallet/eth/EthData.java b/app/src/main/java/com/tangem/wallet/eth/EthData.java index 2135c886a1..73511313ca 100644 --- a/app/src/main/java/com/tangem/wallet/eth/EthData.java +++ b/app/src/main/java/com/tangem/wallet/eth/EthData.java @@ -14,6 +14,8 @@ public class EthData extends CoinData { private BigInteger countConfirmedTX = null; private BigInteger countUnconfirmedTX = BigInteger.valueOf(0); + private String resolvedPayIdAddress = null; + public BigInteger getConfirmedTXCount() { if (countConfirmedTX == null) { countConfirmedTX = BigInteger.valueOf(0); @@ -46,6 +48,7 @@ public class EthData extends CoinData { public void clearInfo() { super.clearInfo(); balance = null; + resolvedPayIdAddress = null; } public CoinEngine.InternalAmount getBalanceInInternalUnits() { @@ -57,6 +60,13 @@ public class EthData extends CoinData { balance = value; } + public String getResolvedPayIdAddress() { + return resolvedPayIdAddress; + } + + public void setResolvedPayIdAddress(String resolvedPayIdAddress) { + this.resolvedPayIdAddress = resolvedPayIdAddress; + } @Override public void loadFromBundle(Bundle B) { @@ -73,6 +83,8 @@ public class EthData extends CoinData { countConfirmedTX = new BigInteger(B.getString("confirmTx"), 16); if (B.containsKey("unconfirmTx")) countUnconfirmedTX = new BigInteger(B.getString("unconfirmTx"), 16); + if (B.containsKey("ResolvedPayIdAddress")) resolvedPayIdAddress = B.getString("ResolvedPayIdAddress"); + else resolvedPayIdAddress = null; } @Override @@ -86,6 +98,7 @@ public class EthData extends CoinData { B.putString("confirmTx", getConfirmedTXCount().toString(16)); B.putString("unconfirmTx", getUnconfirmedTXCount().toString(16)); + if (resolvedPayIdAddress != null) B.putString("ResolvedPayIdAddress", resolvedPayIdAddress); } catch (Exception e) { Log.e("Can't save to bundle ", e.getMessage()); diff --git a/app/src/main/java/com/tangem/wallet/eth/EthEngine.java b/app/src/main/java/com/tangem/wallet/eth/EthEngine.java index 4c31f74772..0567b5bf47 100644 --- a/app/src/main/java/com/tangem/wallet/eth/EthEngine.java +++ b/app/src/main/java/com/tangem/wallet/eth/EthEngine.java @@ -9,10 +9,13 @@ import com.tangem.Constant; import com.tangem.data.Blockchain; import com.tangem.data.network.ServerApiBlockcypher; import com.tangem.data.network.ServerApiInfura; +import com.tangem.data.network.ServerApiPayId; import com.tangem.data.network.model.BlockcypherFee; import com.tangem.data.network.model.BlockcypherResponse; import com.tangem.data.network.model.BlockcypherTxref; import com.tangem.data.network.model.InfuraResponse; +import com.tangem.data.network.model.PayIdAddress; +import com.tangem.data.network.model.PayIdResponse; import com.tangem.tangem_card.data.TangemCard; import com.tangem.tangem_card.tasks.SignTask; import com.tangem.util.CryptoUtil; @@ -33,8 +36,12 @@ import org.bitcoinj.core.ECKey; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; +import java.net.URL; import java.util.Arrays; +import io.reactivex.SingleObserver; +import io.reactivex.observers.DisposableSingleObserver; + /** * Created by Ilia on 15.02.2018. */ @@ -137,6 +144,21 @@ public class EthEngine extends CoinEngine { return false; } + if (address.contains("$")) { // PayID + String[] addressParts = address.split("\\$"); + + if (addressParts.length != 2) { + return false; + } + String addressURL = "https://" + addressParts[1] + "/" + addressParts[0]; + try { + new URL(addressURL).toURI(); + return true; + } catch (Exception e) { + return false; + } + } + if (!address.startsWith("0x") && !address.startsWith("0X")) { return false; } @@ -266,7 +288,7 @@ public class EthEngine extends CoinEngine { @Override public boolean checkNewTransactionAmount(Amount amount) { - if( BuildConfig.FLAVOR== Constant.FLAVOR_TANGEM_CARDANO ) { + if (BuildConfig.FLAVOR == Constant.FLAVOR_TANGEM_CARDANO) { return true; } Amount balance = getBalance(); @@ -387,6 +409,14 @@ public class EthEngine extends CoinEngine { Log.e(TAG, "Construct transaction " + amountValue.toString() + " with fee " + feeValue.toString() + (IncFee ? " including" : " excluding")); + String destination; + //PayID + if (coinData.getResolvedPayIdAddress() != null) { + destination = coinData.getResolvedPayIdAddress(); + } else { + destination = targetAddress; + } + BigInteger nonceValue = coinData.getConfirmedTXCount(); byte[] pbKey = ctx.getCard().getWalletPublicKey(); @@ -401,13 +431,12 @@ public class EthEngine extends CoinEngine { BigInteger gasLimit = BigInteger.valueOf(21000); Integer chainId = this.getChainIdNum(); - String to = targetAddress; - if (to.startsWith("0x") || to.startsWith("0X")) { - to = to.substring(2); + if (destination.startsWith("0x") || destination.startsWith("0X")) { + destination = destination.substring(2); } - final EthTransaction tx = EthTransaction.create(to, weiAmount, nonceValue, gasPrice, gasLimit, chainId); + final EthTransaction tx = EthTransaction.create(destination, weiAmount, nonceValue, gasPrice, gasLimit, chainId); return new SignTask.TransactionToSign() { @Override @@ -510,7 +539,7 @@ public class EthEngine extends CoinEngine { break; } - if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) { + if (serverApiInfura.isRequestsSequenceCompleted() && serverApiBlockcypher.isRequestsSequenceCompleted()) { blockchainRequestsCallbacks.onComplete(!ctx.hasError()); } else { blockchainRequestsCallbacks.onProgress(); @@ -521,7 +550,7 @@ public class EthEngine extends CoinEngine { public void onFail(String method, String message) { Log.e(TAG, "onFail: " + method + " " + message); ctx.setError(message); - if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) { + if (serverApiInfura.isRequestsSequenceCompleted() && serverApiBlockcypher.isRequestsSequenceCompleted()) { blockchainRequestsCallbacks.onComplete(false); } else { blockchainRequestsCallbacks.onProgress(); @@ -556,7 +585,7 @@ public class EthEngine extends CoinEngine { Log.e(TAG, "FAIL BLOCKCYPHER_ADDRESS Exception"); } - if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) { + if (serverApiInfura.isRequestsSequenceCompleted() && serverApiBlockcypher.isRequestsSequenceCompleted()) { blockchainRequestsCallbacks.onComplete(!ctx.hasError()); } else { blockchainRequestsCallbacks.onProgress(); @@ -565,7 +594,7 @@ public class EthEngine extends CoinEngine { public void onSuccess(String method, BlockcypherFee blockcypherFee) { Log.e(TAG, "Wrong response type for requestBalanceAndUnspentTransactions"); - if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) { + if (serverApiInfura.isRequestsSequenceCompleted() && serverApiBlockcypher.isRequestsSequenceCompleted()) { blockchainRequestsCallbacks.onComplete(!ctx.hasError()); } else { blockchainRequestsCallbacks.onProgress(); @@ -575,7 +604,7 @@ public class EthEngine extends CoinEngine { @Override public void onFail(String method, String message) { Log.i(TAG, "onFail: " + method + " " + message); - if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) { + if (serverApiInfura.isRequestsSequenceCompleted() && serverApiBlockcypher.isRequestsSequenceCompleted()) { blockchainRequestsCallbacks.onComplete(!ctx.hasError()); } else { blockchainRequestsCallbacks.onProgress(); @@ -593,6 +622,8 @@ public class EthEngine extends CoinEngine { @Override public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) { ServerApiInfura serverApiInfura = new ServerApiInfura(ctx.getBlockchain()); + final ServerApiPayId serverApiPayId = new ServerApiPayId(); + // request requestData eth gasPrice listener ServerApiInfura.ResponseListener responseListener = new ServerApiInfura.ResponseListener() { @Override @@ -623,7 +654,11 @@ public class EthEngine extends CoinEngine { Log.i(TAG, "normal fee: " + coinData.normalFee.toString()); Log.i(TAG, "max fee : " + coinData.maxFee.toString()); - blockchainRequestsCallbacks.onComplete(true); + if (serverApiInfura.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError()); + } else { + blockchainRequestsCallbacks.onProgress(); + } } @Override @@ -634,6 +669,57 @@ public class EthEngine extends CoinEngine { }; serverApiInfura.setResponseListener(responseListener); + if (targetAddress.contains("$")) { // PayID + + SingleObserver observer = new DisposableSingleObserver() { + @Override + public void onSuccess(PayIdResponse payIdResponse) { + try { + String resolvedAddress = null; + for (PayIdAddress address : payIdResponse.getAddresses()) { + if (address.getPaymentNetwork().equals("ETH") && + address.getEnvironment().equals("MAINNET")) { + resolvedAddress = address.getAddressDetails().getAddress(); + break; + } + } + if (validateAddress(resolvedAddress)) { + if (resolvedAddress.equals(coinData.getWallet())) { + ctx.setError(R.string.prepare_transaction_error_same_address); + blockchainRequestsCallbacks.onComplete(false); + } else { + coinData.setResolvedPayIdAddress(resolvedAddress); + if (serverApiInfura.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError()); + } else { + blockchainRequestsCallbacks.onProgress(); + } + } + } else { + ctx.setError("Unknown address format in PayID response"); + blockchainRequestsCallbacks.onComplete(false); + } + } catch (Exception e) { + ctx.setError("Unknown response format on PayID request"); + blockchainRequestsCallbacks.onComplete(false); + } + if (serverApiInfura.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError()); + } else { + blockchainRequestsCallbacks.onProgress(); + } + } + + @Override + public void onError(Throwable e) { + Log.i(TAG, "onFail: " + "payID" + " " + e.getMessage()); + ctx.setError("PayID error:" + e.getMessage()); + blockchainRequestsCallbacks.onComplete(false); + } + }; + + serverApiPayId.getAddress(targetAddress, ctx.getBlockchain(), observer); + } serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_GAS_PRICE, 67, coinData.getWallet(), "", ""); } @@ -648,7 +734,7 @@ public class EthEngine extends CoinEngine { @Override public void onSuccess(String method, InfuraResponse infuraResponse) { if (method.equals(ServerApiInfura.INFURA_ETH_SEND_RAW_TRANSACTION)) { - if (infuraResponse.getResult()==null || infuraResponse.getResult().isEmpty()) { + if (infuraResponse.getResult() == null || infuraResponse.getResult().isEmpty()) { ctx.setError("Rejected by node: " + infuraResponse.getError()); blockchainRequestsCallbacks.onComplete(false); } else { @@ -675,7 +761,9 @@ public class EthEngine extends CoinEngine { serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_SEND_RAW_TRANSACTION, 67, coinData.getWallet(), "", txStr); } - public int pendingTransactionTimeoutInSeconds() { return 10; } + public int pendingTransactionTimeoutInSeconds() { + return 10; + } @Override public boolean needMultipleLinesForBalance() { diff --git a/app/src/main/java/com/tangem/wallet/xrp/XrpBase58.java b/app/src/main/java/com/tangem/wallet/xrp/XrpBase58.java deleted file mode 100644 index c149776f2c..0000000000 --- a/app/src/main/java/com/tangem/wallet/xrp/XrpBase58.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.tangem.wallet.xrp; - -import java.math.BigInteger; - -/** - * Created by Ilia on 15.02.2018. - */ - -public class XrpBase58 { - private static final char[] BASE58 = "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz".toCharArray(); - - private static final int BASE58_CHUNK_DIGITS = 10;//how many base 58 digits fits in long - private static final BigInteger BASE58_CHUNK_MOD = BigInteger.valueOf(0x5fa8624c7fba400L); //58^BASE58_CHUNK_DIGITS - private static final byte[] BASE58_VALUES = new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, -1, -1, -1, -1, -1, - -1, 9, 10, 11, 12, 13, 14, 15, 16, -1, 17, 18, 19, 20, 21, -1, - 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1, - -1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, 46, - 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; - - public static byte[] decodeBase58(String input) { - if (input == null) { - return null; - } - input = input.trim(); - if (input.length() == 0) { - return new byte[0]; - } - BigInteger resultNum = BigInteger.ZERO; - int nLeadingZeros = 0; - while (nLeadingZeros < input.length() && input.charAt(nLeadingZeros) == BASE58[0]) { - nLeadingZeros++; - } - long acc = 0; - int nDigits = 0; - int p = nLeadingZeros; - while (p < input.length()) { - int v = BASE58_VALUES[input.charAt(p) & 0xff]; - if (v >= 0) { - acc *= 58; - acc += v; - nDigits++; - if (nDigits == BASE58_CHUNK_DIGITS) { - resultNum = resultNum.multiply(BASE58_CHUNK_MOD).add(BigInteger.valueOf(acc)); - acc = 0; - nDigits = 0; - } - p++; - } else { - break; - } - } - if (nDigits > 0) { - long mul = 58; - while (--nDigits > 0) { - mul *= 58; - } - resultNum = resultNum.multiply(BigInteger.valueOf(mul)).add(BigInteger.valueOf(acc)); - } - final int BASE58_SPACE = -2; - while (p < input.length() && BASE58_VALUES[input.charAt(p) & 0xff] == BASE58_SPACE) { - p++; - } - if (p < input.length()) { - return null; - } - byte[] plainNumber = resultNum.toByteArray(); - int plainNumbersOffs = plainNumber[0] == 0 ? 1 : 0; - byte[] result = new byte[nLeadingZeros + plainNumber.length - plainNumbersOffs]; - System.arraycopy(plainNumber, plainNumbersOffs, result, nLeadingZeros, plainNumber.length - plainNumbersOffs); - return result; - } - - public static String encodeBase58(byte[] input) { - if (input == null) { - return null; - } - StringBuilder str = new StringBuilder((input.length * 350) / 256 + 1); - BigInteger bn = new BigInteger(1, input); - long rem; - while (true) { - BigInteger[] divideAndRemainder = bn.divideAndRemainder(BASE58_CHUNK_MOD); - bn = divideAndRemainder[0]; - rem = divideAndRemainder[1].longValue(); - if (bn.compareTo(BigInteger.ZERO) == 0) { - break; - } - for (int i = 0; i < BASE58_CHUNK_DIGITS; i++) { - str.append(BASE58[(int) (rem % 58)]); - rem /= 58; - } - } - while (rem != 0) { - str.append(BASE58[(int) (rem % 58)]); - rem /= 58; - } - str.reverse(); - int nLeadingZeros = 0; - while (nLeadingZeros < input.length && input[nLeadingZeros] == 0) { - str.insert(0, BASE58[0]); - nLeadingZeros++; - } - return str.toString(); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/xrp/XrpData.java b/app/src/main/java/com/tangem/wallet/xrp/XrpData.java index ca79936381..09bc6276fa 100644 --- a/app/src/main/java/com/tangem/wallet/xrp/XrpData.java +++ b/app/src/main/java/com/tangem/wallet/xrp/XrpData.java @@ -19,6 +19,8 @@ public class XrpData extends CoinData { private Boolean accountNotFound, targetAccountCreated = false; + private String resolvedPayIdAddress = null; + @Override public void loadFromBundle(Bundle B) { super.loadFromBundle(B); @@ -35,6 +37,8 @@ public class XrpData extends CoinData { else accountNotFound = false; if (B.containsKey("TargetAccountCreated")) targetAccountCreated = B.getBoolean("TargetAccountCreated"); else targetAccountCreated = false; + if (B.containsKey("ResolvedPayIdAddress")) resolvedPayIdAddress = B.getString("ResolvedPayIdAddress"); + else resolvedPayIdAddress = null; } @Override @@ -47,6 +51,7 @@ public class XrpData extends CoinData { if (reserve != null) B.putLong("Reserve", reserve); if (accountNotFound != null) B.putBoolean("AccoundNotFound", accountNotFound); if (targetAccountCreated != null) B.putBoolean("TargetAccountCreated", targetAccountCreated); + if (resolvedPayIdAddress != null) B.putString("ResolvedPayIdAddress", resolvedPayIdAddress); } catch (Exception e) { Log.e("Can't save to bundle ", e.getMessage()); } @@ -61,6 +66,7 @@ public class XrpData extends CoinData { reserve = 20000000L; accountNotFound = false; targetAccountCreated = false; + resolvedPayIdAddress = null; } // balanceUnconfirmed is just the latest balance, it equals balanceConfirmed if no unconfirmed transaction present @@ -124,4 +130,12 @@ public class XrpData extends CoinData { public boolean hasUnconfirmed() { return hasBalanceInfo() && !balanceConfirmed.equals(balanceUnconfirmed); } + + public String getResolvedPayIdAddress() { + return resolvedPayIdAddress; + } + + public void setResolvedPayIdAddress(String resolvedPayIdAddress) { + this.resolvedPayIdAddress = resolvedPayIdAddress; + } } diff --git a/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java b/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java index ff06160ba2..2b5e4ca733 100644 --- a/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java +++ b/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java @@ -10,7 +10,10 @@ import com.ripple.crypto.ecdsa.ECDSASignature; import com.ripple.encodings.addresses.Addresses; import com.ripple.utils.HashUtils; import com.tangem.App; +import com.tangem.data.network.ServerApiPayId; import com.tangem.data.network.ServerApiRipple; +import com.tangem.data.network.model.PayIdAddress; +import com.tangem.data.network.model.PayIdResponse; import com.tangem.data.network.model.RippleResponse; import com.tangem.tangem_card.data.TangemCard; import com.tangem.tangem_card.reader.CardProtocol; @@ -27,8 +30,12 @@ import com.tangem.wallet.TangemContext; import java.math.BigDecimal; import java.math.BigInteger; +import java.net.URL; import java.util.Arrays; +import io.reactivex.SingleObserver; +import io.reactivex.observers.DisposableSingleObserver; + public class XrpEngine extends CoinEngine { private static final String TAG = XrpEngine.class.getSimpleName(); @@ -116,25 +123,26 @@ public class XrpEngine extends CoinEngine { if (address == null || address.isEmpty()) { return false; } + if (address.contains("$")) { // PayID + String[] addressParts = address.split("\\$"); - if (address.length() < 25) { - return false; - } - - if (address.length() > 35) { - return false; - } - - if (!address.startsWith("r")) { - return false; + if (addressParts.length != 2) { + return false; + } + String addressURL = "https://" + addressParts[1] + "/" + addressParts[0]; + try { + new URL(addressURL).toURI(); + return true; + } catch (Exception e) { + return false; + } } try { Addresses.decodeAccountID(address); + return true; } catch (Exception e) { - return false; + return XrpXAddressService.Companion.validate(address); } - - return true; } @Override @@ -342,7 +350,22 @@ public class XrpEngine extends CoinEngine { public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception { checkBlockchainDataExists(); - String amount, fee; + String amount, fee, destination; + Integer destinationTag = null; + + //PayID + if (coinData.getResolvedPayIdAddress() != null) { + destination = coinData.getResolvedPayIdAddress(); + } else { + destination = targetAddress; + } + + //X-address + XrpXAddressDecoded decodedXAddress = XrpXAddressService.Companion.decode(destination); + if (decodedXAddress != null) { + destination = decodedXAddress.getAddress(); + destinationTag = decodedXAddress.getDestinationTag(); + } if (IncFee) { amount = convertToInternalAmount(amountValue).subtract(convertToInternalAmount(feeValue)).setScale(0).toPlainString(); @@ -363,10 +386,13 @@ public class XrpEngine extends CoinEngine { // Put `as` AccountID field Account, `Object` o payment.as(AccountID.Account, coinData.getWallet()); - payment.as(AccountID.Destination, targetAddress); + payment.as(AccountID.Destination, destination); payment.as(com.ripple.core.coretypes.Amount.Amount, amount); payment.as(UInt32.Sequence, coinData.getSequence()); payment.as(com.ripple.core.coretypes.Amount.Fee, fee); + if (destinationTag != null) { + payment.as(UInt32.DestinationTag, destinationTag); + } XrpSignedTransaction signedTx = payment.prepare(canonisePubKey(ctx.getCard().getWalletPublicKeyRar())); @@ -525,13 +551,14 @@ public class XrpEngine extends CoinEngine { @Override public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) { final ServerApiRipple serverApiRipple = new ServerApiRipple(); + final ServerApiPayId serverApiPayId = new ServerApiPayId(); ServerApiRipple.ResponseListener rippleListener = new ServerApiRipple.ResponseListener() { @Override public void onSuccess(String method, RippleResponse rippleResponse) { Log.i(TAG, "onSuccess: " + method); switch (method) { - case ServerApiRipple.RIPPLE_ACCOUNT_INFO: { + case ServerApiRipple.RIPPLE_ACCOUNT_INFO: { //check if target account is created try { if (rippleResponse.getResult().getError_code().equals(19)) { // "Account not found" coinData.setTargetAccountCreated(false); @@ -542,7 +569,7 @@ public class XrpEngine extends CoinEngine { coinData.setTargetAccountCreated(true); //expected behaviour, if account exists, there should be no error code -> null pointer } - if (serverApiRipple.isRequestsSequenceCompleted()) { + if (serverApiRipple.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) { blockchainRequestsCallbacks.onComplete(!ctx.hasError()); } else { blockchainRequestsCallbacks.onProgress(); @@ -566,7 +593,7 @@ public class XrpEngine extends CoinEngine { ctx.setError(e.getMessage()); } - if (serverApiRipple.isRequestsSequenceCompleted()) { + if (serverApiRipple.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) { blockchainRequestsCallbacks.onComplete(!ctx.hasError()); } else { blockchainRequestsCallbacks.onProgress(); @@ -585,9 +612,71 @@ public class XrpEngine extends CoinEngine { }; serverApiRipple.setResponseListener(rippleListener); - - serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, targetAddress, ""); serverApiRipple.requestData(ServerApiRipple.RIPPLE_FEE, "", ""); + + if (targetAddress.contains("$")) { // PayID + + SingleObserver observer = new DisposableSingleObserver() { + @Override + public void onSuccess(PayIdResponse payIdResponse) { + try { + String resolvedAddress = null; + for (PayIdAddress address : payIdResponse.getAddresses()) { + if (address.getPaymentNetwork().equals("XRPL") && + address.getEnvironment().equals("MAINNET")) { + resolvedAddress = address.getAddressDetails().getAddress(); + break; + } + } + if (validateAddress(resolvedAddress)) { + coinData.setResolvedPayIdAddress(resolvedAddress); + + XrpXAddressDecoded xAddressDecoded = XrpXAddressService.Companion.decode(resolvedAddress); + if (xAddressDecoded == null) { // classic address + if (resolvedAddress.equals(coinData.getWallet())) { + ctx.setError(R.string.prepare_transaction_error_same_address); + blockchainRequestsCallbacks.onComplete(false); + } else { + serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, resolvedAddress, ""); + } + } else { // X-address + if (xAddressDecoded.getAddress().equals(coinData.getWallet())) { + ctx.setError(R.string.prepare_transaction_error_same_address); + blockchainRequestsCallbacks.onComplete(false); + } else { + serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, xAddressDecoded.getAddress(), ""); + } + } + } else { + ctx.setError("Unknown address format in PayID response"); + } + } catch (Exception e) { + ctx.setError("Unknown response format on PayID request"); + } + if (serverApiRipple.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError()); + } else { + blockchainRequestsCallbacks.onProgress(); + } + } + + @Override + public void onError(Throwable e) { + Log.i(TAG, "onFail: " + "payID" + " " + e.getMessage()); + ctx.setError("PayID error:" + e.getMessage()); + blockchainRequestsCallbacks.onComplete(false); + } + }; + + serverApiPayId.getAddress(targetAddress, ctx.getBlockchain(), observer); + } else { + XrpXAddressDecoded xAddressDecoded = XrpXAddressService.Companion.decode(targetAddress); + if (xAddressDecoded == null) { // classic address + serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, targetAddress, ""); + } else { // X-address + serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, xAddressDecoded.getAddress(), ""); + } + } } @Override diff --git a/app/src/main/java/com/tangem/wallet/xrp/XrpTaggedAddress.kt b/app/src/main/java/com/tangem/wallet/xrp/XrpTaggedAddress.kt new file mode 100644 index 0000000000..b0e63aa0e0 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/xrp/XrpTaggedAddress.kt @@ -0,0 +1,53 @@ +package com.tangem.wallet.xrp + +import com.ripple.encodings.addresses.Addresses +import com.ripple.encodings.base58.B58 +import org.kethereum.extensions.toBigInteger + +class XrpXAddressService { + companion object { + private val xrpBase58 = B58("rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz") + private val xrpMainnetPrefix = byteArrayOf(0x05, 0x44) + private val zeroTagBytes = ByteArray(4) { 0 } + + fun validate(address: String): Boolean { + return decode(address) != null + } + + fun decode(address: String): XrpXAddressDecoded? { + try { + val addressBytes = xrpBase58.decodeChecked(address) + if (addressBytes.size != 31) return null + + val prefix = addressBytes.slice(0..1).toByteArray() + if (!prefix.contentEquals(xrpMainnetPrefix)) return null + + val accountBytes = addressBytes.slice(2..21).toByteArray() + val classicAddress = Addresses.encodeAccountID(accountBytes) + + val flag = addressBytes[22] + + val tagBytes = addressBytes.slice(23..26).toByteArray() + val reservedTagBytes = addressBytes.slice(27..30).toByteArray() + if (!reservedTagBytes.contentEquals(zeroTagBytes)) return null + + var tag: Int? = null + when (flag) { + 0.toByte() -> if (!tagBytes.contentEquals(zeroTagBytes)) return null + 1.toByte() -> { + tag = tagBytes.reversedArray().toBigInteger().toInt() + } + else -> return null + } + return XrpXAddressDecoded(classicAddress, tag) + } catch (e: Exception) { + return null + } + } + } +} + +data class XrpXAddressDecoded( + val address: String, + val destinationTag: Int? +) \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_payid_logo_dark.xml b/app/src/main/res/drawable/ic_payid_logo_dark.xml new file mode 100644 index 0000000000..d3fd64e0ea --- /dev/null +++ b/app/src/main/res/drawable/ic_payid_logo_dark.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_pay_id.xml b/app/src/main/res/layout/dialog_pay_id.xml new file mode 100644 index 0000000000..e11c21fae7 --- /dev/null +++ b/app/src/main/res/layout/dialog_pay_id.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fr_loaded_wallet.xml b/app/src/main/res/layout/fr_loaded_wallet.xml index 7c19a82299..a2271f089a 100644 --- a/app/src/main/res/layout/fr_loaded_wallet.xml +++ b/app/src/main/res/layout/fr_loaded_wallet.xml @@ -123,19 +123,38 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintHeight_min="wrap" app:layout_constraintTop_toBottomOf="@+id/guideline"> - - + android:orientation="horizontal" + android:layout_gravity="center" + android:gravity="center"> + + + + + + + Show QR-code LOAD + Create PayID + Create + Enter PayID first + New PayID was created + Error creating PayID + + Verified balance Unknown balance @@ -182,6 +189,7 @@ enter wallet address + Address or PayID enter amount Verify Not enough funds diff --git a/app/src/tangemAccess/java/com/tangem/ui/ConfirmTransactionFragment.kt b/app/src/tangemAccess/java/com/tangem/ui/ConfirmTransactionFragment.kt index 8c48003c22..f934c827fc 100644 --- a/app/src/tangemAccess/java/com/tangem/ui/ConfirmTransactionFragment.kt +++ b/app/src/tangemAccess/java/com/tangem/ui/ConfirmTransactionFragment.kt @@ -202,16 +202,15 @@ class ConfirmTransactionFragment : BaseFragment(), NavigationResultListener, Nfc object : CoinEngine.BlockchainRequestsCallbacks { override fun onComplete(success: Boolean) { if (success) { - onProgress() progressBar?.visibility = View.INVISIBLE dtVerified = Date() + doSetFee(rgFee?.checkedRadioButtonId ?: R.id.rbNormalFee) } else { finishWithError(Activity.RESULT_CANCELED, ctx.error) } } override fun onProgress() { - doSetFee(rgFee?.checkedRadioButtonId ?: R.id.rbNormalFee) } override fun allowAdvance(): Boolean { diff --git a/app/src/tangemAccess/java/com/tangem/ui/PrepareTransactionFragment.kt b/app/src/tangemAccess/java/com/tangem/ui/PrepareTransactionFragment.kt index 1b5fa7d2c7..1e75eda210 100644 --- a/app/src/tangemAccess/java/com/tangem/ui/PrepareTransactionFragment.kt +++ b/app/src/tangemAccess/java/com/tangem/ui/PrepareTransactionFragment.kt @@ -2,7 +2,6 @@ package com.tangem.ui import android.app.Activity import android.content.Context -import android.net.Uri import android.nfc.NfcAdapter import android.nfc.Tag import android.os.Build @@ -13,7 +12,6 @@ import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.Toast import com.tangem.Constant -import com.tangem.data.Blockchain import com.tangem.ui.activity.MainActivity import com.tangem.ui.fragment.BaseFragment import com.tangem.ui.fragment.qr.CameraPermissionManager @@ -24,7 +22,6 @@ import com.tangem.wallet.R import com.tangem.wallet.TangemContext import kotlinx.android.synthetic.tangemAccess.fragment_prepare_transaction.* import java.io.IOException -import java.util.* class PrepareTransactionFragment : BaseFragment(), NavigationResultListener, NfcAdapter.ReaderCallback { companion object { @@ -48,6 +45,8 @@ class PrepareTransactionFragment : BaseFragment(), NavigationResultListener, Nfc Html.fromHtml(engine!!.balanceHTML) tvBalance.text = html + if (ctx.blockchain.isPayIdSupported) etWallet.hint = getString(R.string.prepare_transaction_hint_address_or_pay_id) + if (!engine.allowSelectFeeInclusion()) { rgIncFee.visibility = View.INVISIBLE } else { @@ -86,7 +85,7 @@ class PrepareTransactionFragment : BaseFragment(), NavigationResultListener, Nfc } btnVerify.setOnClickListener { - if (!UtilHelper.isOnline(context!!)) { + if (!UtilHelper.isOnline(requireContext())) { Toast.makeText(context, R.string.general_error_no_connection, Toast.LENGTH_LONG).show() return@setOnClickListener } diff --git a/app/src/tangemAccess/java/com/tangem/ui/SignTransactionActivity.kt b/app/src/tangemAccess/java/com/tangem/ui/SignTransactionActivity.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/app/src/tangemAccess/java/com/tangem/ui/SignTransactionFragment.kt b/app/src/tangemAccess/java/com/tangem/ui/SignTransactionFragment.kt index bb5858b8e3..fdcdc64a72 100644 --- a/app/src/tangemAccess/java/com/tangem/ui/SignTransactionFragment.kt +++ b/app/src/tangemAccess/java/com/tangem/ui/SignTransactionFragment.kt @@ -9,7 +9,6 @@ import android.nfc.Tag import android.nfc.tech.IsoDep import android.os.Bundle import android.view.View -import android.widget.Toast import androidx.activity.OnBackPressedCallback import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.crashlytics.FirebaseCrashlytics @@ -80,7 +79,7 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener, mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound) // init NFC Antenna - nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc) + nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc) nfcDeviceAntenna.init() amount = CoinEngine.Amount(arguments?.getString(Constant.EXTRA_AMOUNT), arguments?.getString(Constant.EXTRA_AMOUNT_CURRENCY)) @@ -165,7 +164,7 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener, } override fun onReadStart(cardProtocol: CardProtocol) { - rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE } + rlProgressBar?.post { rlProgressBar.visibility = View.VISIBLE } progressBar?.post { progressBar?.visibility = View.VISIBLE @@ -234,7 +233,9 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener, NoExtendedLengthSupportDialog().show(requireFragmentManager(), NoExtendedLengthSupportDialog.TAG) } } else { - Toast.makeText(context, R.string.general_notification_scan_again, Toast.LENGTH_LONG).show() + (activity as? MainActivity)?.toastHelper?.showSingleToast( + context, getString(R.string.general_notification_scan_again) + ) } progressBar?.progress = 100 progressBar?.progressTintList = ColorStateList.valueOf(Color.RED) @@ -280,7 +281,7 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener, override fun onReadBeforeRequest(timeout: Int) { LOG.i(TAG, "onReadBeforeRequest timeout $timeout") - WaitSecurityDelayDialog.onReadBeforeRequest(activity!!, timeout) + activity?.let { WaitSecurityDelayDialog.onReadBeforeRequest(it, timeout) } // if (!waitSecurityDelayDialogNew.isAdded) // waitSecurityDelayDialogNew.show(supportFragmentManager, WaitSecurityDelayDialogNew.TAG) @@ -293,7 +294,7 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener, override fun onReadAfterRequest() { LOG.i(TAG, "onReadAfterRequest") - WaitSecurityDelayDialog.onReadAfterRequest(activity!!) + activity?.let { WaitSecurityDelayDialog.onReadAfterRequest(it) } // val readAfterRequest = ReadAfterRequest() // EventBus.getDefault().post(readAfterRequest) @@ -301,7 +302,7 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener, override fun onReadWait(msec: Int) { LOG.i(TAG, "onReadWait msec $msec") - WaitSecurityDelayDialog.onReadWait(activity!!, msec) + activity?.let { WaitSecurityDelayDialog.onReadWait(it, msec) } // val readWait = ReadWait() // readWait.msec = msec diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressService.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressService.kt index d4bf305e42..1483284942 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressService.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressService.kt @@ -1,68 +1,62 @@ package com.tangem.blockchain.blockchains.bitcoin +import com.tangem.blockchain.blockchains.litecoin.LitecoinMainNetParams import com.tangem.blockchain.common.AddressService +import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.calculateRipemd160 import com.tangem.common.extensions.calculateSha256 -import org.bitcoinj.core.AddressFormatException -import org.bitcoinj.core.Base58 -import org.bitcoinj.core.SegwitAddress +import org.bitcoinj.core.* import org.bitcoinj.params.MainNetParams import org.bitcoinj.params.TestNet3Params import java.security.MessageDigest -class BitcoinAddressService(private val testNet: Boolean = false): AddressService { +class BitcoinAddressService(private val blockchain: Blockchain) : AddressService { + + private val networkParameters: NetworkParameters = when (blockchain) { + Blockchain.Bitcoin -> MainNetParams() + Blockchain.BitcoinTestnet -> TestNet3Params() + Blockchain.Litecoin -> LitecoinMainNetParams() + else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}") + } + override fun makeAddress(walletPublicKey: ByteArray): String { - val netSelectionByte = if (testNet) 0x6f.toByte() else 0x00.toByte() - val hash1 = walletPublicKey.calculateSha256().calculateRipemd160() - val hash2 = byteArrayOf(netSelectionByte).plus(hash1).calculateSha256().calculateSha256() - val result = byteArrayOf(netSelectionByte) + hash1 + hash2[0] + hash2[1] + hash2[2] + hash2[3] - return Base58.encode(result) - } + val publicKeyHash = walletPublicKey.calculateSha256().calculateRipemd160() + val checksum = byteArrayOf(networkParameters.addressHeader.toByte()).plus(publicKeyHash) + .calculateSha256().calculateSha256() + val result = byteArrayOf(networkParameters.addressHeader.toByte()) + publicKeyHash + checksum.copyOfRange(0, 4) + return Base58.encode(result) + } - override fun validate(address: String): Boolean { - if (firstLetters.contains(address.first())) { - if (testNet && firstLettersNonTestNet.contains(address.first())) return false - if (address.length !in 26..35) return false - val decoded = address.decodeBase58() ?: return false - val hash = recursiveSha256(decoded, 0, 21, 2) - return hash.sliceArray(0..3).contentEquals(decoded.sliceArray(21..24)) - } else { - return validateSegwitAddress(address, testNet) + override fun validate(address: String): Boolean { + return validateLegacyAddress(address) || validateSegwitAddress(address) + } + + private fun validateSegwitAddress(address: String): Boolean { + return try { + when (blockchain) { + Blockchain.Bitcoin -> SegwitAddress.fromBech32(MainNetParams(), address) + Blockchain.BitcoinTestnet -> SegwitAddress.fromBech32(TestNet3Params(), address) + Blockchain.Litecoin -> SegwitAddress.fromBech32(LitecoinMainNetParams(), address) + else -> return false } + true + } catch (e: Exception) { + false } + } - private fun recursiveSha256(data: ByteArray, start: Int, len: Int, recursion: Int): ByteArray { - if (recursion == 0) return data - val md = MessageDigest.getInstance("SHA-256") - md.update(data.sliceArray(start until start + len)) - return recursiveSha256(md.digest(), 0, 32, recursion - 1) - } - - private fun String.decodeBase58(): ByteArray? { - return try { - Base58.decode(this) - } catch (exception: AddressFormatException) { - null + private fun validateLegacyAddress(address: String): Boolean { + return try { + when (blockchain) { + Blockchain.Bitcoin -> LegacyAddress.fromBase58(MainNetParams(), address) + Blockchain.BitcoinTestnet -> LegacyAddress.fromBase58(TestNet3Params(), address) + Blockchain.Litecoin -> LegacyAddress.fromBase58(LitecoinMainNetParams(), address) + else -> return false } + true + } catch (e: Exception) { + false } - - private fun validateSegwitAddress(address: String, testNet: Boolean): Boolean { - return try { - if (testNet) { - SegwitAddress.fromBech32(TestNet3Params(), address) - true - } else { - SegwitAddress.fromBech32(MainNetParams(), address) - true - } - } catch (e: Exception) { - false - } - } - - companion object { - private const val firstLetters = "123nm" - private const val firstLettersNonTestNet = "13" } } \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt index ef0cbe5500..ede62d31a4 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinTransactionBuilder.kt @@ -1,21 +1,30 @@ package com.tangem.blockchain.blockchains.bitcoin +import com.tangem.blockchain.blockchains.litecoin.LitecoinMainNetParams +import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.isZero import org.bitcoinj.core.* import org.bitcoinj.crypto.TransactionSignature +import org.bitcoinj.params.MainNetParams +import org.bitcoinj.params.TestNet3Params import org.bitcoinj.script.Script import org.bitcoinj.script.ScriptBuilder import java.math.BigDecimal import java.math.BigInteger open class BitcoinTransactionBuilder( - private val walletPublicKey: ByteArray, private val testNet: Boolean = false + private val walletPublicKey: ByteArray, blockchain: Blockchain ) { private lateinit var transaction: Transaction - protected var networkParameters: NetworkParameters? = null + protected var networkParameters = when (blockchain) { + Blockchain.Bitcoin, Blockchain.BitcoinCash -> MainNetParams() + Blockchain.BitcoinTestnet -> TestNet3Params() + Blockchain.Litecoin -> LitecoinMainNetParams() + else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}") + } var unspentOutputs: List? = null open fun buildToSign( @@ -25,11 +34,6 @@ open class BitcoinTransactionBuilder( val change: BigDecimal = calculateChange(transactionData, unspentOutputs!!) - networkParameters = if (testNet) { - NetworkParameters.fromID(NetworkParameters.ID_TESTNET) - } else { - NetworkParameters.fromID(NetworkParameters.ID_MAINNET) - } transaction = transactionData.toBitcoinJTransaction(networkParameters, unspentOutputs!!, change) val hashesForSign: MutableList = MutableList(transaction.inputs.size) { byteArrayOf() } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinWalletManager.kt index 0670f32ad4..2a723fe5f9 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinWalletManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinWalletManager.kt @@ -74,11 +74,16 @@ open class BitcoinWalletManager( val minFee = feeResult.data.minimalPerKb.calculateFee(transactionSize) val normalFee = feeResult.data.normalPerKb.calculateFee(transactionSize) val priorityFee = feeResult.data.priorityPerKb.calculateFee(transactionSize) - return Result.Success( - listOf(Amount(minFee, blockchain), - Amount(normalFee, blockchain), - Amount(priorityFee, blockchain)) + val fees = listOf(Amount(minFee, blockchain), + Amount(normalFee, blockchain), + Amount(priorityFee, blockchain) ) + + val minimalFee = transactionSize.movePointLeft(blockchain.decimals()) + for (fee in fees) { + if (fee.value!! < minimalFee) fee.value = minimalFee + } + return Result.Success(fees) } } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BitcoinNetworkManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BitcoinNetworkManager.kt index 8483883d82..c6190937a4 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BitcoinNetworkManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BitcoinNetworkManager.kt @@ -1,26 +1,29 @@ package com.tangem.blockchain.blockchains.bitcoin.network import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput -import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockchainInfoApi -import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockcypherApi -import com.tangem.blockchain.blockchains.bitcoin.network.api.EstimatefeeApi +import com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo.BlockchainInfoApi +import com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo.BlockchainInfoProvider +import com.tangem.blockchain.network.blockcypher.BlockcypherApi +import com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo.EstimatefeeApi +import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.network.API_BLOCKCHAIN_INFO import com.tangem.blockchain.network.API_BLOCKCYPHER import com.tangem.blockchain.network.API_ESTIMATEFEE +import com.tangem.blockchain.network.blockcypher.BlockcypherProvider import com.tangem.blockchain.network.createRetrofitInstance import retrofit2.HttpException import java.io.IOException import java.math.BigDecimal -class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinProvider { +class BitcoinNetworkManager(blockchain: Blockchain) : BitcoinProvider { private val blockcypherProvider by lazy { val api = createRetrofitInstance(API_BLOCKCYPHER) .create(BlockcypherApi::class.java) - BlockcypherProvider(api, isTestNet) + BlockcypherProvider(api, blockchain) } private val blockchainInfoProvider by lazy { @@ -31,10 +34,10 @@ class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinPro BlockchainInfoProvider(api, estimateFeeApi) } - private var bitcoinProvider: BitcoinProvider = blockchainInfoProvider + private var provider: BitcoinProvider = blockchainInfoProvider private fun changeProvider() { - bitcoinProvider = if (bitcoinProvider == blockchainInfoProvider) { + provider = if (provider == blockchainInfoProvider) { blockcypherProvider } else { blockchainInfoProvider @@ -42,13 +45,13 @@ class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinPro } override suspend fun getInfo(address: String): Result { - val result = bitcoinProvider.getInfo(address) + val result = provider.getInfo(address) when (result) { is Result.Success -> return result is Result.Failure -> { if (result.error is IOException || result.error is HttpException) { changeProvider() - return bitcoinProvider.getInfo(address) + return provider.getInfo(address) } else { return result } @@ -57,13 +60,13 @@ class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinPro } override suspend fun getFee(): Result { - val result = bitcoinProvider.getFee() + val result = provider.getFee() when (result) { is Result.Success -> return result is Result.Failure -> { if (result.error is IOException || result.error is HttpException) { changeProvider() - return bitcoinProvider.getFee() + return provider.getFee() } else { return result } @@ -72,13 +75,13 @@ class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinPro } override suspend fun sendTransaction(transaction: String): SimpleResult { - val result = bitcoinProvider.sendTransaction(transaction) + val result = provider.sendTransaction(transaction) when (result) { is SimpleResult.Success -> return result is SimpleResult.Failure -> { if (result.error is IOException || result.error is HttpException) { changeProvider() - return bitcoinProvider.sendTransaction(transaction) + return provider.sendTransaction(transaction) } else { return result } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/api/BlockchainInfoApi.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoApi.kt similarity index 64% rename from blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/api/BlockchainInfoApi.kt rename to blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoApi.kt index 294846b58c..2f1884dcb3 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/api/BlockchainInfoApi.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoApi.kt @@ -1,7 +1,5 @@ -package com.tangem.blockchain.blockchains.bitcoin.network.api +package com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo -import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockchainInfoAddress -import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockchainInfoUnspents import okhttp3.ResponseBody import retrofit2.http.* diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BlockchainInfoProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoProvider.kt similarity index 91% rename from blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BlockchainInfoProvider.kt rename to blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoProvider.kt index cb9dc12e6a..ee177be908 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BlockchainInfoProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoProvider.kt @@ -1,8 +1,9 @@ -package com.tangem.blockchain.blockchains.bitcoin.network +package com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput -import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockchainInfoApi -import com.tangem.blockchain.blockchains.bitcoin.network.api.EstimatefeeApi +import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinAddressResponse +import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinFee +import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinProvider import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/response/BlockchainInfoResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoResponse.kt similarity index 93% rename from blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/response/BlockchainInfoResponse.kt rename to blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoResponse.kt index e7ec0f2734..38e3035552 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/response/BlockchainInfoResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/BlockchainInfoResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.blockchain.blockchains.bitcoin.network.response +package com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/api/EstimateFeeApi.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/EstimateFeeApi.kt similarity index 83% rename from blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/api/EstimateFeeApi.kt rename to blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/EstimateFeeApi.kt index 393e03a00b..43d9cc42f9 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/api/EstimateFeeApi.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/blockchaininfo/EstimateFeeApi.kt @@ -1,4 +1,4 @@ -package com.tangem.blockchain.blockchains.bitcoin.network.api +package com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo import retrofit2.http.GET diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashNetworkManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashNetworkManager.kt index 5a594aa312..d4ad5981bf 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashNetworkManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashNetworkManager.kt @@ -12,7 +12,7 @@ import com.tangem.blockchain.network.blockchair.BlockchairProvider import com.tangem.blockchain.network.createRetrofitInstance class BitcoinCashNetworkManager : BitcoinProvider { - private val blockchain: Blockchain = Blockchain.BitcoinCash + private val blockchain = Blockchain.BitcoinCash private val blockchairProvider by lazy { val api = createRetrofitInstance(API_BLOCKCHAIR).create(BlockchairApi::class.java) diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashTransactionBuilder.kt index 58bec1f6e3..cb3dff45a6 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashTransactionBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashTransactionBuilder.kt @@ -2,6 +2,7 @@ package com.tangem.blockchain.blockchains.bitcoincash import com.tangem.blockchain.blockchains.bitcoin.BitcoinTransactionBuilder import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput +import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.isZero @@ -13,8 +14,8 @@ import org.bitcoinj.script.ScriptBuilder import java.math.BigDecimal import java.math.BigInteger -class BitcoinCashTransactionBuilder(private val walletPublicKey: ByteArray) - : BitcoinTransactionBuilder(walletPublicKey) { +class BitcoinCashTransactionBuilder(private val walletPublicKey: ByteArray, blockchain: Blockchain) + : BitcoinTransactionBuilder(walletPublicKey, blockchain) { private lateinit var transaction: BitcoinCashTransaction @@ -25,7 +26,6 @@ class BitcoinCashTransactionBuilder(private val walletPublicKey: ByteArray) val change: BigDecimal = calculateChange(transactionData, unspentOutputs!!) - networkParameters = NetworkParameters.fromID(NetworkParameters.ID_MAINNET) transaction = transactionData.toBitcoinCashTransaction(networkParameters, unspentOutputs!!, change) val hashesForSign: MutableList = MutableList(transaction.inputs.size) { byteArrayOf() } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashWalletManager.kt index b0793c6cf9..2d2d310ec2 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashWalletManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoincash/BitcoinCashWalletManager.kt @@ -10,8 +10,8 @@ import java.math.BigDecimal class BitcoinCashWalletManager( cardId: String, wallet: Wallet, - private val transactionBuilder: BitcoinCashTransactionBuilder, - private val networkManager: BitcoinCashNetworkManager + transactionBuilder: BitcoinCashTransactionBuilder, + networkManager: BitcoinCashNetworkManager ) : BitcoinWalletManager(cardId, wallet, transactionBuilder, networkManager), TransactionSender { override suspend fun getFee(amount: Amount, destination: String): Result> { val minimalFee = BigDecimal("0.00001") diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumTransactionBuilder.kt index ead68d974b..74d554ccb9 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumTransactionBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/EthereumTransactionBuilder.kt @@ -22,7 +22,7 @@ class EthereumTransactionBuilder(private val walletPublicKey: ByteArray, blockch private val chainId = when (blockchain) { Blockchain.Ethereum -> Chain.Mainnet.id Blockchain.RSK -> Chain.RskMainnet.id - else -> throw Exception("${blockchain.fullName} blockchain is not supported by EthereumTransactionBuilder") + else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}") } fun buildToSign(transactionData: TransactionData, nonce: BigInteger?): TransactionToSign? { diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/network/EthereumNetworkManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/network/EthereumNetworkManager.kt index 0fd3078d40..b5da72dd74 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/network/EthereumNetworkManager.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/ethereum/network/EthereumNetworkManager.kt @@ -23,7 +23,7 @@ class EthereumNetworkManager(blockchain: Blockchain) { val baseUrl = when (blockchain) { Blockchain.Ethereum -> API_INFURA + infuraPath Blockchain.RSK -> API_RSK - else -> throw Exception("${blockchain.fullName} blockchain is not supported by EthereumNetworkManager") + else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}") } createRetrofitInstance(baseUrl).create(EthereumApi::class.java) } @@ -31,7 +31,7 @@ class EthereumNetworkManager(blockchain: Blockchain) { private val apiKey = when (blockchain) { Blockchain.Ethereum -> INFURA_API_KEY Blockchain.RSK -> "" - else -> throw Exception("${blockchain.fullName} blockchain is not supported by EthereumNetworkManager") + else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}") } private val provider: EthereumProvider by lazy { EthereumProvider(api, apiKey) } diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/litecoin/LitecoinMainNetParams.java b/blockchain/src/main/java/com/tangem/blockchain/blockchains/litecoin/LitecoinMainNetParams.java new file mode 100644 index 0000000000..1329dfe69c --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/litecoin/LitecoinMainNetParams.java @@ -0,0 +1,69 @@ +package com.tangem.blockchain.blockchains.litecoin; + +import org.bitcoinj.core.Utils; +import org.bitcoinj.params.AbstractBitcoinNetParams; +import org.bitcoinj.params.MainNetParams; +import org.spongycastle.util.encoders.Hex; + +import static com.google.common.base.Preconditions.checkState; + + +public class LitecoinMainNetParams extends AbstractBitcoinNetParams { + public static final int MAINNET_MAJORITY_WINDOW = MainNetParams.MAINNET_MAJORITY_WINDOW; + public static final int MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED = MainNetParams.MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED; + public static final int MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE = MainNetParams.MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE; + + public LitecoinMainNetParams() { + super(); + id = "org.bitcoinj.litecoin_mainnet"; + // Genesis hash is 12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2 + packetMagic = 0xfbc0b6db; + + maxTarget = Utils.decodeCompactBits(0x1e0fffffL); + port = 9333; + addressHeader = 48; + p2shHeader = 50; + segwitAddressHrp = "ltc"; + dumpedPrivateKeyHeader = 176; + + spendableCoinbaseDepth = 100; + subsidyDecreaseBlockCount = 840000; + + genesisBlock.setTime(1317972665L); + genesisBlock.setDifficultyTarget(0x1e0ffff0L); + genesisBlock.setNonce(2084524493); + + String genesisHash = genesisBlock.getHashAsString(); + checkState(genesisHash.equals("5155a7ed2219a75c0735c58b5d459c6d07d97917570e27b9d1d4546fb8431381")); + alertSigningKey = Hex.decode("040184710fa689ad5023690c80f3a49c8f13f8d45b8c857fbcbc8bc4a8e4d3eb4b10f4d4604fa08dce601aaf0f470216fe1b51850b4acf21b179c45070ac7b03a9"); + + majorityEnforceBlockUpgrade = MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE; + majorityRejectBlockOutdated = MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED; + majorityWindow = MAINNET_MAJORITY_WINDOW; + + dnsSeeds = new String[]{ + "dnsseed.litecointools.com", + "dnsseed.litecoinpool.org", + "dnsseed.ltc.xurious.com", + "dnsseed.koin-project.com", + "dnsseed.weminemnc.com" + }; + bip32HeaderP2PKHpub = 0x0488B21E; + bip32HeaderP2PKHpriv = 0x0488ADE4; + } + + @Override + public String getPaymentProtocolId() { + return PAYMENT_PROTOCOL_ID_MAINNET; + } + + + private static LitecoinMainNetParams instance; + + public static synchronized LitecoinMainNetParams get() { + if (instance == null) { + instance = new LitecoinMainNetParams(); + } + return instance; + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/litecoin/LitecoinNetworkManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/litecoin/LitecoinNetworkManager.kt new file mode 100644 index 0000000000..95e5e592ce --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/litecoin/LitecoinNetworkManager.kt @@ -0,0 +1,85 @@ +package com.tangem.blockchain.blockchains.litecoin + +import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinAddressResponse +import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinFee +import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinProvider +import com.tangem.blockchain.network.blockcypher.BlockcypherProvider +import com.tangem.blockchain.network.blockcypher.BlockcypherApi +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.blockchain.network.API_BLOCKCHAIR +import com.tangem.blockchain.network.API_BLOCKCYPHER +import com.tangem.blockchain.network.blockchair.BlockchairApi +import com.tangem.blockchain.network.blockchair.BlockchairProvider +import com.tangem.blockchain.network.createRetrofitInstance +import retrofit2.HttpException +import java.io.IOException + + +class LitecoinNetworkManager : BitcoinProvider { + private val blockchain = Blockchain.Litecoin + + private val blockchairProvider by lazy { + val api = createRetrofitInstance(API_BLOCKCHAIR) + .create(BlockchairApi::class.java) + BlockchairProvider(api, blockchain) + } + + private val blockcypherProvider by lazy { + val api = createRetrofitInstance(API_BLOCKCYPHER) + .create(BlockcypherApi::class.java) + BlockcypherProvider(api, blockchain) + } + + private var provider: BitcoinProvider = blockchairProvider + + private fun changeProvider() { + provider = if (provider == blockchairProvider) blockcypherProvider else blockchairProvider + } + + override suspend fun getInfo(address: String): Result { + val result = provider.getInfo(address) + when (result) { + is Result.Success -> return result + is Result.Failure -> { + if (result.error is IOException || result.error is HttpException) { + changeProvider() + return provider.getInfo(address) + } else { + return result + } + } + } + } + + override suspend fun getFee(): Result { + val result = provider.getFee() + when (result) { + is Result.Success -> return result + is Result.Failure -> { + if (result.error is IOException || result.error is HttpException) { + changeProvider() + return provider.getFee() + } else { + return result + } + } + } + } + + override suspend fun sendTransaction(transaction: String): SimpleResult { + val result = provider.sendTransaction(transaction) + when (result) { + is SimpleResult.Success -> return result + is SimpleResult.Failure -> { + if (result.error is IOException || result.error is HttpException) { + changeProvider() + return provider.sendTransaction(transaction) + } else { + return result + } + } + } + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/litecoin/LitecoinWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/litecoin/LitecoinWalletManager.kt new file mode 100644 index 0000000000..8c12297f26 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/litecoin/LitecoinWalletManager.kt @@ -0,0 +1,29 @@ +package com.tangem.blockchain.blockchains.litecoin + +import com.tangem.blockchain.blockchains.bitcoin.BitcoinTransactionBuilder +import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.TransactionSender +import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.extensions.Result +import java.math.BigDecimal + +class LitecoinWalletManager( + cardId: String, + wallet: Wallet, + transactionBuilder: BitcoinTransactionBuilder, + networkManager: LitecoinNetworkManager +) : BitcoinWalletManager(cardId, wallet, transactionBuilder, networkManager), TransactionSender { + override suspend fun getFee(amount: Amount, destination: String): Result> { + val minimalFee = BigDecimal("0.00001") + when (val result = super.getFee(amount, destination)) { + is Result.Success -> { + for (fee in result.data) { + if (fee.value!! < minimalFee) fee.value = minimalFee + } + return result + } + is Result.Failure -> return result + } + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/TezosAddressService.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/TezosAddressService.kt new file mode 100644 index 0000000000..813d80ce33 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/TezosAddressService.kt @@ -0,0 +1,37 @@ +package com.tangem.blockchain.blockchains.tezos + +import com.tangem.blockchain.common.AddressService +import com.tangem.common.extensions.calculateSha256 +import com.tangem.common.extensions.hexToBytes +import org.bitcoinj.core.Base58 +import org.spongycastle.jcajce.provider.digest.Blake2b + +class TezosAddressService : AddressService { + override fun makeAddress(walletPublicKey: ByteArray): String { + val publicKeyHash = Blake2b.Blake2b160().digest(walletPublicKey) + + val tz1Prefix = "06A19F".hexToBytes() + val prefixedHash = tz1Prefix + publicKeyHash + + val checksum = prefixedHash.calculateTezosChecksum() + val prefixedHashWithChecksum = prefixedHash + checksum + + return Base58.encode(prefixedHashWithChecksum) + } + + override fun validate(address: String): Boolean { + val prefixedHashWithChecksum = Base58.decode(address) + if (prefixedHashWithChecksum == null || prefixedHashWithChecksum.size != 27) return false + + val prefixedHash = prefixedHashWithChecksum.copyOf(23) + val checksum = prefixedHashWithChecksum.copyOfRange(23, 27) + + val calculatedChecksum = prefixedHash.calculateTezosChecksum() + + return calculatedChecksum.contentEquals(checksum) + } + + companion object { + fun ByteArray.calculateTezosChecksum() = this.calculateSha256().calculateSha256().copyOfRange(0, 4) + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/TezosTransactionBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/TezosTransactionBuilder.kt new file mode 100644 index 0000000000..8b4503b9ac --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/TezosTransactionBuilder.kt @@ -0,0 +1,70 @@ +package com.tangem.blockchain.blockchains.tezos + +import com.tangem.blockchain.blockchains.tezos.TezosAddressService.Companion.calculateTezosChecksum +import com.tangem.blockchain.blockchains.tezos.network.TezosOperationContent +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.bigIntegerValue +import com.tangem.common.extensions.hexToBytes +import com.tangem.common.extensions.toHexString +import org.bitcoinj.core.Base58 +import org.spongycastle.jcajce.provider.digest.Blake2b + +class TezosTransactionBuilder(private val walletPublicKey: ByteArray) { + var counter: Long? = null + + fun buildContents(transactionData: TransactionData, + publicKeyRevealed: Boolean + ): Result> { + if (counter == null) return Result.Failure(Exception("counter is null")) + var counter = counter!! + + val contents = arrayListOf() + + if (!publicKeyRevealed) { + counter++ + val revealOp = TezosOperationContent( + kind = "reveal", + source = transactionData.sourceAddress, + fee = "1300", + counter = counter.toString(), + gas_limit = "10000", + storage_limit = "0", + public_key = encodePublicKey(walletPublicKey) + ) + contents.add(revealOp) + } + + counter++ + val transactionOp = TezosOperationContent( + kind = "transaction", + source = transactionData.sourceAddress, + fee = "1350", + counter = counter.toString(), + gas_limit = "10600", + storage_limit = "277", + destination = transactionData.destinationAddress, + amount = transactionData.amount.bigIntegerValue().toString() + ) + contents.add(transactionOp) + + return Result.Success(contents) + } + + fun buildToSign(forgedContents: String): ByteArray { + val genericOperationWatermark = "03" + return Blake2b.Blake2b256().digest((genericOperationWatermark + forgedContents).hexToBytes()) + } + + fun buildToSend(signature: ByteArray, forgedContents: String) = forgedContents + signature.toHexString() + + private fun encodePublicKey(pkUncompressed: ByteArray): String { + val edpkPrefix = "0D0F25D9".hexToBytes() + val prefixedPubKey = edpkPrefix + pkUncompressed + + val checksum = prefixedPubKey.calculateTezosChecksum() + val prefixedHashWithChecksum = prefixedPubKey + checksum + + return Base58.encode(prefixedHashWithChecksum) + } +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/TezosWalletManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/TezosWalletManager.kt new file mode 100644 index 0000000000..e1867942e7 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/TezosWalletManager.kt @@ -0,0 +1,109 @@ +package com.tangem.blockchain.blockchains.tezos + +import android.util.Log +import com.tangem.blockchain.blockchains.tezos.network.TezosInfoResponse +import com.tangem.blockchain.blockchains.tezos.network.TezosNetworkManager +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.common.extensions.isZero +import com.tangem.common.extensions.toHexString +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import java.math.BigDecimal + +class TezosWalletManager( + cardId: String, + wallet: Wallet, + private val transactionBuilder: TezosTransactionBuilder, + private val networkManager: TezosNetworkManager +) : WalletManager(cardId, wallet), TransactionSender { + + private val blockchain = wallet.blockchain + private var publicKeyRevealed: Boolean? = null + + override suspend fun update() { + val response = networkManager.getInfo(wallet.address) + when (response) { + is Result.Success -> updateWallet(response.data) + is Result.Failure -> updateError(response.error) + } + } + + private fun updateWallet(response: TezosInfoResponse) { + Log.d(this::class.java.simpleName, "Balance is ${response.balance}") + wallet.amounts[AmountType.Coin]?.value = response.balance + transactionBuilder.counter = response.counter + } + + private fun updateError(error: Throwable?) { + Log.e(this::class.java.simpleName, error?.message ?: "") + if (error != null) throw error + } + + override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult { + if (publicKeyRevealed == null) return SimpleResult.Failure(Exception("publicKeyRevealed is null")) + + val contents = + when (val response = transactionBuilder.buildContents(transactionData, publicKeyRevealed!!)) { + is Result.Failure -> return SimpleResult.Failure(response.error) + is Result.Success -> response.data + } + val header = + when (val response = networkManager.getHeader()) { + is Result.Failure -> return SimpleResult.Failure(response.error) + is Result.Success -> response.data + } + val forgedContents = //TODO: CHANGE FOR PRODUCTION, this is potential security vulnerability, transaction should be forged locally + when (val response = networkManager.forgeContents(header.hash, contents)) { + is Result.Failure -> return SimpleResult.Failure(response.error) + is Result.Success -> response.data + } + val dataToSign = transactionBuilder.buildToSign(forgedContents) + + val signature = when (val signerResponse = signer.sign(arrayOf(dataToSign), cardId)) { + is CompletionResult.Failure -> return SimpleResult.Failure(signerResponse.error) + is CompletionResult.Success -> signerResponse.data.signature + } + + when (val response = networkManager.checkTransaction(header, contents, signature)) { + is SimpleResult.Failure -> return response + is SimpleResult.Success -> { + val transactionToSend = transactionBuilder.buildToSend(signature,forgedContents) + return networkManager.sendTransaction(transactionToSend) + } + } + } + + override suspend fun getFee(amount: Amount, destination: String): Result> { + var fee: BigDecimal = BigDecimal.valueOf(0.00135) + var error: Result.Failure? = null + + coroutineScope { + val publicKeyRevealedDeferred = async { networkManager.isPublicKeyRevealed(wallet.address) } + val destinationInfoDeferred = async { networkManager.getInfo(destination) } + + when (val result = publicKeyRevealedDeferred.await()) { + is Result.Failure -> error = result + is Result.Success -> { + publicKeyRevealed = result.data + if (!publicKeyRevealed!!) { + fee += BigDecimal.valueOf(0.0013) + } + } + } + + when (val result = destinationInfoDeferred.await()) { + is Result.Failure -> error = result + is Result.Success -> { + if (result.data.balance.isZero()) { + fee += BigDecimal.valueOf(0.257) + } + } + } + } + return if (error == null) Result.Success(listOf(Amount(fee, blockchain))) else error!! + } + +} \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosApi.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosApi.kt new file mode 100644 index 0000000000..27b37b48d0 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosApi.kt @@ -0,0 +1,50 @@ +package com.tangem.blockchain.blockchains.tezos.network + +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path + +interface TezosApi { + @GET("chains/main/blocks/head/context/contracts/{address}") + suspend fun getAddressData(@Path("address") address: String): TezosAddressResponse + + @GET("chains/main/blocks/head/header") + suspend fun getHeader(): TezosHeaderResponse + + @GET("chains/main/blocks/head/context/contracts/{address}/manager_key") + suspend fun getManagerKey(@Path("address") address: String): String + + @POST("chains/main/blocks/head/helpers/forge/operations") + suspend fun forgeOperations(@Body tezosForgeBody: TezosForgeBody): String + + @POST("chains/main/blocks/head/helpers/preapply/operations") + suspend fun preapplyOperations(@Body tezosPreapplyBodyList: List) + + @POST("injection/operation") + suspend fun sendTransaction(@Body transaction: String) +} + +data class TezosForgeBody( + val branch: String, + val contents: List +) + +data class TezosOperationContent( + val kind: String, + val source: String, + val fee: String, + val counter: String, + val gas_limit: String, + val storage_limit: String, + val public_key: String? = null, + val destination: String? = null, + val amount: String? = null +) + +data class TezosPreapplyBody( + val protocol: String, + val branch: String, + val contents: List, + val signature: String +) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosNetworkManager.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosNetworkManager.kt new file mode 100644 index 0000000000..3bca7e5e9d --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosNetworkManager.kt @@ -0,0 +1,134 @@ +package com.tangem.blockchain.blockchains.tezos.network + +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.blockchain.network.API_TEZOS +import com.tangem.blockchain.network.API_TEZOS_RESERVE +import com.tangem.blockchain.network.createRetrofitInstance +import retrofit2.HttpException +import java.io.IOException +import java.math.BigDecimal + +class TezosNetworkManager { + private val tezosProvider by lazy { + val api = createRetrofitInstance(API_TEZOS) + .create(TezosApi::class.java) + TezosProvider(api) + } + + private val tezosReserveProvider by lazy { + val api = createRetrofitInstance(API_TEZOS_RESERVE) + .create(TezosApi::class.java) + TezosProvider(api) + } + + var provider = tezosProvider + + private fun changeProvider() { + provider = if (provider == tezosProvider) tezosReserveProvider else tezosProvider + } + + suspend fun getInfo(address: String): Result { + val result = provider.getInfo(address) + when (result) { + is Result.Success -> return result + is Result.Failure -> { + if (result.error is IOException || result.error is HttpException) { + changeProvider() + return provider.getInfo(address) + } else { + return result + } + } + } + } + + suspend fun isPublicKeyRevealed(address: String): Result { + val result = provider.isPublicKeyRevealed(address) + when (result) { + is Result.Success -> return result + is Result.Failure -> { + if (result.error is IOException || result.error is HttpException) { + changeProvider() + return provider.isPublicKeyRevealed(address) + } else { + return result + } + } + } + } + + suspend fun getHeader(): Result { + val result = provider.getHeader() + when (result) { + is Result.Success -> return result + is Result.Failure -> { + if (result.error is IOException || result.error is HttpException) { + changeProvider() + return provider.getHeader() + } else { + return result + } + } + } + } + + suspend fun forgeContents(headerHash: String, contents: List): Result { + val result = provider.forgeContents(headerHash, contents) + when (result) { + is Result.Success -> return result + is Result.Failure -> { + if (result.error is IOException || result.error is HttpException) { + changeProvider() + return provider.forgeContents(headerHash, contents) + } else { + return result + } + } + } + } + + suspend fun checkTransaction( + header: TezosHeader, + contents: List, + signature: ByteArray + ): SimpleResult { + val result = provider.checkTransaction(header, contents, signature) + when (result) { + is SimpleResult.Success -> return result + is SimpleResult.Failure -> { + if (result.error is IOException || result.error is HttpException) { + changeProvider() + return provider.checkTransaction(header, contents, signature) + } else { + return result + } + } + } + } + + suspend fun sendTransaction(transaction: String): SimpleResult { + val result = provider.sendTransaction(transaction) + when (result) { + is SimpleResult.Success -> return result + is SimpleResult.Failure -> { + if (result.error is IOException || result.error is HttpException) { + changeProvider() + return provider.sendTransaction(transaction) + } else { + return result + } + } + } + } +} + +data class TezosInfoResponse( + val balance: BigDecimal, + val counter: Long +) + +data class TezosHeader( + val hash: String, + val protocol: String +) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosProvider.kt new file mode 100644 index 0000000000..4b264d2857 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosProvider.kt @@ -0,0 +1,92 @@ +package com.tangem.blockchain.blockchains.tezos.network + +import com.tangem.blockchain.blockchains.tezos.TezosAddressService.Companion.calculateTezosChecksum +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.blockchain.extensions.retryIO +import com.tangem.common.extensions.hexToBytes +import org.bitcoinj.core.Base58 + +class TezosProvider(private val api: TezosApi) { + private val decimals = Blockchain.Tezos.decimals() + + suspend fun getInfo(address: String): Result { + return try { + val addressData = retryIO { api.getAddressData(address) } + Result.Success(TezosInfoResponse( + balance = addressData.balance!!.toBigDecimal().movePointLeft(decimals), + counter = addressData.counter!! + )) + } catch (exception: Exception) { + Result.Failure(exception) + } + } + + suspend fun isPublicKeyRevealed(address: String): Result { + return try { + retryIO { api.getManagerKey(address) } + Result.Success(true) + } catch (exception: Exception) { //TODO: check exception + Result.Success(false) + } + } + + suspend fun getHeader(): Result { + return try { + val headerResponse = retryIO { api.getHeader() } + Result.Success(TezosHeader( + hash = headerResponse.hash!!, + protocol = headerResponse.protocol!! + )) + } catch (exception: Exception) { + Result.Failure(exception) + } + } + + suspend fun forgeContents(headerHash: String, contents: List): Result { + return try { + val forgedContents = retryIO { api.forgeOperations(TezosForgeBody(headerHash, contents)) } + Result.Success(forgedContents) + } catch (exception: Exception) { + Result.Failure(exception) + } + } + + suspend fun checkTransaction( + header: TezosHeader, + contents: List, + signature: ByteArray + ): SimpleResult { + return try { + val tezosPreapplyBody = TezosPreapplyBody( + protocol = header.protocol, + branch = header.hash, + contents = contents, + signature = encodeSignature(signature) + ) + retryIO { api.preapplyOperations(listOf(tezosPreapplyBody)) } + SimpleResult.Success + } catch (exception: Exception) { + SimpleResult.Failure(exception) + } + } + + suspend fun sendTransaction(transaction: String): SimpleResult { + return try { + retryIO { api.sendTransaction(transaction) } + SimpleResult.Success + } catch (exception: Exception) { + SimpleResult.Failure(exception) + } + } + + private fun encodeSignature(signature: ByteArray): String { + val edsigPrefix = "09F5CD8612".hexToBytes() + val prefixedSignature = edsigPrefix + signature + val checksum = prefixedSignature.calculateTezosChecksum() + val prefixedSignatureWithChecksum = prefixedSignature + checksum + + return Base58.encode(prefixedSignatureWithChecksum) + } +} diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosResponse.kt new file mode 100644 index 0000000000..f0ac10e947 --- /dev/null +++ b/blockchain/src/main/java/com/tangem/blockchain/blockchains/tezos/network/TezosResponse.kt @@ -0,0 +1,22 @@ +package com.tangem.blockchain.blockchains.tezos.network + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TezosAddressResponse( + @Json(name = "balance") + var balance: Long? = null, + + @Json(name = "counter") + var counter: Long? = null +) + +@JsonClass(generateAdapter = true) +data class TezosHeaderResponse( + @Json(name = "protocol") + var protocol: String? = null, + + @Json(name = "hash") + var hash: String? = null +) \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt index c579c2d3db..d7ba034407 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/Blockchain.kt @@ -6,6 +6,7 @@ import com.tangem.blockchain.blockchains.bitcoincash.BitcoinCashAddressService import com.tangem.blockchain.blockchains.cardano.CardanoAddressService import com.tangem.blockchain.blockchains.ethereum.EthereumAddressService import com.tangem.blockchain.blockchains.stellar.StellarAddressService +import com.tangem.blockchain.blockchains.tezos.TezosAddressService import com.tangem.blockchain.blockchains.xrp.XrpAddressService enum class Blockchain( @@ -17,17 +18,19 @@ enum class Blockchain( Bitcoin("BTC", "BTC", "Bitcoin"), BitcoinTestnet("BTC/test", "BTCt", "Bitcoin Testnet"), BitcoinCash("BCH", "BCH", "Bitcoin Cash"), + Litecoin("LTC", "LTC", "Litecoin"), Ethereum("ETH", "ETH", "Ethereum"), RSK("RSK", "RBTC", "RSK"), Cardano("CARDANO", "ADA", "Cardano"), XRP("XRP", "XRP", "XRP Ledger"), Binance("BINANCE", "BNB", "Binance"), BinanceTestnet("BINANCE/test", "BNBt", "Binance"), - Stellar("XLM", "XLM", "Stellar"); + Stellar("XLM", "XLM", "Stellar"), + Tezos("TEZOS", "XTZ", "Tezos"); fun decimals(): Int = when (this) { - Bitcoin, BitcoinTestnet, BitcoinCash, Binance, BinanceTestnet -> 8 - Cardano, XRP -> 6 + Bitcoin, BitcoinTestnet, BitcoinCash, Binance, BinanceTestnet, Litecoin -> 8 + Cardano, XRP, Tezos -> 6 Ethereum, RSK -> 18 Stellar -> 7 Unknown -> 0 @@ -43,8 +46,7 @@ enum class Blockchain( private fun getAddressService(): AddressService = when (this) { Unknown -> throw Exception("unsupported blockchain") - Bitcoin -> BitcoinAddressService() - BitcoinTestnet -> BitcoinAddressService(true) + Bitcoin, BitcoinTestnet, Litecoin -> BitcoinAddressService(this) BitcoinCash -> BitcoinCashAddressService() Ethereum, RSK -> EthereumAddressService() Cardano -> CardanoAddressService() @@ -52,6 +54,7 @@ enum class Blockchain( Binance -> BinanceAddressService() BinanceTestnet -> BinanceAddressService(true) Stellar -> StellarAddressService() + Tezos -> TezosAddressService() } fun getShareUri(address: String): String = when (this) { @@ -63,9 +66,11 @@ enum class Blockchain( fun getExploreUrl(address: String, token: Token? = null): String = when (this) { Binance -> "https://explorer.binance.org/address/$address" + BinanceTestnet -> "https://testnet-explorer.binance.org/address/$address" Bitcoin -> "https://blockchain.info/address/$address" BitcoinTestnet -> "https://live.blockcypher.com/btc-testnet/address/$address" BitcoinCash -> "https://blockchair.com/bitcoin-cash/address/$address" + Litecoin -> "https://live.blockcypher.com/ltc/address/$address" Cardano -> "https://cardanoexplorer.com/address/$address" Ethereum -> if (token == null) { "https://etherscan.io/address/" @@ -81,7 +86,8 @@ enum class Blockchain( } Stellar -> "https://stellar.expert/explorer/public/account/$address" XRP -> "https://xrpscan.com/account/$address" - else -> throw Exception("Explore URL not defined!") + Tezos -> "https://tezblock.io/account/$address" + Unknown -> throw Exception("unsupported blockchain") } companion object { diff --git a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt index 621ed2f129..1c7af4e354 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/common/WalletManagerFactory.kt @@ -15,10 +15,15 @@ import com.tangem.blockchain.blockchains.cardano.network.CardanoNetworkManager import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionBuilder import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.blockchains.ethereum.network.EthereumNetworkManager +import com.tangem.blockchain.blockchains.litecoin.LitecoinNetworkManager +import com.tangem.blockchain.blockchains.litecoin.LitecoinWalletManager import com.tangem.blockchain.blockchains.stellar.StellarNetworkManager import com.tangem.blockchain.blockchains.stellar.StellarTransactionBuilder import com.tangem.blockchain.blockchains.stellar.StellarWalletManager +import com.tangem.blockchain.blockchains.tezos.TezosTransactionBuilder +import com.tangem.blockchain.blockchains.tezos.TezosWalletManager +import com.tangem.blockchain.blockchains.tezos.network.TezosNetworkManager import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder import com.tangem.blockchain.blockchains.xrp.XrpWalletManager import com.tangem.blockchain.blockchains.xrp.network.XrpNetworkManager @@ -42,24 +47,31 @@ object WalletManagerFactory { Blockchain.Bitcoin -> { return BitcoinWalletManager( cardId, wallet, - BitcoinTransactionBuilder(walletPublicKey), - BitcoinNetworkManager() + BitcoinTransactionBuilder(walletPublicKey, blockchain), + BitcoinNetworkManager(blockchain) ) } Blockchain.BitcoinTestnet -> { return BitcoinWalletManager( cardId, wallet, - BitcoinTransactionBuilder(walletPublicKey, true), - BitcoinNetworkManager(true) + BitcoinTransactionBuilder(walletPublicKey, blockchain), + BitcoinNetworkManager(blockchain) ) } Blockchain.BitcoinCash -> { return BitcoinCashWalletManager( cardId, wallet, - BitcoinCashTransactionBuilder(walletPublicKey.toCompressedPublicKey()), + BitcoinCashTransactionBuilder(walletPublicKey.toCompressedPublicKey(), blockchain), BitcoinCashNetworkManager() ) } + Blockchain.Litecoin -> { + return LitecoinWalletManager( + cardId, wallet, + BitcoinTransactionBuilder(walletPublicKey, blockchain), + LitecoinNetworkManager() + ) + } Blockchain.Ethereum, Blockchain.RSK -> { return EthereumWalletManager( cardId, wallet, @@ -104,7 +116,14 @@ object WalletManagerFactory { BinanceNetworkManager(true) ) } - else -> return null + Blockchain.Tezos -> { + return TezosWalletManager( + cardId, wallet, + TezosTransactionBuilder(walletPublicKey), + TezosNetworkManager() + ) + } + Blockchain.Unknown -> throw Exception("unsupported blockchain") } } diff --git a/blockchain/src/main/java/com/tangem/blockchain/extensions/Amount.kt b/blockchain/src/main/java/com/tangem/blockchain/extensions/Amount.kt index 9c76097bc5..659630c79c 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/extensions/Amount.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/extensions/Amount.kt @@ -4,7 +4,7 @@ import com.tangem.blockchain.common.Amount import java.math.BigInteger fun Amount.bigIntegerValue(): BigInteger? { - return this.value?.movePointRight(this.decimals.toInt())?.toBigInteger() + return this.value?.movePointRight(this.decimals)?.toBigInteger() } fun Amount.isAboveZero(): Boolean { diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/RetrofitBuilder.kt b/blockchain/src/main/java/com/tangem/blockchain/network/RetrofitBuilder.kt index 94d89c0531..8573dff8a7 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/RetrofitBuilder.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/RetrofitBuilder.kt @@ -53,4 +53,6 @@ const val API_ADALITE = "https://explorer3.adalite.io/" const val API_ADALITE_RESERVE = "https://nodes.southeastasia.cloudapp.azure.com/" const val API_RIPPLED = "https://s1.ripple.com:51234/" const val API_RIPPLED_RESERVE = "https://s2.ripple.com:51234/" -const val API_BLOCKCHAIR = "https://api.blockchair.com/" \ No newline at end of file +const val API_BLOCKCHAIR = "https://api.blockchair.com/" +const val API_TEZOS = "https://teznode.letzbake.com" +const val API_TEZOS_RESERVE = "https://mainnet.tezrpc.me" \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairProvider.kt index a14c6b8561..69e8111e66 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockchair/BlockchairProvider.kt @@ -15,7 +15,8 @@ import java.math.RoundingMode class BlockchairProvider(private val api: BlockchairApi, blockchain: Blockchain) : BitcoinProvider { private val blockchainPath = when (blockchain) { Blockchain.BitcoinCash -> "bitcoin-cash" - else -> throw Exception("${blockchain.fullName} blockchain is not supported by BlockchairProvider") + Blockchain.Litecoin -> "litecoin" + else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}") } private val decimals = blockchain.decimals() diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/api/BlockcypherApi.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockcypher/BlockcypherApi.kt similarity index 80% rename from blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/api/BlockcypherApi.kt rename to blockchain/src/main/java/com/tangem/blockchain/network/blockcypher/BlockcypherApi.kt index 14b78b0e53..77ea5eaf45 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/api/BlockcypherApi.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockcypher/BlockcypherApi.kt @@ -1,9 +1,6 @@ -package com.tangem.blockchain.blockchains.bitcoin.network.api +package com.tangem.blockchain.network.blockcypher import com.squareup.moshi.JsonClass -import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherFee -import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherResponse -import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherTx import retrofit2.http.* interface BlockcypherApi { diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BlockcypherProvider.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockcypher/BlockcypherProvider.kt similarity index 66% rename from blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BlockcypherProvider.kt rename to blockchain/src/main/java/com/tangem/blockchain/network/blockcypher/BlockcypherProvider.kt index 58b6bec6a4..857e06f449 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/BlockcypherProvider.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockcypher/BlockcypherProvider.kt @@ -1,30 +1,35 @@ -package com.tangem.blockchain.blockchains.bitcoin.network +package com.tangem.blockchain.network.blockcypher import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput -import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockcypherApi -import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockcypherBody -import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherFee -import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherResponse +import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinAddressResponse +import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinFee +import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinProvider +import com.tangem.blockchain.network.blockcypher.BlockcypherApi +import com.tangem.blockchain.network.blockcypher.BlockcypherBody import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.extensions.retryIO import com.tangem.common.extensions.hexToBytes -class BlockcypherProvider(private val api: BlockcypherApi, isTestNet: Boolean) : BitcoinProvider { +class BlockcypherProvider(private val api: BlockcypherApi, blockchain: Blockchain) : BitcoinProvider { - private val blockchain = "btc" - private val decimals = Blockchain.Bitcoin.decimals() - - private val network = if (isTestNet) { - BlockcypherNetwork.Test.network - } else { - BlockcypherNetwork.Main.network + private val blockchainPath = when (blockchain) { + Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> "btc" + Blockchain.Litecoin -> "ltc" + else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}") } + private val network = when (blockchain) { + Blockchain.BitcoinTestnet -> "test3" + else -> "main" + } + + private val decimals = blockchain.decimals() + override suspend fun getInfo(address: String): Result { try { - val addressData: BlockcypherResponse = retryIO { api.getAddressData(blockchain, network, address) } + val addressData: BlockcypherResponse = retryIO { api.getAddressData(blockchainPath, network, address) } val unspents = addressData.txrefs?.map { BitcoinUnspentOutput( it.amount!!.toBigDecimal().movePointLeft(decimals), @@ -45,7 +50,7 @@ class BlockcypherProvider(private val api: BlockcypherApi, isTestNet: Boolean) : override suspend fun getFee(): Result { return try { - val receivedFee: BlockcypherFee = retryIO { api.getFee(blockchain, network) } + val receivedFee: BlockcypherFee = retryIO { api.getFee(blockchainPath, network) } Result.Success( BitcoinFee(receivedFee.minFeePerKb!!.toBigDecimal().movePointLeft(decimals), receivedFee.normalFeePerKb!!.toBigDecimal().movePointLeft(decimals), @@ -60,7 +65,7 @@ class BlockcypherProvider(private val api: BlockcypherApi, isTestNet: Boolean) : return try { retryIO { api.sendTransaction( - blockchain, network, BlockcypherBody(transaction), BlockcypherToken.getToken()) + blockchainPath, network, BlockcypherBody(transaction), BlockcypherToken.getToken()) } SimpleResult.Success } catch (error: Exception) { @@ -76,9 +81,4 @@ private object BlockcypherToken { "66a8a37c5e9d4d2c9bb191acfe7f93aa") fun getToken(): String = tokens.random() -} - -private enum class BlockcypherNetwork(val network: String) { - Main("main"), - Test("test3") } \ No newline at end of file diff --git a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/response/BlockcypherResponse.kt b/blockchain/src/main/java/com/tangem/blockchain/network/blockcypher/BlockcypherResponse.kt similarity index 95% rename from blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/response/BlockcypherResponse.kt rename to blockchain/src/main/java/com/tangem/blockchain/network/blockcypher/BlockcypherResponse.kt index a0e79c45b2..71dec770fe 100644 --- a/blockchain/src/main/java/com/tangem/blockchain/blockchains/bitcoin/network/response/BlockcypherResponse.kt +++ b/blockchain/src/main/java/com/tangem/blockchain/network/blockcypher/BlockcypherResponse.kt @@ -1,4 +1,4 @@ -package com.tangem.blockchain.blockchains.bitcoin.network.response +package com.tangem.blockchain.network.blockcypher import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/blockchain/src/test/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressTest.kt b/blockchain/src/test/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressTest.kt index c8b7cca7ef..9fabb12561 100644 --- a/blockchain/src/test/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressTest.kt +++ b/blockchain/src/test/java/com/tangem/blockchain/blockchains/bitcoin/BitcoinAddressTest.kt @@ -1,12 +1,13 @@ package com.tangem.blockchain.blockchains.bitcoin import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.hexToBytes import org.junit.Test class BitcoinAddressTest { - private val addressService = BitcoinAddressService() + private val addressService = BitcoinAddressService(Blockchain.Bitcoin) @Test fun makeAddressFromCorrectPublicKey() { diff --git a/blockchain/src/test/java/com/tangem/blockchain/blockchains/litecoin/LitecoinAddressTest.kt b/blockchain/src/test/java/com/tangem/blockchain/blockchains/litecoin/LitecoinAddressTest.kt new file mode 100644 index 0000000000..a8753d94a4 --- /dev/null +++ b/blockchain/src/test/java/com/tangem/blockchain/blockchains/litecoin/LitecoinAddressTest.kt @@ -0,0 +1,28 @@ +package com.tangem.blockchain.blockchains.litecoin + +import com.google.common.truth.Truth +import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressService +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.hexToBytes +import org.junit.Test + +class LitecoinAddressTest { + + private val addressService = BitcoinAddressService(Blockchain.Litecoin) + + @Test + fun makeAddressFromCorrectPublicKey() { + val walletPublicKey = "044A76C9A70422160F515F956D0F50C71BBBA4F9862A22913817D63F0B1EF7C2FAF512E1C91B1BE827560EFE24FB1652B47337E296C778DFB1014D080CDD35EF65".hexToBytes() + val expected = "LeweDi2SMmishGyCqQN2972qNByaSRdcfT" + + Truth.assertThat(addressService.makeAddress(walletPublicKey)) + .isEqualTo(expected) + } + + @Test + fun validateCorrectAddress() { + val address = "LeweDi2SMmishGyCqQN2972qNByaSRdcfT" + Truth.assertThat(addressService.validate(address)) + .isTrue() + } +} \ No newline at end of file diff --git a/blockchain/src/test/java/com/tangem/blockchain/blockchains/tezos/TezosAddressTest.kt b/blockchain/src/test/java/com/tangem/blockchain/blockchains/tezos/TezosAddressTest.kt new file mode 100644 index 0000000000..3f9c6d5839 --- /dev/null +++ b/blockchain/src/test/java/com/tangem/blockchain/blockchains/tezos/TezosAddressTest.kt @@ -0,0 +1,26 @@ +package com.tangem.blockchain.blockchains.tezos + +import com.google.common.truth.Truth +import com.tangem.common.extensions.hexToBytes +import org.junit.Test + +class TezosAddressTest { + + private val addressService = TezosAddressService() + + @Test + fun makeAddressFromCorrectPublicKey() { + val walletPublicKey = "98E0E504F3A5FDE704400302ABB0A2EFB0DF0F95C166C91D7F207DEDCE10CBA3".hexToBytes() + val expected = "tz1hhRdWDAvGsgEioZ9GAp4bUVQkd9ng2MMR" + + Truth.assertThat(addressService.makeAddress(walletPublicKey)) + .isEqualTo(expected) + } + + @Test + fun validateCorrectAddress() { + val address = "tz1hhRdWDAvGsgEioZ9GAp4bUVQkd9ng2MMR" + Truth.assertThat(addressService.validate(address)) + .isTrue() + } +} \ No newline at end of file diff --git a/blockchain/src/test/java/com/tangem/blockchain/common/WalletManagerFactoryTest.kt b/blockchain/src/test/java/com/tangem/blockchain/common/WalletManagerFactoryTest.kt index ff9edbcdd1..33c6e97c89 100644 --- a/blockchain/src/test/java/com/tangem/blockchain/common/WalletManagerFactoryTest.kt +++ b/blockchain/src/test/java/com/tangem/blockchain/common/WalletManagerFactoryTest.kt @@ -7,7 +7,9 @@ import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager import com.tangem.blockchain.blockchains.bitcoincash.BitcoinCashWalletManager import com.tangem.blockchain.blockchains.cardano.CardanoWalletManager import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager +import com.tangem.blockchain.blockchains.litecoin.LitecoinWalletManager import com.tangem.blockchain.blockchains.stellar.StellarWalletManager +import com.tangem.blockchain.blockchains.tezos.TezosWalletManager import com.tangem.blockchain.blockchains.xrp.XrpWalletManager import com.tangem.commands.ReadCommand import com.tangem.common.apdu.ResponseApdu @@ -21,7 +23,7 @@ internal class WalletManagerFactoryTest { val data = "0108bb00000000000304200754414e47454d00020102800a322e3432642053444b000341040876bdec26b89bd2159a668b9af3d9fe86370f318717c92b8d6c1186fb3648c32a5f9321998cc2d042901c91d40601e79a641e1cbcebe7a2358be6054e1b6e5d0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034254438640e17ceec48c5be36240c98019f95ad8b6e56acfebe60d11979c6279f715d607d76a860a137da8d109e805753f3f56b0130709f4bbf4cb9974b4c57b8469bf4b873041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b310008040000006407010009020bb8604104752a727e14bba5bd73b6714d72500f61ffd11026ad1196d2e1c54577cbeeac3d11fc68a64700f8d533f4e311964ea8fb3aa26c588295f2133868d69c3e62869362040000005c6304000000090f01009000" val responseApdu = ResponseApdu(data.hexToBytes()) val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu) - val walletManager = WalletManagerFactory.makeWalletManager(card!!) + val walletManager = WalletManagerFactory.makeWalletManager(card) Truth.assertThat(walletManager) .isInstanceOf(BitcoinWalletManager::class.java) @@ -32,7 +34,7 @@ internal class WalletManagerFactoryTest { val data = "0108bb00000000000536200754414e47454d00020102800a322e3432642053444b000341046c8aea0d5a850b0a608acf9a0c453c39ea86131e88bfa78800de3cfb5bf1007aeaa7b9ffc184212255758605c2461be343c0a661d73cabafa4c9c175b3f0e59a0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034554488640431b6244acfeac479becdff201a7f720a7d70a97edc4e019fb678596baf52dfe9d0e8faf08ceb4443b82d4e66815541f2dc8ec6dd3ff83eb42f06e5eab07f25f3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b3100080400000064070100090205dc60410464dddc3f356744aaecfa07427f9eb996ff537d65f20fb5be3abccf0354352a6b5f8a1942e0f8ddeea3a170eda78d060be8162ad60e94e4e91fbbdf0a7054785562040000005b6304000000090f01009000" val responseApdu = ResponseApdu(data.hexToBytes()) val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu) - val walletManager = WalletManagerFactory.makeWalletManager(card!!) + val walletManager = WalletManagerFactory.makeWalletManager(card) Truth.assertThat(walletManager) .isInstanceOf(EthereumWalletManager::class.java) @@ -43,7 +45,7 @@ internal class WalletManagerFactoryTest { val data = "0108bb00000000000379200754414e47454d00020102800a322e3432642053444b0003410487d7bb51b189213e3cedc3fcfa3fc047b3b71b7805b5b215e14639b3a8ebb1952c9dd5ea4354441b6ada4e8b8327674bb102ddae69df55be69643a2c916edf650a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b008403584c4d86409a4bc2baf0e5836887da21167cf33458d5249d1a610bced0e31dc053f23729ed24d715912bf89e6804669430dfe396ed83274e0031f6803e2bdb8c041fa993413041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc6020e078212d58b2b9d0edc9c936830d10081cd38b90c31778c56dfb1171027e294e62040000003863040000002c0f01009000" val responseApdu = ResponseApdu(data.hexToBytes()) val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu) - val walletManager = WalletManagerFactory.makeWalletManager(card!!) + val walletManager = WalletManagerFactory.makeWalletManager(card) Truth.assertThat(walletManager) .isInstanceOf(StellarWalletManager::class.java) @@ -54,7 +56,7 @@ internal class WalletManagerFactoryTest { val data = "0108bb00000000000502200754414e47454d00020102800a322e3432642053444b0003410402c1e39257d60583489da2d67d35d1cc2a1c005cc05c1021f44838edcaf25d5615cad7c9d11c2e23f5efa93e50904d33c88808d0e169060508df840992e31f4d0a04041e76310c658102ffff8a0101820407e30b0d830b54414e47454d2053444b00840743415244414e4f8640f24ef5c8c6eba0ff97560d5b013edb4a452594270db9647bd0a3543df8104dec75731d4db3ebe0fc493f2afee00195e560b51e3c41189b7c61ba7895d6434b9d3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc60208a71161cfdf1e0a85d8e7ff372aa4a01136046292aceb5f9ad7ebdb98d3f60a86204000000646304000000000f01009000" val responseApdu = ResponseApdu(data.hexToBytes()) val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu) - val walletManager = WalletManagerFactory.makeWalletManager(card!!) + val walletManager = WalletManagerFactory.makeWalletManager(card) Truth.assertThat(walletManager) .isInstanceOf(CardanoWalletManager::class.java) @@ -65,7 +67,7 @@ internal class WalletManagerFactoryTest { val data = "0108cb21000000002154200b534d4152542043415348000201028006322e31317200034104bdad63848f97c535da53cf8fd300d24fa33f0516d194aa78ec164a06994d00204bae243a424e316c6ec845e02d9b15eafae8c19018a926b0b7435e6e941cdadb0a0400007e210c5a81020028820407e30502830754414e47454d00840358525086400ed8734b877869722c7d0b37ffb154b9fef21c54bf2c6496feb1fb5c1fc28a2ac28e201dde84f27495fa7f08b3ca2be2fb4954bf0fe78af027d6cdc16c3eee923041048196aa4b410ac44a3b9cce18e7be226aea070acc83a9cf67540fac49af25129f6a538a28ad6341358e3c4f9963064f7e365372a651d374e5c23cdd37fd099bf2050a736563703235366b31000804000f4240070100090205dc604104d2b9fb288540d54e5b32ecaf0381cd571f97f6f1ecd036b66bb11aa52ffe9981110d883080e2e255c6b1640586f7765e6faa325d1340f49b56b83d9de56bc7ed6204000f42406304000000000f01009000" val responseApdu = ResponseApdu(data.hexToBytes()) val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu) - val walletManager = WalletManagerFactory.makeWalletManager(card!!) + val walletManager = WalletManagerFactory.makeWalletManager(card) Truth.assertThat(walletManager) .isInstanceOf(XrpWalletManager::class.java) @@ -76,7 +78,7 @@ internal class WalletManagerFactoryTest { val data = "0108BB00000000000015200754414E47454D00020102800A322E3432642053444B0003410446D4155890B08BE217F0B1FA7DCCB16138C24B3E825A27315D5E4BBD6CAF76A28C7902007052BC1347355A78D54BD73216C9431D555CED827B54FD9255EB3A830A04041E76310C658102FFFF8A0101820407E40410830B54414E47454D2053444B00840742494E414E4345864029F115878EDC7B0CB2A6F4A4009447DCB43BBE922D7629AEBD0C9A910AD1E3BF15AE409C4F579700951ED2FE4D775171A86CFA8E50009A05938CE210D6D4A2583041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A0070100604104E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376F62040001869F6304000000010F01009000" val responseApdu = ResponseApdu(data.hexToBytes()) val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu) - val walletManager = WalletManagerFactory.makeWalletManager(card!!) + val walletManager = WalletManagerFactory.makeWalletManager(card) Truth.assertThat(walletManager) .isInstanceOf(BinanceWalletManager::class.java) @@ -87,9 +89,31 @@ internal class WalletManagerFactoryTest { val data = "0108BB00000000000049200754414E47454D00020102800A322E3432642053444B00034104766A1586D164B436E5D420AED01FDAB41B2AE7EDF0C865D7AF1DA995D70AB297E5B94B761CFBB405084C21BC97C02B4A1EA9ED4F515576EAB4D83AD3A0DFAA8A0A04041E76310C618102FFFF8A0101820407E4041B830B54414E47454D2053444B00840342434886408058F0F628C2466B09ECEB13F2A8EFDD4558F5D2DBDA9BD0628EE8C8CC99A778FF0F1AECD35704B9F3518486EA5C1D20F9DFCBAA66184F4CCCD9282E2632882C3041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A0070100604104BE37CD5251C8999EDBBFC759D800EB41E4DCB718289601EB15819404E1B2F2ED90FE50C2A481D06EC790D1EF6184974EB655ABAE4BE56A6D1C9E1A17B1EFDF0262040001869A6304000000060F01009000" val responseApdu = ResponseApdu(data.hexToBytes()) val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu) - val walletManager = WalletManagerFactory.makeWalletManager(card!!) + val walletManager = WalletManagerFactory.makeWalletManager(card) Truth.assertThat(walletManager) .isInstanceOf(BitcoinCashWalletManager::class.java) } + + @Test + fun createLitecoinWalletManager() { + val data = "0108BB00000000000023200754414E47454D00020102800A322E3432642053444B000341043539F86A40ADD04CE165764A761FD3E4D251028615D2A573B1C3AE652E60AFDBFAF02E3239E89EF2C43FA448A327557ADC5AF36376A0574570F6DBD20113514A0A04041E76310C618102FFFF8A0101820407E40414830B54414E47454D2053444B0084034C5443864004BDEAD0117544886346CB47F7CA84ABA8C34239502F23D28595A4B16CAD72F7DE506BA818B86A649C2BB945986D4574993B3B755B47CBEE31C4FB931F6748183041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A00701006041044A76C9A70422160F515F956D0F50C71BBBA4F9862A22913817D63F0B1EF7C2FAF512E1C91B1BE827560EFE24FB1652B47337E296C778DFB1014D080CDD35EF6562040001869D6304000000030F01009000" + val responseApdu = ResponseApdu(data.hexToBytes()) + val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu) + val walletManager = WalletManagerFactory.makeWalletManager(card) + + Truth.assertThat(walletManager) + .isInstanceOf(LitecoinWalletManager::class.java) + } + + @Test + fun createLTezosWalletManager() { + val data = "0108BB00000000000080200754414E47454D00020102800A322E3432642053444B0003410436CFC5D0A11353AE6AFEEDC84A2D02B2635C044DEEE47F99913072B8D166D14E557230AC5FB5272F1A0E523332CCE1A744B51DB53102FF7D3FDE023DC3477C460A04041E76310C638102FFFF8A0101820407E40514830B54414E47454D2053444B00840554455A4F538640C752685B29333CFB0DB0A7347579A0AE763F2B5C4BB09FD68E0B81A06CD01EC51347001732815A3ECFFCD78DDE4E53877581B9E4914B069570629D0C40A771B93041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050865643235353139000804000186A0070100602098E0E504F3A5FDE704400302ABB0A2EFB0DF0F95C166C91D7F207DEDCE10CBA362040001869F6304000000010F01009000" + val responseApdu = ResponseApdu(data.hexToBytes()) + val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu) + val walletManager = WalletManagerFactory.makeWalletManager(card) + + Truth.assertThat(walletManager) + .isInstanceOf(TezosWalletManager::class.java) + } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 511fa6ac99..40bc31028b 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { maven { url 'https://maven.fabric.io/public' } } dependencies { - classpath "com.android.tools.build:gradle:$versions.build_gradle" + classpath "com.android.tools.build:gradle:${versions.build_gradle}" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" classpath "com.github.dcendents:android-maven-gradle-plugin:2.1" classpath 'com.google.gms:google-services:4.3.3' diff --git a/dependencies.gradle b/dependencies.gradle index 3c7aaf12d8..f61f047e5a 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,4 +1,4 @@ ext.versions = [ kotlin : '1.3.72', - build_gradle: '3.6.3', + build_gradle: '4.0.0', ] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3cbbb9a496..01e71a44df 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Mon Mar 30 09:15:33 MSK 2020 +#Thu Jun 11 11:33:17 MSK 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/server-android/build.gradle b/server-android/build.gradle index a853878415..7c6568c89a 100644 --- a/server-android/build.gradle +++ b/server-android/build.gradle @@ -33,10 +33,13 @@ dependencies { // implementation 'com.github.TangemCash.card_android-common:card_android-android:0.1.0' implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'com.google.code.gson:gson:2.8.5' - implementation 'com.squareup.retrofit2:converter-gson:2.6.0' - implementation 'com.squareup.retrofit2:retrofit:2.6.0' - implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0' + implementation 'com.google.code.gson:gson:2.8.6' + implementation 'com.squareup.retrofit2:converter-gson:2.6.2' + implementation 'com.squareup.retrofit2:retrofit:2.8.1' + implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.7" + testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' diff --git a/server-android/src/main/java/com/tangem/server_android/NetworkUtils.kt b/server-android/src/main/java/com/tangem/server_android/NetworkUtils.kt new file mode 100644 index 0000000000..b631e7faa9 --- /dev/null +++ b/server-android/src/main/java/com/tangem/server_android/NetworkUtils.kt @@ -0,0 +1,40 @@ +package com.tangem.server_android + +import kotlinx.coroutines.delay +import java.io.IOException + + +suspend fun retryIO( + times: Int = 3, + initialDelay: Long = 100, + maxDelay: Long = 1000, + factor: Double = 2.0, + block: suspend () -> T): T +{ + var currentDelay = initialDelay + repeat(times - 1) { + try { + return block() + } catch (e: IOException) { + // you can log an error here and/or make a more finer-grained + // analysis of the cause to see if retry is needed + } + delay(currentDelay) + currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) + } + return block() +} + +sealed class Result { + data class Success(val data: T) : Result() + data class Failure(val error: Throwable?) : Result() +} + +suspend fun performRequest(block: suspend () -> T): Result { + return try { + val result = retryIO { block() } + Result.Success(result) + } catch (exception: Exception) { + Result.Failure(exception) + } +} \ No newline at end of file diff --git a/server-android/src/main/java/com/tangem/server_android/PayIdService.kt b/server-android/src/main/java/com/tangem/server_android/PayIdService.kt new file mode 100644 index 0000000000..804eb5fddb --- /dev/null +++ b/server-android/src/main/java/com/tangem/server_android/PayIdService.kt @@ -0,0 +1,49 @@ +package com.tangem.server_android + +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +class PayIdService { + + private val tangemApi: TangemApi by lazy { + provideRetrofit() + .create(TangemApi::class.java) + } + + suspend fun getPayId(cardId: String, publicKey: String): Result { + return performRequest { tangemApi.getPayId(cardId, publicKey) } + } + + suspend fun setPayId(cardId: String, publicKey: String, payId: String, address: String, network: String): Result { + return performRequest { tangemApi.setPayId(cardId, publicKey, payId, address, network) } + } +} + +data class PayIdResponse( + val payId: String +) + +data class SetPayIdResponse( + val success: Boolean +) + +fun provideRetrofit(): Retrofit { + val builder = Retrofit.Builder() + .baseUrl("https://tangem.com/") + .addConverterFactory(GsonConverterFactory.create()) + if (BuildConfig.DEBUG) + builder.client(createOkHttpClient()) + return builder.build() +} + +private fun createOkHttpClient(): OkHttpClient { + return OkHttpClient.Builder().addInterceptor(createHttpLoggingInterceptor()).build() +} + +private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor { + val logging = HttpLoggingInterceptor() + logging.level = HttpLoggingInterceptor.Level.BODY + return logging +} \ No newline at end of file diff --git a/server-android/src/main/java/com/tangem/server_android/Server.java b/server-android/src/main/java/com/tangem/server_android/Server.java index ba1f2f2ed9..92e024cd93 100644 --- a/server-android/src/main/java/com/tangem/server_android/Server.java +++ b/server-android/src/main/java/com/tangem/server_android/Server.java @@ -12,6 +12,7 @@ public class Server { static final String VERIFY = URL_TANGEM + "verify"; static final String VERIFY_AND_GET_INFO = URL_TANGEM + "card/verify-and-get-info"; static final String ARTWORK = URL_TANGEM + "card/artwork"; + static final String PAY_ID = ServerURL.API_PAY_ID_TANGEM; } } } \ No newline at end of file diff --git a/server-android/src/main/java/com/tangem/server_android/ServerURL.java b/server-android/src/main/java/com/tangem/server_android/ServerURL.java index 64fec024e6..ba1ec7cb3d 100644 --- a/server-android/src/main/java/com/tangem/server_android/ServerURL.java +++ b/server-android/src/main/java/com/tangem/server_android/ServerURL.java @@ -2,4 +2,5 @@ package com.tangem.server_android; class ServerURL { static final String API_TANGEM = "https://verify.tangem.com/"; + static final String API_PAY_ID_TANGEM = "https://payid.tangem.com/"; } \ No newline at end of file diff --git a/server-android/src/main/java/com/tangem/server_android/TangemApi.java b/server-android/src/main/java/com/tangem/server_android/TangemApi.java deleted file mode 100644 index 655975d1b2..0000000000 --- a/server-android/src/main/java/com/tangem/server_android/TangemApi.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.server_android; - -import com.tangem.server_android.model.CardVerifyAndGetInfo; - -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.http.Body; -import retrofit2.http.GET; -import retrofit2.http.Headers; -import retrofit2.http.POST; -import retrofit2.http.Query; - -public interface TangemApi { - @Headers("Content-Type: application/json") - @POST(Server.ApiTangem.Method.VERIFY_AND_GET_INFO) - Call getCardVerifyAndGetInfo(@Body CardVerifyAndGetInfo.Request requestBody); - - @GET(Server.ApiTangem.Method.ARTWORK) - Call getArtwork(@Query("artworkId") String artworkId, @Query("CID") String CID, @Query("publicKey") String publicKey); - -} \ No newline at end of file diff --git a/server-android/src/main/java/com/tangem/server_android/TangemApi.kt b/server-android/src/main/java/com/tangem/server_android/TangemApi.kt new file mode 100644 index 0000000000..e120ef75c1 --- /dev/null +++ b/server-android/src/main/java/com/tangem/server_android/TangemApi.kt @@ -0,0 +1,35 @@ +package com.tangem.server_android + +import com.tangem.server_android.model.CardVerifyAndGetInfo +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.http.* + +interface TangemApi { + @Headers("Content-Type: application/json") + @POST(Server.ApiTangem.Method.VERIFY_AND_GET_INFO) + fun getCardVerifyAndGetInfo(@Body requestBody: CardVerifyAndGetInfo.Request?): Call + + @GET(Server.ApiTangem.Method.ARTWORK) + fun getArtwork( + @Query("artworkId") artworkId: String?, + @Query("CID") CID: String?, + @Query("publicKey") publicKey: String? + ): Call + + @GET(Server.ApiTangem.Method.PAY_ID) + suspend fun getPayId( + @Query("cid") cardId: String, + @Query("key") publicKey: String + ): PayIdResponse + + @POST(Server.ApiTangem.Method.PAY_ID) + suspend fun setPayId( + @Query("cid") cardId: String, + @Query("key") publicKey: String, + @Query("payid") payId: String, + @Query("address") address: String, + @Query("network") network: String + ): SetPayIdResponse + +} \ No newline at end of file diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLV.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLV.java index 4e704fe1f0..69cf204c87 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLV.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLV.java @@ -284,7 +284,7 @@ public class TLV { return String.format("%s[%d]: %s (%s)", tag.name(), Value.length, Util.bytesToHex(Value), ProductMask.getDescription(iValue)); } catch (Exception e) { e.printStackTrace(); - return String.format("%s[%d]: %s (%s)", tag.name(), Value.length, Util.bytesToHex(Value)); + return String.format("%s[%d]: %s", tag.name(), Value.length, Util.bytesToHex(Value)); } } else { return String.format("%s[]: [[NULL]]", tag.name()); diff --git a/tangem-core/build.gradle b/tangem-core/build.gradle index fe6ed6638a..220c949038 100644 --- a/tangem-core/build.gradle +++ b/tangem-core/build.gradle @@ -8,11 +8,19 @@ group = "$jitpackSdk.group" version "$jitpackSdk.version" dependencies { + // kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin" + implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin" + + // crypto implementation "com.madgag.spongycastle:core:1.58.0.0" implementation "com.madgag.spongycastle:prov:1.58.0.0" implementation 'net.i2p.crypto:eddsa:0.3.0' - implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin" + + // misc + implementation 'com.google.code.gson:gson:2.8.6' + + // tests testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2' testImplementation "com.google.truth:truth:1.0" } diff --git a/tangem-core/src/main/java/com/tangem/CardSession.kt b/tangem-core/src/main/java/com/tangem/CardSession.kt index 6021923099..c7fa4559e7 100644 --- a/tangem-core/src/main/java/com/tangem/CardSession.kt +++ b/tangem-core/src/main/java/com/tangem/CardSession.kt @@ -19,6 +19,8 @@ import com.tangem.crypto.pbkdf2Hash */ interface CardSessionRunnable { + val performPreflightRead: Boolean + /** * The starting point for custom business logic. * Implement this interface and use [TangemSdk.startSessionWithRunnable] to run. @@ -55,6 +57,8 @@ class CardSession( */ private var isBusy = false + private var performPreflightRead = true + /** * This metod starts a card session, performs preflight [ReadCommand], * invokes [CardSessionRunnable.run] and closes the session. @@ -64,6 +68,8 @@ class CardSession( fun , R : CommandResponse> startWithRunnable( runnable: T, callback: (result: CompletionResult) -> Unit) { + performPreflightRead = runnable.performPreflightRead + start { session, error -> if (error != null) { callback(CompletionResult.Failure(error)) @@ -104,6 +110,11 @@ class CardSession( callback(this, error) } + if (!performPreflightRead) { + callback(this, null) + return + } + preflightRead() { result -> when (result) { is CompletionResult.Failure -> { @@ -174,7 +185,7 @@ class CardSession( * Stops the current session on error. * @param error An error that will be shown. */ - private fun stopWithError(error: Exception) { + private fun stopWithError(error: TangemSdkError) { if (!isBusy) return reader.closeSession() @@ -187,7 +198,7 @@ class CardSession( } if (error !is TangemSdkError.UserCancelled) { Log.e(tag, "Finishing with error: $errorMessage") - viewDelegate.onError(errorMessage) + viewDelegate.onError(error) } else { Log.i(tag, "User cancelled NFC session") } diff --git a/tangem-core/src/main/java/com/tangem/SessionViewDelegate.kt b/tangem-core/src/main/java/com/tangem/SessionViewDelegate.kt index c372b1730c..969b147b8f 100644 --- a/tangem-core/src/main/java/com/tangem/SessionViewDelegate.kt +++ b/tangem-core/src/main/java/com/tangem/SessionViewDelegate.kt @@ -40,7 +40,7 @@ interface SessionViewDelegate { /** * It is called when some error occur during NFC session. */ - fun onError(errorMessage: String) + fun onError(error: TangemSdkError) /** * It is called when a user is expected to enter pin code. diff --git a/tangem-core/src/main/java/com/tangem/TangemSdk.kt b/tangem-core/src/main/java/com/tangem/TangemSdk.kt index ce0c0e8a47..dc8f8dbe06 100644 --- a/tangem-core/src/main/java/com/tangem/TangemSdk.kt +++ b/tangem-core/src/main/java/com/tangem/TangemSdk.kt @@ -70,7 +70,7 @@ class TangemSdk( * in the form of [SignResponse] if the task was performed successfully * or [TangemSdkError] in case of an error. */ - fun sign(hashes: Array, cardId: String, initialMessage: Message? = null, + fun sign(hashes: Array, cardId: String? = null, initialMessage: Message? = null, callback: (result: CompletionResult) -> Unit) { startSessionWithRunnable(SignCommand(hashes), cardId, initialMessage, callback) } @@ -88,7 +88,7 @@ class TangemSdk( * card response in the form of [ReadIssuerDataResponse] if the task was performed successfully * or [TangemSdkError] in case of an error. */ - fun readIssuerData(cardId: String, initialMessage: Message? = null, + fun readIssuerData(cardId: String? = null, initialMessage: Message? = null, callback: (result: CompletionResult) -> Unit) { startSessionWithRunnable(ReadIssuerDataCommand(config.issuerPublicKey), cardId, initialMessage, callback) } @@ -107,7 +107,7 @@ class TangemSdk( * card response in the form of [ReadIssuerExtraDataResponse] if the task was performed successfully * or [TangemSdkError] in case of an error. */ - fun readIssuerExtraData(cardId: String, + fun readIssuerExtraData(cardId: String? = null, callback: (result: CompletionResult) -> Unit) { startSessionWithRunnable(ReadIssuerExtraDataCommand(config.issuerPublicKey), cardId, null, callback) } @@ -128,7 +128,7 @@ class TangemSdk( * card response in the form of [WriteIssuerDataResponse] if the task was performed successfully * or [TangemSdkError] in case of an error. */ - fun writeIssuerData(cardId: String, + fun writeIssuerData(cardId: String? = null, issuerData: ByteArray, issuerDataSignature: ByteArray, issuerDataCounter: Int? = null, @@ -166,7 +166,7 @@ class TangemSdk( * card response in the form of [WriteIssuerDataResponse] if the task was performed successfully * or [TangemSdkError] in case of an error. */ - fun writeIssuerExtraData(cardId: String, + fun writeIssuerExtraData(cardId: String? = null, issuerData: ByteArray, startingSignature: ByteArray, finalizingSignature: ByteArray, @@ -195,7 +195,7 @@ class TangemSdk( * Writing of UserCounter and UserData is protected only by PIN1. */ fun writeUserData( - cardId: String, + cardId: String? = null, userData: ByteArray? = null, userCounter: Int? = null, initialMessage: Message? = null, @@ -219,7 +219,7 @@ class TangemSdk( * UserProtectedCounter and UserProtectedData require PIN2 for confirmation. */ fun writeProtectedUserData( - cardId: String, + cardId: String? = null, userProtectedData: ByteArray? = null, userProtectedCounter: Int? = null, initialMessage: Message? = null, @@ -248,7 +248,7 @@ class TangemSdk( * card response in the form of [ReadUserDataResponse] if the task was performed successfully * or [TangemSdkError] in case of an error. */ - fun readUserData(cardId: String, initialMessage: Message? = null, + fun readUserData(cardId: String? = null, initialMessage: Message? = null, callback: (result: CompletionResult) -> Unit) { startSessionWithRunnable(ReadUserDataCommand(), cardId, initialMessage, callback) } @@ -270,7 +270,7 @@ class TangemSdk( * card response in the form of [CreateWalletResponse] if the task was performed successfully * or [TangemSdkError] in case of an error. */ - fun createWallet(cardId: String, initialMessage: Message? = null, + fun createWallet(cardId: String? = null, initialMessage: Message? = null, callback: (result: CompletionResult) -> Unit) { startSessionWithRunnable(CreateWalletTask(), cardId, initialMessage, callback) } @@ -289,7 +289,7 @@ class TangemSdk( * card response in the form of [PurgeWalletResponse] if the task was performed successfully * or [TangemSdkError] in case of an error. */ - fun purgeWallet(cardId: String, initialMessage: Message? = null, + fun purgeWallet(cardId: String? = null, initialMessage: Message? = null, callback: (result: CompletionResult) -> Unit) { startSessionWithRunnable(PurgeWalletCommand(), cardId, initialMessage, callback) } @@ -307,7 +307,7 @@ class TangemSdk( * card response in the form of [DepersonalizeResponse] if the task was performed successfully * or [TangemSdkError] in case of an error. * */ - fun depersonalize(cardId: String, initialMessage: Message? = null, + fun depersonalize(cardId: String? = null, initialMessage: Message? = null, callback: (result: CompletionResult) -> Unit) { startSessionWithRunnable(DepersonalizeCommand(), cardId, initialMessage, callback) } diff --git a/tangem-core/src/main/java/com/tangem/TangemSdkError.kt b/tangem-core/src/main/java/com/tangem/TangemSdkError.kt index bcf51d0617..4aabb3da6b 100644 --- a/tangem-core/src/main/java/com/tangem/TangemSdkError.kt +++ b/tangem-core/src/main/java/com/tangem/TangemSdkError.kt @@ -101,7 +101,10 @@ sealed class TangemSdkError(val code: Int) : Exception(code.toString()) { * Tangem cards can sign currently up to 10 hashes during one [com.tangem.commands.SignCommand]. * This error is returned when a [com.tangem.commands.SignCommand] receives more than 10 hashes to sign. */ - class TooManyhHashesInOneTransaction : TangemSdkError(40906) + class TooManyHashesInOneTransaction : TangemSdkError(40906) + + //Write Extra Issuer Data Errors + class ExendedDataSizeTooLarge : TangemSdkError(41101) //General Errors class NotPersonalized() : TangemSdkError(40001) @@ -114,6 +117,7 @@ sealed class TangemSdkError(val code: Int) : Exception(code.toString()) { */ class VerificationFailed : TangemSdkError(40005) class DataSizeTooLarge : TangemSdkError(40006) + /** * This error is returned when [ReadIssuerDataTask] or [ReadIssuerExtraDataTask] expects a counter * (when the card's requires it), but the counter is missing. 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 e7e90c5123..0b17642735 100644 --- a/tangem-core/src/main/java/com/tangem/commands/Command.kt +++ b/tangem-core/src/main/java/com/tangem/commands/Command.kt @@ -19,6 +19,8 @@ interface CommandResponse */ abstract class Command : CardSessionRunnable { + override val performPreflightRead: Boolean = true + /** * Serializes data into an array of [com.tangem.common.tlv.Tlv], * then creates [CommandApdu] with this data. diff --git a/tangem-core/src/main/java/com/tangem/commands/SignCommand.kt b/tangem-core/src/main/java/com/tangem/commands/SignCommand.kt index 607ad41cd7..fc926be116 100644 --- a/tangem-core/src/main/java/com/tangem/commands/SignCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/SignCommand.kt @@ -112,7 +112,7 @@ class SignCommand(private val hashes: Array) private fun checkForErrors() { if (hashes.isEmpty()) throw TangemSdkError.EmptyHashes() - if (hashes.size > 10) throw TangemSdkError.TooManyhHashesInOneTransaction() + if (hashes.size > 10) throw TangemSdkError.TooManyHashesInOneTransaction() if (hashes.any { it.size != hashSizes }) throw TangemSdkError.HashSizeMustBeEqual() } diff --git a/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt b/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt index 8cb0dfc949..84e0b50851 100644 --- a/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/WriteIssuerExtraDataCommand.kt @@ -94,7 +94,7 @@ class WriteIssuerExtraDataCommand( return true } if (issuerData.size > MAX_SIZE) { - callback(CompletionResult.Failure(TangemSdkError.DataSizeTooLarge())) + callback(CompletionResult.Failure(TangemSdkError.ExendedDataSizeTooLarge())) return true } if (!isCounterValid(issuerDataCounter, card)) { diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/converter/ResponseConverter.kt b/tangem-core/src/main/java/com/tangem/commands/common/ResponseConverter.kt similarity index 97% rename from tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/converter/ResponseConverter.kt rename to tangem-core/src/main/java/com/tangem/commands/common/ResponseConverter.kt index 392bdc9f96..54d38798a1 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/converter/ResponseConverter.kt +++ b/tangem-core/src/main/java/com/tangem/commands/common/ResponseConverter.kt @@ -1,9 +1,9 @@ -package com.tangem.tangem_sdk_new.converter +package com.tangem.commands.common import com.google.gson.* import com.tangem.commands.* +import com.tangem.common.extensions.print import com.tangem.common.extensions.toHexString -import com.tangem.tangem_sdk_new.extensions.print import java.lang.reflect.Type import java.text.DateFormat import java.util.* diff --git a/tangem-core/src/main/java/com/tangem/commands/personalization/DepersonalizeCommand.kt b/tangem-core/src/main/java/com/tangem/commands/personalization/DepersonalizeCommand.kt index c3bb2d4de8..00dccae6a1 100644 --- a/tangem-core/src/main/java/com/tangem/commands/personalization/DepersonalizeCommand.kt +++ b/tangem-core/src/main/java/com/tangem/commands/personalization/DepersonalizeCommand.kt @@ -1,12 +1,8 @@ package com.tangem.commands.personalization -import com.tangem.CardSession import com.tangem.SessionEnvironment -import com.tangem.TangemSdkError -import com.tangem.commands.CardStatus import com.tangem.commands.Command import com.tangem.commands.CommandResponse -import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.Instruction import com.tangem.common.apdu.ResponseApdu @@ -21,17 +17,19 @@ data class DepersonalizeResponse(val success: Boolean) : CommandResponse */ class DepersonalizeCommand : Command() { - override fun performPreCheck(session: CardSession, callback: (result: CompletionResult) -> Unit): Boolean { - if (session.environment.card?.status == CardStatus.NotPersonalized) { - callback(CompletionResult.Failure(TangemSdkError.NotPersonalized())) - return true - } - if (session.environment.card?.firmwareVersion?.contains("SDK") == false) { - callback(CompletionResult.Failure(TangemSdkError.CannotBeDepersonalized())) - return true - } - return false - } + override val performPreflightRead = false + +// override fun performPreCheck(session: CardSession, callback: (result: CompletionResult) -> Unit): Boolean { +// if (session.environment.card?.status == CardStatus.NotPersonalized) { +// callback(CompletionResult.Failure(TangemSdkError.NotPersonalized())) +// return true +// } +// if (session.environment.card?.firmwareVersion?.contains("SDK") == false) { +// callback(CompletionResult.Failure(TangemSdkError.CannotBeDepersonalized())) +// return true +// } +// return false +// } override fun serialize(environment: SessionEnvironment): CommandApdu { return CommandApdu( diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/List.kt b/tangem-core/src/main/java/com/tangem/common/extensions/List.kt similarity index 89% rename from tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/List.kt rename to tangem-core/src/main/java/com/tangem/common/extensions/List.kt index a86c326453..2579809de6 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/List.kt +++ b/tangem-core/src/main/java/com/tangem/common/extensions/List.kt @@ -1,4 +1,4 @@ -package com.tangem.tangem_sdk_new.extensions +package com.tangem.common.extensions fun List.print(delimiter: String = ", ", wrap: Boolean = true): String { val builder = StringBuilder() diff --git a/tangem-core/src/main/java/com/tangem/tasks/CreateWalletTask.kt b/tangem-core/src/main/java/com/tangem/tasks/CreateWalletTask.kt index c9b87b4da2..317134af60 100644 --- a/tangem-core/src/main/java/com/tangem/tasks/CreateWalletTask.kt +++ b/tangem-core/src/main/java/com/tangem/tasks/CreateWalletTask.kt @@ -11,6 +11,8 @@ import com.tangem.common.CompletionResult class CreateWalletTask : CardSessionRunnable { + override val performPreflightRead = true + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { val curve = session.environment.card?.curve if (curve == null) { diff --git a/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt b/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt index db78568806..c05384d5a1 100644 --- a/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt +++ b/tangem-core/src/main/java/com/tangem/tasks/ScanTask.kt @@ -13,6 +13,8 @@ import com.tangem.common.CompletionResult */ internal class ScanTask : CardSessionRunnable { + override val performPreflightRead = true + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { val card = session.environment.card diff --git a/tangem-devkit/build.gradle b/tangem-devkit/build.gradle index 266e0bf816..fd35b8f7ed 100644 --- a/tangem-devkit/build.gradle +++ b/tangem-devkit/build.gradle @@ -9,8 +9,8 @@ android { applicationId "com.tangem.devkit" minSdkVersion 21 targetSdkVersion 29 - versionCode 3 - versionName "1.1" + versionCode 4 + versionName "1.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/actions/DepersonalizeAction.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/actions/DepersonalizeAction.kt index d938eb85ad..9cfec29492 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/actions/DepersonalizeAction.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/domain/actions/DepersonalizeAction.kt @@ -1,26 +1,14 @@ 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 com.tangem.devkit.ucase.domain.paramsManager.triggers.afterAction.AfterScanModifier /** [REDACTED_AUTHOR] */ class DepersonalizeAction : BaseAction() { override fun executeMainAction(payload: PayloadHolder, attrs: AttrForAction, callback: ActionCallback) { - val item = attrs.itemList.findItem(TlvId.CardId) ?: return - val cardId = item.viewModel.data as? String ?: return - - attrs.tangemSdk.depersonalize(cardId) { 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 - } + attrs.tangemSdk.depersonalize { handleResult(payload, it, AfterScanModifier(), attrs, callback) } } } \ No newline at end of file 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 bb6b16540e..350a72b9f1 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 @@ -10,13 +10,7 @@ import com.tangem.devkit.ucase.variants.TlvId */ class ScanItemsManager : BaseItemsManager(ScanAction()) -class DepersonalizeItemsManager : BaseItemsManager(DepersonalizeAction()) { - - init { - setItemChangeConsequences(CardIdConsequence()) - setItems(listOf(EditTextItem(TlvId.CardId, null))) - } -} +class DepersonalizeItemsManager : BaseItemsManager(DepersonalizeAction()) class SignItemsManager : BaseItemsManager(SignAction()) { diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/depersonalize/ui/DepersonalizeActionFragment.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/depersonalize/ui/DepersonalizeActionFragment.kt index 802ba1e807..9d1409d58d 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/depersonalize/ui/DepersonalizeActionFragment.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/depersonalize/ui/DepersonalizeActionFragment.kt @@ -1,8 +1,18 @@ package com.tangem.devkit.ucase.variants.depersonalize.ui +import android.os.Bundle +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.widget.FrameLayout +import android.widget.TextView +import com.tangem.commands.Card +import com.tangem.devkit.R import com.tangem.devkit.ucase.domain.paramsManager.ItemsManager import com.tangem.devkit.ucase.domain.paramsManager.managers.DepersonalizeItemsManager import com.tangem.devkit.ucase.ui.BaseCardActionFragment +import com.tangem.devkit.ucase.variants.responses.ui.ResponseFragment +import ru.dev.gbixahue.eu4d.lib.android._android.views.dpToPx /** [REDACTED_AUTHOR] @@ -10,4 +20,37 @@ import com.tangem.devkit.ucase.ui.BaseCardActionFragment class DepersonalizeActionFragment : BaseCardActionFragment() { override val itemsManager: ItemsManager by lazy { DepersonalizeItemsManager() } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val howToUseView = createHowToUse() + contentContainer.layoutParams = FrameLayout.LayoutParams(-1, -1, Gravity.CENTER) + contentContainer.addView(howToUseView) + } + + private fun createHowToUse(): View { + val fl = FrameLayout(requireContext()) + fl.layoutParams = FrameLayout.LayoutParams(-1, -1, Gravity.CENTER) + val tv = TextView(requireContext()).apply { + val padding = dpToPx(16f).toInt() + setPadding(padding, 0, padding, 0) + gravity = Gravity.CENTER + setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f) + setText(R.string.htu_depersonalize_action) + } + fl.addView(tv) + return fl + } + + override fun initViews() { + swrLayout.isEnabled = false + actionFab.setOnClickListener { actionVM.invokeMainAction() } + } + + override fun handleResponseCardData(card: Card) { + super.handleResponseCardData(card) + val bundle = ResponseFragment.setTittle(R.string.fg_name_response_depersonalization) + navigateTo(R.id.action_nav_card_action_to_response_screen, bundle, null) + } } \ No newline at end of file diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/ItemTypes.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/ItemTypes.kt index f8c304044c..052430a792 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/ItemTypes.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/personalize/converter/ItemTypes.kt @@ -38,10 +38,11 @@ class ItemTypes { ) val hiddenList = mutableListOf( - CardNumberId.Series, CardNumberId.BatchId, PinsId.Pin3, SigningMethodId.SignExternal, + CardNumberId.Series, CardNumberId.BatchId, SigningMethodId.SignExternal, SignHashExPropId.CryptoExKey, SignHashExPropId.CheckPin3, SettingsMaskId.OneApdu, SettingsMaskId.UseBlock, SettingsMaskId.ProtectIssuerDataAgainstReplay, - SignHashExPropId.RequireTerminalCertSig, SignHashExPropId.RequireTerminalTxSig + SignHashExPropId.RequireTerminalCertSig, SignHashExPropId.RequireTerminalTxSig, + BlockId.Pins, PinsId.Pin, PinsId.Pin2, PinsId.Pin3, PinsId.Cvc ) val oftenUsedList = listOf( diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/responses/converter/BaseResponseConverter.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/responses/converter/BaseResponseConverter.kt index 34ead3b362..bf9667298b 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/responses/converter/BaseResponseConverter.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/responses/converter/BaseResponseConverter.kt @@ -1,9 +1,9 @@ package com.tangem.devkit.ucase.variants.responses.converter +import com.tangem.commands.common.ResponseFieldConverter import com.tangem.devkit._arch.structure.Id import com.tangem.devkit._arch.structure.abstraction.* import com.tangem.devkit.ucase.variants.responses.item.TextHeaderItem -import com.tangem.tangem_sdk_new.converter.ResponseFieldConverter import ru.dev.gbixahue.eu4d.lib.kotlin.stringOf /** diff --git a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/responses/ui/ResponseFragment.kt b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/responses/ui/ResponseFragment.kt index ea236daef2..44097bf01f 100644 --- a/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/responses/ui/ResponseFragment.kt +++ b/tangem-devkit/src/main/java/com/tangem/devkit/ucase/variants/responses/ui/ResponseFragment.kt @@ -8,6 +8,7 @@ import androidx.core.os.bundleOf import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.Observer +import com.tangem.commands.common.ResponseConverter import com.tangem.devkit.R import com.tangem.devkit._arch.widget.WidgetBuilder import com.tangem.devkit._main.MainViewModel @@ -15,7 +16,6 @@ import com.tangem.devkit.extensions.shareText import com.tangem.devkit.ucase.ui.BaseFragment import com.tangem.devkit.ucase.variants.responses.ResponseViewModel import com.tangem.devkit.ucase.variants.responses.ui.widget.ResponseItemBuilder -import com.tangem.tangem_sdk_new.converter.ResponseConverter /** [REDACTED_AUTHOR] diff --git a/tangem-devkit/src/main/res/values/strings_action.xml b/tangem-devkit/src/main/res/values/strings_action.xml index 68d55b7bd9..f1a1c5f9ca 100644 --- a/tangem-devkit/src/main/res/values/strings_action.xml +++ b/tangem-devkit/src/main/res/values/strings_action.xml @@ -30,5 +30,6 @@ This command writes to the card User Data and User Counter fields. This command writes to the card User Protected Data and User Protected Counter fields. + To depersonalize the card, press the button at the right bottom corner of the screen \ No newline at end of file diff --git a/tangem-sdk-old/build.gradle b/tangem-sdk-old/build.gradle index e7ef5a0015..189bc3b334 100644 --- a/tangem-sdk-old/build.gradle +++ b/tangem-sdk-old/build.gradle @@ -24,7 +24,7 @@ android { versionNameSuffix "-beta" } } - buildToolsVersion '28.0.3' + buildToolsVersion '29.0.2' compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 diff --git a/tangem-sdk/build.gradle b/tangem-sdk/build.gradle index 9c2742da98..0664ab0e25 100644 --- a/tangem-sdk/build.gradle +++ b/tangem-sdk/build.gradle @@ -10,7 +10,7 @@ version "$jitpackSdk.version" android { compileSdkVersion 29 - buildToolsVersion "29.0.2" + buildToolsVersion "29.0.3" defaultConfig { @@ -58,6 +58,4 @@ dependencies { testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - - implementation 'com.google.code.gson:gson:2.8.6' } \ No newline at end of file diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultSessionViewDelegate.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultSessionViewDelegate.kt index a15f16521e..d8ca2ad645 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultSessionViewDelegate.kt +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/DefaultSessionViewDelegate.kt @@ -6,12 +6,10 @@ import android.view.HapticFeedbackConstants import android.view.View import android.view.animation.DecelerateInterpolator import com.google.android.material.bottomsheet.BottomSheetDialog -import com.tangem.Log -import com.tangem.LoggerInterface -import com.tangem.Message -import com.tangem.SessionViewDelegate +import com.tangem.* import com.tangem.common.CompletionResult import com.tangem.tangem_sdk_new.extensions.hide +import com.tangem.tangem_sdk_new.extensions.localizedDescription import com.tangem.tangem_sdk_new.extensions.show import com.tangem.tangem_sdk_new.nfc.NfcReader import com.tangem.tangem_sdk_new.ui.TouchCardAnimation @@ -142,14 +140,17 @@ class DefaultSessionViewDelegate(private val reader: NfcReader) : SessionViewDel postUI(300) { readingDialog?.dismiss() } } - override fun onError(errorMessage: String) { + override fun onError(error: TangemSdkError) { postUI { readingDialog?.lTouchCard?.hide() readingDialog?.flSecurityDelay?.hide() readingDialog?.flCompletion?.hide() readingDialog?.flError?.show() readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_error) - readingDialog?.tvTaskText?.text = errorMessage + readingDialog?.tvTaskText?.text = activity.getString( + R.string.error_message, + error.code.toString(), activity.getString(error.localizedDescription()) + ) performHapticFeedback() } } diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/TangemSdkError.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/TangemSdkError.kt new file mode 100644 index 0000000000..09c84716e4 --- /dev/null +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/extensions/TangemSdkError.kt @@ -0,0 +1,56 @@ +package com.tangem.tangem_sdk_new.extensions + +import com.tangem.TangemSdkError +import com.tangem.tangem_sdk_new.R + +fun TangemSdkError.localizedDescription(): Int { + return when (this) { + is TangemSdkError.TagLost -> R.string.error_tag_lost + is TangemSdkError.ExtendedLengthNotSupported -> R.string.error_operation + is TangemSdkError.SerializeCommandError -> R.string.error_operation + is TangemSdkError.DeserializeApduFailed -> R.string.error_operation + is TangemSdkError.EncodingFailedTypeMismatch -> R.string.error_operation + is TangemSdkError.EncodingFailed -> R.string.error_operation + is TangemSdkError.DecodingFailedMissingTag -> R.string.error_operation + is TangemSdkError.DecodingFailedTypeMismatch -> R.string.error_operation + is TangemSdkError.DecodingFailed -> R.string.error_operation + is TangemSdkError.UnknownStatus -> R.string.error_operation + is TangemSdkError.ErrorProcessingCommand -> R.string.error_operation + is TangemSdkError.InvalidState -> R.string.error_operation + is TangemSdkError.InsNotSupported -> R.string.error_operation + is TangemSdkError.InvalidParams -> R.string.error_operation + is TangemSdkError.NeedEncryption -> R.string.error_operation + is TangemSdkError.AlreadyPersonalized -> R.string.error_already_personalized + is TangemSdkError.CannotBeDepersonalized -> R.string.error_cannot_be_depersonalized + is TangemSdkError.Pin1Required -> R.string.error_operation + is TangemSdkError.AlreadyCreated -> R.string.error_already_created + is TangemSdkError.PurgeWalletProhibited -> R.string.error_purge_prohibited + is TangemSdkError.Pin1CannotBeChanged -> R.string.error_pin1_cannot_be_changed + is TangemSdkError.Pin2CannotBeChanged -> R.string.error_pin2_cannot_be_changed + is TangemSdkError.Pin1CannotBeDefault -> R.string.error_pin1_cannot_be_default + is TangemSdkError.NoRemainingSignatures -> R.string.error_no_remaining_signatures + is TangemSdkError.EmptyHashes -> R.string.error_empty_hashes + is TangemSdkError.HashSizeMustBeEqual -> R.string.error_cannot_be_signed + is TangemSdkError.CardIsEmpty -> R.string.error_card_is_empty + is TangemSdkError.SignHashesNotAvailable -> R.string.error_cannot_be_signed + is TangemSdkError.TooManyHashesInOneTransaction -> R.string.error_cannot_be_signed + is TangemSdkError.NotPersonalized -> R.string.error_not_personalized + is TangemSdkError.NotActivated -> R.string.error_not_activated + is TangemSdkError.CardIsPurged -> R.string.error_purged + is TangemSdkError.Pin2OrCvcRequired -> R.string.error_operation + is TangemSdkError.VerificationFailed -> R.string.error_verification_failed + is TangemSdkError.DataSizeTooLarge -> R.string.error_data_size_too_large + is TangemSdkError.ExendedDataSizeTooLarge -> R.string.error_data_size_too_large_extended + is TangemSdkError.MissingCounter -> R.string.error_missing_counter + is TangemSdkError.OverwritingDataIsProhibited -> R.string.error_data_cannot_be_written + is TangemSdkError.DataCannotBeWritten -> R.string.error_data_cannot_be_written + is TangemSdkError.MissingIssuerPubicKey -> R.string.error_missing_issuer_public_key + is TangemSdkError.UnknownError -> R.string.error_operation + is TangemSdkError.UserCancelled -> R.string.error_user_cancelled + is TangemSdkError.Busy -> R.string.error_busy + is TangemSdkError.MissingPreflightRead -> R.string.error_operation + is TangemSdkError.WrongCardNumber -> R.string.error_wrong_card_number + is TangemSdkError.WrongCardType -> R.string.error_wrong_card_type + is TangemSdkError.CardError -> R.string.error_card_error + } +} \ No newline at end of file diff --git a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcReader.kt b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcReader.kt index aa5ce9972e..559adf3f80 100644 --- a/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcReader.kt +++ b/tangem-sdk/src/main/java/com/tangem/tangem_sdk_new/nfc/NfcReader.kt @@ -10,6 +10,7 @@ import com.tangem.TangemSdkError import com.tangem.common.CompletionResult import com.tangem.common.apdu.CommandApdu import com.tangem.common.apdu.ResponseApdu +import com.tangem.common.extensions.toHexString /** * Provides NFC communication between an Android application and Tangem card. @@ -76,7 +77,9 @@ class NfcReader : CardReader { val rawResponse: ByteArray? try { Log.i(this::class.simpleName!!, "Sending data to the card, size is ${data?.size}") + Log.v(this::class.simpleName!!, "Raw data that is to be sent to the card: ${data?.toHexString()}") rawResponse = isoDep?.transceive(data) + Log.v(this::class.simpleName!!, "Raw data that was received from the card: ${rawResponse?.toHexString()}") } catch (exception: TagLostException) { callback?.invoke(CompletionResult.Failure(TangemSdkError.TagLost())) isoDep = null diff --git a/tangem-sdk/src/main/res/values/strings_errors.xml b/tangem-sdk/src/main/res/values/strings_errors.xml new file mode 100644 index 0000000000..b41a0e5d1f --- /dev/null +++ b/tangem-sdk/src/main/res/values/strings_errors.xml @@ -0,0 +1,32 @@ + + + %s: %s + + An error occurred during the operation + Tag lost. + This card has already been personalized + This card cannot be depersonalized + A wallet has already been created + This card’s configuration does not allow you to erase the wallet + PIN1 Access Code cannot be changed + PIN2 Passcode cannot be changed + PIN1 Access Code cannot be changed to the default value. Please create a new PIN1 Access Code. + You cannot sign data with this wallet because the card has reached its signature limit + There is nothing to sign here + This data cannot be signed + Please create a wallet to sign data + This card has not been personalized + This card has not been activated + You cannot sign data with this wallet because the card has been purged + Data verification failed + Data size exceeds the max limit of 512 bytes + Data size exceeds the max limit of 32 Kbytes + The data cannot be written to the card because there is no counter + The data cannot be written to the card + The issuer public key is missing + The user cancelled the operation + The CardSession is running a command. Please wait and try again + You tapped a different card. Please match the in-app Card ID to the your physical Card ID to continue this process. + This card is configured for a different app. Please see the information on your card and download the related app. + Your card is missing essential data + \ No newline at end of file