Updated on 2026-08-14
This commit is contained in:
commit
c55736dbd4
34 changed files with 998 additions and 251 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
15
app/src/main/java/com/tangem/data/network/PayIdApi.java
Normal file
15
app/src/main/java/com/tangem/data/network/PayIdApi.java
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
53
app/src/main/java/com/tangem/wallet/xrp/XrpTaggedAddress.kt
Normal file
53
app/src/main/java/com/tangem/wallet/xrp/XrpTaggedAddress.kt
Normal 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?
|
||||
)
|
||||
37
app/src/main/res/drawable/ic_payid_logo_dark.xml
Normal file
37
app/src/main/res/drawable/ic_payid_logo_dark.xml
Normal 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>
|
||||
43
app/src/main/res/layout/dialog_pay_id.xml
Normal file
43
app/src/main/res/layout/dialog_pay_id.xml
Normal 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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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/";
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ version "$jitpackSdk.version"
|
|||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion "29.0.2"
|
||||
buildToolsVersion "29.0.3"
|
||||
|
||||
|
||||
defaultConfig {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue