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/wallet/LoadedWalletFragment.kt b/app/src/main/java/com/tangem/ui/fragment/wallet/LoadedWalletFragment.kt index 2eca438aa8..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,7 +25,7 @@ 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 @@ -49,11 +50,13 @@ 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.util.* import kotlin.concurrent.timerTask @@ -62,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 @@ -69,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 @@ -111,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) @@ -127,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) @@ -140,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() } @@ -149,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) @@ -176,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 -> { } } @@ -297,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() @@ -308,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) { @@ -703,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 @@ -836,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 { @@ -845,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/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/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-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 085db2391b..7b4e3dd92f 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 {