Updated on 2026-08-14

This commit is contained in:
Tangem 2020-06-17 20:53:14 +03:00
commit c55736dbd4
34 changed files with 998 additions and 251 deletions

View file

@ -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'

View file

@ -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<Blockchain> 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);
}
}

View file

@ -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<PayIdResponse> getAddress(@Path("user") String user, @Header("Accept") String acceptHeader);
}

View file

@ -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<PayIdResponse> 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<PayIdResponse> addressSingle = api.getAddress(user, getAcceptHeader(blockchain))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((object, throwable) -> requestsCount--);
addressSingle.subscribe(addressObserver);
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.data.network.model
import com.google.gson.annotations.SerializedName
data class PayIdResponse(
@SerializedName("addresses")
var addresses: List<PayIdAddress>? = 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
)

View file

@ -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 {

View file

@ -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 {

View file

@ -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<CharSequence>
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<CharSequence>(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<Float> { rate ->
viewModel.getRateInfo().observe(viewLifecycleOwner, Observer<Float> { 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()
}
}

View file

@ -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<Float> by lazy {
MutableLiveData<Float>().also {
@ -17,6 +21,10 @@ class LoadedWalletViewModel : ViewModel() {
}
}
private val scope = viewModelScope.coroutineContext + CoroutineExceptionHandler { _, ex ->
throw ex
}
fun getRateInfo(): LiveData<Float> {
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<Result<PayIdResponse>> {
return liveData(viewModelScope.coroutineContext) {
emit(
payIdService.getPayId(
cardId,
publicKey
)
)
}
}
fun setPayId(
cardId: String,
publicKey: String,
payId: String,
address: String,
network: String
): LiveData<Result<SetPayIdResponse>> {
return liveData(scope) {
emit(
payIdService.setPayId(
cardId, publicKey, payId, address, network
)
)
}
}
}

View file

@ -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;
}
}

View file

@ -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<PayIdResponse> payIdObserver = new DisposableSingleObserver<PayIdResponse>() {
@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 {

View file

@ -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());

View file

@ -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<PayIdResponse> observer = new DisposableSingleObserver<PayIdResponse>() {
@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() {

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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<PayIdResponse> observer = new DisposableSingleObserver<PayIdResponse>() {
@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

View file

@ -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?
)

View file

@ -0,0 +1,37 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="80dp"
android:height="25dp"
android:viewportWidth="314"
android:viewportHeight="100">
<group>
<clip-path
android:pathData="M0.192,0h313v99.168h-313z"/>
<path
android:pathData="M109.98,26.948H130.523C143.582,26.948 149.462,34.196 149.462,44.072C149.462,53.948 143.582,61.221 130.523,61.221H119.832V77.426H109.98V26.948ZM130.676,52.111C137.218,52.111 139.458,48.716 139.458,44.072C139.458,39.428 137.218,36.136 130.676,36.136H119.832V52.111H130.676Z"
android:fillColor="#000E33"/>
<path
android:pathData="M152.008,58.746C152.008,46.624 160.484,38.279 172.55,38.279C184.489,38.279 192.787,46.369 192.787,58.644V77.375H184.413V69.898C182.096,75.537 176.954,78.396 170.972,78.396C161.732,78.421 152.008,71.786 152.008,58.746ZM183.674,58.388C183.674,51.651 179.194,46.879 172.474,46.879C165.754,46.879 161.274,51.651 161.274,58.388C161.274,65.126 165.805,69.898 172.474,69.898C179.143,69.898 183.674,65.074 183.674,58.388Z"
android:fillColor="#000E33"/>
<path
android:pathData="M200.45,81.611H209.614C211.167,85.158 214.17,86.766 218.778,86.766C225.015,86.766 228.553,83.78 228.553,76.354V71.556C226.236,75.716 222.24,77.962 217.251,77.962C208.468,77.962 200.17,72.245 200.17,58.669V39.3H209.308V58.669C209.308,65.916 213.152,69.387 218.931,69.387C224.556,69.387 228.553,65.763 228.553,58.669V39.3H237.717V76.124C237.717,89.394 229.317,94.728 218.778,94.728C209.919,94.702 202.588,90.594 200.45,81.611Z"
android:fillColor="#000E33"/>
<path
android:pathData="M248.204,26.948H258.081V77.426H248.204V26.948Z"
android:fillColor="#000E33"/>
<path
android:pathData="M268.441,26.948H286.311C303.926,26.948 313.192,37.054 313.192,52.187C313.192,67.32 303.952,77.426 286.311,77.426H268.441V26.948ZM286.311,68.315C297.944,68.315 303.163,61.987 303.163,52.187C303.163,42.388 297.944,36.136 286.311,36.136H278.318V68.341L286.311,68.315Z"
android:fillColor="#000E33"/>
</group>
<path
android:pathData="M53.687,79.504C60.442,78.606 66.698,75.461 71.447,70.575V70.575L99.1,42.231C97.34,41.943 95.558,41.809 93.774,41.832C88.916,41.862 84.136,43.063 79.842,45.334C77.133,46.781 74.665,48.637 72.524,50.838L62.325,61.287C62.264,61.354 62.198,61.42 62.132,61.49C58.926,64.777 54.553,66.667 49.963,66.751C45.372,66.835 40.933,65.105 37.61,61.936L48.262,72.329C50.423,74.432 52.252,76.851 53.687,79.504V79.504Z"
android:fillColor="#000E33"/>
<path
android:pathData="M28.525,71.246L56.871,98.898C57.952,92.255 56.834,85.44 53.686,79.491C52.252,76.841 50.424,74.424 48.265,72.322L37.613,61.93C35.941,60.339 34.602,58.432 33.673,56.319C32.743,54.207 32.243,51.931 32.2,49.623C32.157,47.316 32.572,45.023 33.422,42.877C34.272,40.732 35.539,38.776 37.151,37.124L26.764,47.773C24.624,49.973 22.155,51.828 19.447,53.273C20.338,60.123 23.541,66.463 28.525,71.246V71.246Z"
android:fillColor="#000E33"/>
<path
android:pathData="M0.192,56.395C1.951,56.681 3.732,56.813 5.515,56.791C10.373,56.759 15.152,55.557 19.447,53.285C22.156,51.841 24.625,49.986 26.765,47.785L37.161,37.13C38.759,35.488 40.666,34.178 42.771,33.273C44.876,32.369 47.139,31.888 49.43,31.859C51.721,31.83 53.995,32.252 56.123,33.102C58.251,33.952 60.19,35.213 61.831,36.813L51.321,26.56C49.079,24.381 47.196,21.863 45.738,19.097C38.937,19.973 32.633,23.128 27.854,28.045L0.192,56.395Z"
android:fillColor="#000E33"/>
<path
android:pathData="M62.328,61.293L72.527,50.844C74.668,48.644 77.136,46.787 79.845,45.341C78.973,38.63 75.881,32.403 71.061,27.653L42.711,0C42.424,1.759 42.291,3.54 42.316,5.323C42.346,10.124 43.52,14.849 45.739,19.107C47.196,21.872 49.08,24.391 51.321,26.57L61.831,36.823C65.109,40.02 66.998,44.378 67.091,48.956C67.184,53.535 65.473,57.966 62.328,61.293V61.293Z"
android:fillColor="#000E33"/>
</vector>

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/dialogTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Create Pay ID"
android:layout_gravity="center"
android:textAppearance="?android:attr/textAppearanceLarge" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="40dp">
<EditText
android:id="@+id/etPayId"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:hint="Pay ID"
android:inputType="text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="$payid.tangem.com"/>
</LinearLayout>
<TextView
android:id="@+id/tvErrorPayId"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
</LinearLayout>

View file

@ -123,19 +123,38 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHeight_min="wrap"
app:layout_constraintTop_toBottomOf="@+id/guideline">
<TextView
android:id="@+id/tvBlockchain"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:fontFamily="@font/maaxmedium"
android:textAlignment="center"
android:textColor="@color/primary_dark"
android:textSize="18dp"
android:textStyle="normal"
tools:text="Bitcoin" />
android:orientation="horizontal"
android:layout_gravity="center"
android:gravity="center">
<TextView
android:id="@+id/tvBlockchain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:fontFamily="@font/maaxmedium"
android:textAlignment="center"
android:textColor="@color/primary_dark"
android:textSize="18dp"
android:textStyle="normal"
tools:text="Bitcoin" />
<ImageView
android:id="@+id/ivPayId"
android:layout_width="80dp"
android:layout_height="30dp"
android:layout_marginBottom="0.5dp"
android:layout_marginStart="24dp"
android:src="@drawable/ic_payid_logo_dark"
android:visibility="gone"/>
</LinearLayout>
<TextView
android:id="@+id/tvWallet"

View file

@ -116,6 +116,13 @@
<string name="loaded_wallet_load_via_qr">Show QR-code</string>
<string name="loaded_wallet_dialog_show_qr">LOAD</string>
<string name="loaded_wallet_create_pay_id">Create PayID</string>
<string name="create_pay_id_create">Create</string>
<string name="create_pay_id_empty">Enter PayID first</string>
<string name="create_pay_id_success">New PayID was created</string>
<string name="create_pay_id_error">Error creating PayID</string>
<!-- LoadedWallet Blockchain Engines -->
<string name="balance_validator_first_line_verified_balance">Verified balance</string>
<string name="balance_validator_first_line_unknown_balance">Unknown balance</string>
@ -182,6 +189,7 @@
<!-- PrepareTransaction -->
<string name="prepare_transaction_hint_enter_address">enter wallet address</string>
<string name="prepare_transaction_hint_address_or_pay_id">Address or PayID</string>
<string name="prepare_transaction_hint_enter_amount">enter amount</string>
<string name="prepare_transaction_btn_verify">Verify</string>
<string name="prepare_transaction_error_not_enough_funds">Not enough funds</string>

View file

@ -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 {

View file

@ -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
}

View file

@ -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'

View file

@ -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

View file

@ -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'

View file

@ -0,0 +1,40 @@
package com.tangem.server_android
import kotlinx.coroutines.delay
import java.io.IOException
suspend fun <T> 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<out T> {
data class Success<out T>(val data: T) : Result<T>()
data class Failure(val error: Throwable?) : Result<Nothing>()
}
suspend fun <T>performRequest(block: suspend () -> T): Result<T> {
return try {
val result = retryIO { block() }
Result.Success(result)
} catch (exception: Exception) {
Result.Failure(exception)
}
}

View file

@ -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<PayIdResponse> {
return performRequest { tangemApi.getPayId(cardId, publicKey) }
}
suspend fun setPayId(cardId: String, publicKey: String, payId: String, address: String, network: String): Result<SetPayIdResponse> {
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
}

View file

@ -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;
}
}
}

View file

@ -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/";
}

View file

@ -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<CardVerifyAndGetInfo.Response> getCardVerifyAndGetInfo(@Body CardVerifyAndGetInfo.Request requestBody);
@GET(Server.ApiTangem.Method.ARTWORK)
Call<ResponseBody> getArtwork(@Query("artworkId") String artworkId, @Query("CID") String CID, @Query("publicKey") String publicKey);
}

View file

@ -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<CardVerifyAndGetInfo.Response>
@GET(Server.ApiTangem.Method.ARTWORK)
fun getArtwork(
@Query("artworkId") artworkId: String?,
@Query("CID") CID: String?,
@Query("publicKey") publicKey: String?
): Call<ResponseBody>
@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
}

View file

@ -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

View file

@ -10,7 +10,7 @@ version "$jitpackSdk.version"
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
buildToolsVersion "29.0.3"
defaultConfig {