Updated on 2026-08-14
This commit is contained in:
commit
752977cd14
101 changed files with 2188 additions and 556 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 {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ class PurgeFragment : BaseFragment(), NfcAdapter.ReaderCallback, CardProtocol.No
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
// init NFC Antenna
|
||||
nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
|
||||
nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
|
||||
nfcDeviceAntenna.init()
|
||||
|
||||
tvCardID.text = ctx.card.cidDescription
|
||||
|
|
@ -99,7 +99,7 @@ class PurgeFragment : BaseFragment(), NfcAdapter.ReaderCallback, CardProtocol.No
|
|||
}
|
||||
|
||||
override fun onReadWait(msec: Int) {
|
||||
WaitSecurityDelayDialog.onReadWait(activity, msec)
|
||||
activity?.let { WaitSecurityDelayDialog.onReadWait(it, msec) }
|
||||
|
||||
// val readWait = ReadWait()
|
||||
// readWait.msec = msec
|
||||
|
|
@ -107,7 +107,7 @@ class PurgeFragment : BaseFragment(), NfcAdapter.ReaderCallback, CardProtocol.No
|
|||
}
|
||||
|
||||
override fun onReadBeforeRequest(timeout: Int) {
|
||||
WaitSecurityDelayDialog.onReadBeforeRequest(activity, timeout)
|
||||
activity?.let { WaitSecurityDelayDialog.onReadBeforeRequest(it, timeout) }
|
||||
|
||||
// if (!waitSecurityDelayDialogNew.isAdded)
|
||||
// waitSecurityDelayDialogNew.show(supportFragmentManager, WaitSecurityDelayDialogNew.TAG)
|
||||
|
|
@ -119,16 +119,16 @@ class PurgeFragment : BaseFragment(), NfcAdapter.ReaderCallback, CardProtocol.No
|
|||
}
|
||||
|
||||
override fun onReadAfterRequest() {
|
||||
WaitSecurityDelayDialog.onReadAfterRequest(activity)
|
||||
activity?.let { WaitSecurityDelayDialog.onReadAfterRequest(it) }
|
||||
|
||||
// val readAfterRequest = ReadAfterRequest()
|
||||
// EventBus.getDefault().post(readAfterRequest)
|
||||
}
|
||||
|
||||
override fun onReadStart(cardProtocol: CardProtocol) {
|
||||
rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE }
|
||||
rlProgressBar?.post { rlProgressBar.visibility = View.VISIBLE }
|
||||
|
||||
progressBar.post {
|
||||
progressBar?.post {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
progressBar.progress = 5
|
||||
}
|
||||
|
|
@ -179,7 +179,7 @@ class PurgeFragment : BaseFragment(), NfcAdapter.ReaderCallback, CardProtocol.No
|
|||
progressBar?.post {
|
||||
if (cardProtocol.error is CardProtocol.TangemException_ExtendedLengthNotSupported) {
|
||||
if (!NoExtendedLengthSupportDialog.allReadyShowed)
|
||||
NoExtendedLengthSupportDialog().show(activity!!.supportFragmentManager, NoExtendedLengthSupportDialog.TAG)
|
||||
NoExtendedLengthSupportDialog().show(requireActivity().supportFragmentManager, NoExtendedLengthSupportDialog.TAG)
|
||||
} else
|
||||
Toast.makeText(context, R.string.general_notification_scan_again_to_verify, Toast.LENGTH_LONG).show()
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class ValidateIdFragment : BaseFragment(), NavigationResultListener,
|
|||
mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound)
|
||||
|
||||
// init NFC Antenna
|
||||
nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
|
||||
nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
|
||||
nfcDeviceAntenna.init()
|
||||
|
||||
progressBar.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
|
||||
|
|
@ -198,7 +198,7 @@ class ValidateIdFragment : BaseFragment(), NavigationResultListener,
|
|||
}
|
||||
|
||||
override fun onReadStart(cardProtocol: CardProtocol) {
|
||||
rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE }
|
||||
rlProgressBar?.post { rlProgressBar.visibility = View.VISIBLE }
|
||||
|
||||
progressBar?.post {
|
||||
progressBar?.visibility = View.VISIBLE
|
||||
|
|
@ -265,7 +265,7 @@ class ValidateIdFragment : BaseFragment(), NavigationResultListener,
|
|||
NoExtendedLengthSupportDialog().show(requireFragmentManager(), NoExtendedLengthSupportDialog.TAG)
|
||||
}
|
||||
} else {
|
||||
(activity as MainActivity).toastHelper.showSingleToast(
|
||||
(activity as? MainActivity)?.toastHelper?.showSingleToast(
|
||||
context,
|
||||
getString(R.string.general_notification_scan_again)
|
||||
)
|
||||
|
|
@ -313,17 +313,17 @@ class ValidateIdFragment : BaseFragment(), NavigationResultListener,
|
|||
|
||||
override fun onReadBeforeRequest(timeout: Int) {
|
||||
LOG.i(TAG, "onReadBeforeRequest timeout $timeout")
|
||||
WaitSecurityDelayDialog.onReadBeforeRequest(activity!!, timeout)
|
||||
activity?.let { WaitSecurityDelayDialog.onReadBeforeRequest(it, timeout) }
|
||||
}
|
||||
|
||||
override fun onReadAfterRequest() {
|
||||
LOG.i(TAG, "onReadAfterRequest")
|
||||
WaitSecurityDelayDialog.onReadAfterRequest(activity!!)
|
||||
activity?.let { WaitSecurityDelayDialog.onReadAfterRequest(it) }
|
||||
}
|
||||
|
||||
override fun onReadWait(msec: Int) {
|
||||
LOG.i(TAG, "onReadWait msec $msec")
|
||||
WaitSecurityDelayDialog.onReadWait(activity!!, msec)
|
||||
activity?.let { WaitSecurityDelayDialog.onReadWait(it, msec) }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ class WriteIdFragment : BaseFragment(), NavigationResultListener,
|
|||
mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound)
|
||||
|
||||
// init NFC Antenna
|
||||
nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
|
||||
nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
|
||||
nfcDeviceAntenna.init()
|
||||
|
||||
tvCardID.text = ctx.card!!.cidDescription
|
||||
|
|
@ -137,7 +137,7 @@ class WriteIdFragment : BaseFragment(), NavigationResultListener,
|
|||
}
|
||||
|
||||
override fun onReadStart(cardProtocol: CardProtocol) {
|
||||
rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE }
|
||||
rlProgressBar?.post { rlProgressBar.visibility = View.VISIBLE }
|
||||
|
||||
progressBar?.post {
|
||||
progressBar?.visibility = View.VISIBLE
|
||||
|
|
@ -279,17 +279,17 @@ class WriteIdFragment : BaseFragment(), NavigationResultListener,
|
|||
|
||||
override fun onReadBeforeRequest(timeout: Int) {
|
||||
LOG.i(TAG, "onReadBeforeRequest timeout $timeout")
|
||||
WaitSecurityDelayDialog.onReadBeforeRequest(activity!!, timeout)
|
||||
activity?.let { WaitSecurityDelayDialog.onReadBeforeRequest(it, timeout) }
|
||||
}
|
||||
|
||||
override fun onReadAfterRequest() {
|
||||
LOG.i(TAG, "onReadAfterRequest")
|
||||
WaitSecurityDelayDialog.onReadAfterRequest(activity!!)
|
||||
activity?.let { WaitSecurityDelayDialog.onReadAfterRequest(it) }
|
||||
}
|
||||
|
||||
override fun onReadWait(msec: Int) {
|
||||
LOG.i(TAG, "onReadWait msec $msec")
|
||||
WaitSecurityDelayDialog.onReadWait(activity!!, msec)
|
||||
activity?.let { WaitSecurityDelayDialog.onReadWait(it, msec) }
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import android.nfc.Tag
|
|||
import android.nfc.tech.IsoDep
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
|
|
@ -80,7 +79,7 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener,
|
|||
mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound)
|
||||
|
||||
// init NFC Antenna
|
||||
nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
|
||||
nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
|
||||
nfcDeviceAntenna.init()
|
||||
|
||||
amount = CoinEngine.Amount(arguments?.getString(Constant.EXTRA_AMOUNT), arguments?.getString(Constant.EXTRA_AMOUNT_CURRENCY))
|
||||
|
|
@ -165,7 +164,7 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener,
|
|||
}
|
||||
|
||||
override fun onReadStart(cardProtocol: CardProtocol) {
|
||||
rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE }
|
||||
rlProgressBar?.post { rlProgressBar.visibility = View.VISIBLE }
|
||||
|
||||
progressBar?.post {
|
||||
progressBar?.visibility = View.VISIBLE
|
||||
|
|
@ -234,7 +233,9 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener,
|
|||
NoExtendedLengthSupportDialog().show(requireFragmentManager(), NoExtendedLengthSupportDialog.TAG)
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(context, R.string.general_notification_scan_again, Toast.LENGTH_LONG).show()
|
||||
(activity as? MainActivity)?.toastHelper?.showSingleToast(
|
||||
context, getString(R.string.general_notification_scan_again)
|
||||
)
|
||||
}
|
||||
progressBar?.progress = 100
|
||||
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
|
||||
|
|
@ -280,7 +281,7 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener,
|
|||
|
||||
override fun onReadBeforeRequest(timeout: Int) {
|
||||
LOG.i(TAG, "onReadBeforeRequest timeout $timeout")
|
||||
WaitSecurityDelayDialog.onReadBeforeRequest(activity!!, timeout)
|
||||
activity?.let { WaitSecurityDelayDialog.onReadBeforeRequest(it, timeout) }
|
||||
|
||||
// if (!waitSecurityDelayDialogNew.isAdded)
|
||||
// waitSecurityDelayDialogNew.show(supportFragmentManager, WaitSecurityDelayDialogNew.TAG)
|
||||
|
|
@ -293,7 +294,7 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener,
|
|||
|
||||
override fun onReadAfterRequest() {
|
||||
LOG.i(TAG, "onReadAfterRequest")
|
||||
WaitSecurityDelayDialog.onReadAfterRequest(activity!!)
|
||||
activity?.let { WaitSecurityDelayDialog.onReadAfterRequest(it) }
|
||||
|
||||
// val readAfterRequest = ReadAfterRequest()
|
||||
// EventBus.getDefault().post(readAfterRequest)
|
||||
|
|
@ -301,7 +302,7 @@ class SignTransactionFragment : BaseFragment(), NavigationResultListener,
|
|||
|
||||
override fun onReadWait(msec: Int) {
|
||||
LOG.i(TAG, "onReadWait msec $msec")
|
||||
WaitSecurityDelayDialog.onReadWait(activity!!, msec)
|
||||
activity?.let { WaitSecurityDelayDialog.onReadWait(it, msec) }
|
||||
|
||||
// val readWait = ReadWait()
|
||||
// readWait.msec = msec
|
||||
|
|
|
|||
|
|
@ -1,68 +1,62 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoin
|
||||
|
||||
|
||||
import com.tangem.blockchain.blockchains.litecoin.LitecoinMainNetParams
|
||||
import com.tangem.blockchain.common.AddressService
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.calculateRipemd160
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import org.bitcoinj.core.AddressFormatException
|
||||
import org.bitcoinj.core.Base58
|
||||
import org.bitcoinj.core.SegwitAddress
|
||||
import org.bitcoinj.core.*
|
||||
import org.bitcoinj.params.MainNetParams
|
||||
import org.bitcoinj.params.TestNet3Params
|
||||
import java.security.MessageDigest
|
||||
|
||||
class BitcoinAddressService(private val testNet: Boolean = false): AddressService {
|
||||
class BitcoinAddressService(private val blockchain: Blockchain) : AddressService {
|
||||
|
||||
private val networkParameters: NetworkParameters = when (blockchain) {
|
||||
Blockchain.Bitcoin -> MainNetParams()
|
||||
Blockchain.BitcoinTestnet -> TestNet3Params()
|
||||
Blockchain.Litecoin -> LitecoinMainNetParams()
|
||||
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
|
||||
}
|
||||
|
||||
override fun makeAddress(walletPublicKey: ByteArray): String {
|
||||
val netSelectionByte = if (testNet) 0x6f.toByte() else 0x00.toByte()
|
||||
val hash1 = walletPublicKey.calculateSha256().calculateRipemd160()
|
||||
val hash2 = byteArrayOf(netSelectionByte).plus(hash1).calculateSha256().calculateSha256()
|
||||
val result = byteArrayOf(netSelectionByte) + hash1 + hash2[0] + hash2[1] + hash2[2] + hash2[3]
|
||||
return Base58.encode(result)
|
||||
}
|
||||
val publicKeyHash = walletPublicKey.calculateSha256().calculateRipemd160()
|
||||
val checksum = byteArrayOf(networkParameters.addressHeader.toByte()).plus(publicKeyHash)
|
||||
.calculateSha256().calculateSha256()
|
||||
val result = byteArrayOf(networkParameters.addressHeader.toByte()) + publicKeyHash + checksum.copyOfRange(0, 4)
|
||||
return Base58.encode(result)
|
||||
}
|
||||
|
||||
override fun validate(address: String): Boolean {
|
||||
if (firstLetters.contains(address.first())) {
|
||||
if (testNet && firstLettersNonTestNet.contains(address.first())) return false
|
||||
if (address.length !in 26..35) return false
|
||||
val decoded = address.decodeBase58() ?: return false
|
||||
val hash = recursiveSha256(decoded, 0, 21, 2)
|
||||
return hash.sliceArray(0..3).contentEquals(decoded.sliceArray(21..24))
|
||||
} else {
|
||||
return validateSegwitAddress(address, testNet)
|
||||
override fun validate(address: String): Boolean {
|
||||
return validateLegacyAddress(address) || validateSegwitAddress(address)
|
||||
}
|
||||
|
||||
private fun validateSegwitAddress(address: String): Boolean {
|
||||
return try {
|
||||
when (blockchain) {
|
||||
Blockchain.Bitcoin -> SegwitAddress.fromBech32(MainNetParams(), address)
|
||||
Blockchain.BitcoinTestnet -> SegwitAddress.fromBech32(TestNet3Params(), address)
|
||||
Blockchain.Litecoin -> SegwitAddress.fromBech32(LitecoinMainNetParams(), address)
|
||||
else -> return false
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun recursiveSha256(data: ByteArray, start: Int, len: Int, recursion: Int): ByteArray {
|
||||
if (recursion == 0) return data
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
md.update(data.sliceArray(start until start + len))
|
||||
return recursiveSha256(md.digest(), 0, 32, recursion - 1)
|
||||
}
|
||||
|
||||
private fun String.decodeBase58(): ByteArray? {
|
||||
return try {
|
||||
Base58.decode(this)
|
||||
} catch (exception: AddressFormatException) {
|
||||
null
|
||||
private fun validateLegacyAddress(address: String): Boolean {
|
||||
return try {
|
||||
when (blockchain) {
|
||||
Blockchain.Bitcoin -> LegacyAddress.fromBase58(MainNetParams(), address)
|
||||
Blockchain.BitcoinTestnet -> LegacyAddress.fromBase58(TestNet3Params(), address)
|
||||
Blockchain.Litecoin -> LegacyAddress.fromBase58(LitecoinMainNetParams(), address)
|
||||
else -> return false
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
|
||||
private fun validateSegwitAddress(address: String, testNet: Boolean): Boolean {
|
||||
return try {
|
||||
if (testNet) {
|
||||
SegwitAddress.fromBech32(TestNet3Params(), address)
|
||||
true
|
||||
} else {
|
||||
SegwitAddress.fromBech32(MainNetParams(), address)
|
||||
true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val firstLetters = "123nm"
|
||||
private const val firstLettersNonTestNet = "13"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,30 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoin
|
||||
|
||||
import com.tangem.blockchain.blockchains.litecoin.LitecoinMainNetParams
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.common.extensions.isZero
|
||||
import org.bitcoinj.core.*
|
||||
import org.bitcoinj.crypto.TransactionSignature
|
||||
import org.bitcoinj.params.MainNetParams
|
||||
import org.bitcoinj.params.TestNet3Params
|
||||
import org.bitcoinj.script.Script
|
||||
import org.bitcoinj.script.ScriptBuilder
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
open class BitcoinTransactionBuilder(
|
||||
private val walletPublicKey: ByteArray, private val testNet: Boolean = false
|
||||
private val walletPublicKey: ByteArray, blockchain: Blockchain
|
||||
) {
|
||||
|
||||
private lateinit var transaction: Transaction
|
||||
protected var networkParameters: NetworkParameters? = null
|
||||
protected var networkParameters = when (blockchain) {
|
||||
Blockchain.Bitcoin, Blockchain.BitcoinCash -> MainNetParams()
|
||||
Blockchain.BitcoinTestnet -> TestNet3Params()
|
||||
Blockchain.Litecoin -> LitecoinMainNetParams()
|
||||
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
|
||||
}
|
||||
var unspentOutputs: List<BitcoinUnspentOutput>? = null
|
||||
|
||||
open fun buildToSign(
|
||||
|
|
@ -25,11 +34,6 @@ open class BitcoinTransactionBuilder(
|
|||
|
||||
val change: BigDecimal = calculateChange(transactionData, unspentOutputs!!)
|
||||
|
||||
networkParameters = if (testNet) {
|
||||
NetworkParameters.fromID(NetworkParameters.ID_TESTNET)
|
||||
} else {
|
||||
NetworkParameters.fromID(NetworkParameters.ID_MAINNET)
|
||||
}
|
||||
transaction = transactionData.toBitcoinJTransaction(networkParameters, unspentOutputs!!, change)
|
||||
|
||||
val hashesForSign: MutableList<ByteArray> = MutableList(transaction.inputs.size) { byteArrayOf() }
|
||||
|
|
|
|||
|
|
@ -74,11 +74,16 @@ open class BitcoinWalletManager(
|
|||
val minFee = feeResult.data.minimalPerKb.calculateFee(transactionSize)
|
||||
val normalFee = feeResult.data.normalPerKb.calculateFee(transactionSize)
|
||||
val priorityFee = feeResult.data.priorityPerKb.calculateFee(transactionSize)
|
||||
return Result.Success(
|
||||
listOf(Amount(minFee, blockchain),
|
||||
Amount(normalFee, blockchain),
|
||||
Amount(priorityFee, blockchain))
|
||||
val fees = listOf(Amount(minFee, blockchain),
|
||||
Amount(normalFee, blockchain),
|
||||
Amount(priorityFee, blockchain)
|
||||
)
|
||||
|
||||
val minimalFee = transactionSize.movePointLeft(blockchain.decimals())
|
||||
for (fee in fees) {
|
||||
if (fee.value!! < minimalFee) fee.value = minimalFee
|
||||
}
|
||||
return Result.Success(fees)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,29 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoin.network
|
||||
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockchainInfoApi
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockcypherApi
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.api.EstimatefeeApi
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo.BlockchainInfoApi
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo.BlockchainInfoProvider
|
||||
import com.tangem.blockchain.network.blockcypher.BlockcypherApi
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo.EstimatefeeApi
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.blockchain.network.API_BLOCKCHAIN_INFO
|
||||
import com.tangem.blockchain.network.API_BLOCKCYPHER
|
||||
import com.tangem.blockchain.network.API_ESTIMATEFEE
|
||||
import com.tangem.blockchain.network.blockcypher.BlockcypherProvider
|
||||
import com.tangem.blockchain.network.createRetrofitInstance
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
||||
class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinProvider {
|
||||
class BitcoinNetworkManager(blockchain: Blockchain) : BitcoinProvider {
|
||||
|
||||
private val blockcypherProvider by lazy {
|
||||
val api = createRetrofitInstance(API_BLOCKCYPHER)
|
||||
.create(BlockcypherApi::class.java)
|
||||
BlockcypherProvider(api, isTestNet)
|
||||
BlockcypherProvider(api, blockchain)
|
||||
}
|
||||
|
||||
private val blockchainInfoProvider by lazy {
|
||||
|
|
@ -31,10 +34,10 @@ class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinPro
|
|||
BlockchainInfoProvider(api, estimateFeeApi)
|
||||
}
|
||||
|
||||
private var bitcoinProvider: BitcoinProvider = blockchainInfoProvider
|
||||
private var provider: BitcoinProvider = blockchainInfoProvider
|
||||
|
||||
private fun changeProvider() {
|
||||
bitcoinProvider = if (bitcoinProvider == blockchainInfoProvider) {
|
||||
provider = if (provider == blockchainInfoProvider) {
|
||||
blockcypherProvider
|
||||
} else {
|
||||
blockchainInfoProvider
|
||||
|
|
@ -42,13 +45,13 @@ class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinPro
|
|||
}
|
||||
|
||||
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
|
||||
val result = bitcoinProvider.getInfo(address)
|
||||
val result = provider.getInfo(address)
|
||||
when (result) {
|
||||
is Result.Success -> return result
|
||||
is Result.Failure -> {
|
||||
if (result.error is IOException || result.error is HttpException) {
|
||||
changeProvider()
|
||||
return bitcoinProvider.getInfo(address)
|
||||
return provider.getInfo(address)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
|
|
@ -57,13 +60,13 @@ class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinPro
|
|||
}
|
||||
|
||||
override suspend fun getFee(): Result<BitcoinFee> {
|
||||
val result = bitcoinProvider.getFee()
|
||||
val result = provider.getFee()
|
||||
when (result) {
|
||||
is Result.Success -> return result
|
||||
is Result.Failure -> {
|
||||
if (result.error is IOException || result.error is HttpException) {
|
||||
changeProvider()
|
||||
return bitcoinProvider.getFee()
|
||||
return provider.getFee()
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
|
|
@ -72,13 +75,13 @@ class BitcoinNetworkManager(private val isTestNet: Boolean = false) : BitcoinPro
|
|||
}
|
||||
|
||||
override suspend fun sendTransaction(transaction: String): SimpleResult {
|
||||
val result = bitcoinProvider.sendTransaction(transaction)
|
||||
val result = provider.sendTransaction(transaction)
|
||||
when (result) {
|
||||
is SimpleResult.Success -> return result
|
||||
is SimpleResult.Failure -> {
|
||||
if (result.error is IOException || result.error is HttpException) {
|
||||
changeProvider()
|
||||
return bitcoinProvider.sendTransaction(transaction)
|
||||
return provider.sendTransaction(transaction)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoin.network.api
|
||||
package com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo
|
||||
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockchainInfoAddress
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockchainInfoUnspents
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.http.*
|
||||
|
||||
|
|
@ -1,8 +1,9 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoin.network
|
||||
package com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo
|
||||
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockchainInfoApi
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.api.EstimatefeeApi
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinAddressResponse
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinFee
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinProvider
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoin.network.response
|
||||
package com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoin.network.api
|
||||
package com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo
|
||||
|
||||
import retrofit2.http.GET
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ import com.tangem.blockchain.network.blockchair.BlockchairProvider
|
|||
import com.tangem.blockchain.network.createRetrofitInstance
|
||||
|
||||
class BitcoinCashNetworkManager : BitcoinProvider {
|
||||
private val blockchain: Blockchain = Blockchain.BitcoinCash
|
||||
private val blockchain = Blockchain.BitcoinCash
|
||||
|
||||
private val blockchairProvider by lazy {
|
||||
val api = createRetrofitInstance(API_BLOCKCHAIR).create(BlockchairApi::class.java)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.blockchain.blockchains.bitcoincash
|
|||
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinTransactionBuilder
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.common.extensions.isZero
|
||||
|
|
@ -13,8 +14,8 @@ import org.bitcoinj.script.ScriptBuilder
|
|||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
|
||||
class BitcoinCashTransactionBuilder(private val walletPublicKey: ByteArray)
|
||||
: BitcoinTransactionBuilder(walletPublicKey) {
|
||||
class BitcoinCashTransactionBuilder(private val walletPublicKey: ByteArray, blockchain: Blockchain)
|
||||
: BitcoinTransactionBuilder(walletPublicKey, blockchain) {
|
||||
|
||||
private lateinit var transaction: BitcoinCashTransaction
|
||||
|
||||
|
|
@ -25,7 +26,6 @@ class BitcoinCashTransactionBuilder(private val walletPublicKey: ByteArray)
|
|||
|
||||
val change: BigDecimal = calculateChange(transactionData, unspentOutputs!!)
|
||||
|
||||
networkParameters = NetworkParameters.fromID(NetworkParameters.ID_MAINNET)
|
||||
transaction = transactionData.toBitcoinCashTransaction(networkParameters, unspentOutputs!!, change)
|
||||
|
||||
val hashesForSign: MutableList<ByteArray> = MutableList(transaction.inputs.size) { byteArrayOf() }
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import java.math.BigDecimal
|
|||
class BitcoinCashWalletManager(
|
||||
cardId: String,
|
||||
wallet: Wallet,
|
||||
private val transactionBuilder: BitcoinCashTransactionBuilder,
|
||||
private val networkManager: BitcoinCashNetworkManager
|
||||
transactionBuilder: BitcoinCashTransactionBuilder,
|
||||
networkManager: BitcoinCashNetworkManager
|
||||
) : BitcoinWalletManager(cardId, wallet, transactionBuilder, networkManager), TransactionSender {
|
||||
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {
|
||||
val minimalFee = BigDecimal("0.00001")
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class EthereumTransactionBuilder(private val walletPublicKey: ByteArray, blockch
|
|||
private val chainId = when (blockchain) {
|
||||
Blockchain.Ethereum -> Chain.Mainnet.id
|
||||
Blockchain.RSK -> Chain.RskMainnet.id
|
||||
else -> throw Exception("${blockchain.fullName} blockchain is not supported by EthereumTransactionBuilder")
|
||||
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
|
||||
}
|
||||
|
||||
fun buildToSign(transactionData: TransactionData, nonce: BigInteger?): TransactionToSign? {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class EthereumNetworkManager(blockchain: Blockchain) {
|
|||
val baseUrl = when (blockchain) {
|
||||
Blockchain.Ethereum -> API_INFURA + infuraPath
|
||||
Blockchain.RSK -> API_RSK
|
||||
else -> throw Exception("${blockchain.fullName} blockchain is not supported by EthereumNetworkManager")
|
||||
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
|
||||
}
|
||||
createRetrofitInstance(baseUrl).create(EthereumApi::class.java)
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ class EthereumNetworkManager(blockchain: Blockchain) {
|
|||
private val apiKey = when (blockchain) {
|
||||
Blockchain.Ethereum -> INFURA_API_KEY
|
||||
Blockchain.RSK -> ""
|
||||
else -> throw Exception("${blockchain.fullName} blockchain is not supported by EthereumNetworkManager")
|
||||
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
|
||||
}
|
||||
|
||||
private val provider: EthereumProvider by lazy { EthereumProvider(api, apiKey) }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
package com.tangem.blockchain.blockchains.litecoin;
|
||||
|
||||
import org.bitcoinj.core.Utils;
|
||||
import org.bitcoinj.params.AbstractBitcoinNetParams;
|
||||
import org.bitcoinj.params.MainNetParams;
|
||||
import org.spongycastle.util.encoders.Hex;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
|
||||
public class LitecoinMainNetParams extends AbstractBitcoinNetParams {
|
||||
public static final int MAINNET_MAJORITY_WINDOW = MainNetParams.MAINNET_MAJORITY_WINDOW;
|
||||
public static final int MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED = MainNetParams.MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED;
|
||||
public static final int MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE = MainNetParams.MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE;
|
||||
|
||||
public LitecoinMainNetParams() {
|
||||
super();
|
||||
id = "org.bitcoinj.litecoin_mainnet";
|
||||
// Genesis hash is 12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2
|
||||
packetMagic = 0xfbc0b6db;
|
||||
|
||||
maxTarget = Utils.decodeCompactBits(0x1e0fffffL);
|
||||
port = 9333;
|
||||
addressHeader = 48;
|
||||
p2shHeader = 50;
|
||||
segwitAddressHrp = "ltc";
|
||||
dumpedPrivateKeyHeader = 176;
|
||||
|
||||
spendableCoinbaseDepth = 100;
|
||||
subsidyDecreaseBlockCount = 840000;
|
||||
|
||||
genesisBlock.setTime(1317972665L);
|
||||
genesisBlock.setDifficultyTarget(0x1e0ffff0L);
|
||||
genesisBlock.setNonce(2084524493);
|
||||
|
||||
String genesisHash = genesisBlock.getHashAsString();
|
||||
checkState(genesisHash.equals("5155a7ed2219a75c0735c58b5d459c6d07d97917570e27b9d1d4546fb8431381"));
|
||||
alertSigningKey = Hex.decode("040184710fa689ad5023690c80f3a49c8f13f8d45b8c857fbcbc8bc4a8e4d3eb4b10f4d4604fa08dce601aaf0f470216fe1b51850b4acf21b179c45070ac7b03a9");
|
||||
|
||||
majorityEnforceBlockUpgrade = MAINNET_MAJORITY_ENFORCE_BLOCK_UPGRADE;
|
||||
majorityRejectBlockOutdated = MAINNET_MAJORITY_REJECT_BLOCK_OUTDATED;
|
||||
majorityWindow = MAINNET_MAJORITY_WINDOW;
|
||||
|
||||
dnsSeeds = new String[]{
|
||||
"dnsseed.litecointools.com",
|
||||
"dnsseed.litecoinpool.org",
|
||||
"dnsseed.ltc.xurious.com",
|
||||
"dnsseed.koin-project.com",
|
||||
"dnsseed.weminemnc.com"
|
||||
};
|
||||
bip32HeaderP2PKHpub = 0x0488B21E;
|
||||
bip32HeaderP2PKHpriv = 0x0488ADE4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPaymentProtocolId() {
|
||||
return PAYMENT_PROTOCOL_ID_MAINNET;
|
||||
}
|
||||
|
||||
|
||||
private static LitecoinMainNetParams instance;
|
||||
|
||||
public static synchronized LitecoinMainNetParams get() {
|
||||
if (instance == null) {
|
||||
instance = new LitecoinMainNetParams();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.blockchain.blockchains.litecoin
|
||||
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinAddressResponse
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinFee
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinProvider
|
||||
import com.tangem.blockchain.network.blockcypher.BlockcypherProvider
|
||||
import com.tangem.blockchain.network.blockcypher.BlockcypherApi
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.blockchain.network.API_BLOCKCHAIR
|
||||
import com.tangem.blockchain.network.API_BLOCKCYPHER
|
||||
import com.tangem.blockchain.network.blockchair.BlockchairApi
|
||||
import com.tangem.blockchain.network.blockchair.BlockchairProvider
|
||||
import com.tangem.blockchain.network.createRetrofitInstance
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
class LitecoinNetworkManager : BitcoinProvider {
|
||||
private val blockchain = Blockchain.Litecoin
|
||||
|
||||
private val blockchairProvider by lazy {
|
||||
val api = createRetrofitInstance(API_BLOCKCHAIR)
|
||||
.create(BlockchairApi::class.java)
|
||||
BlockchairProvider(api, blockchain)
|
||||
}
|
||||
|
||||
private val blockcypherProvider by lazy {
|
||||
val api = createRetrofitInstance(API_BLOCKCYPHER)
|
||||
.create(BlockcypherApi::class.java)
|
||||
BlockcypherProvider(api, blockchain)
|
||||
}
|
||||
|
||||
private var provider: BitcoinProvider = blockchairProvider
|
||||
|
||||
private fun changeProvider() {
|
||||
provider = if (provider == blockchairProvider) blockcypherProvider else blockchairProvider
|
||||
}
|
||||
|
||||
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
|
||||
val result = provider.getInfo(address)
|
||||
when (result) {
|
||||
is Result.Success -> return result
|
||||
is Result.Failure -> {
|
||||
if (result.error is IOException || result.error is HttpException) {
|
||||
changeProvider()
|
||||
return provider.getInfo(address)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getFee(): Result<BitcoinFee> {
|
||||
val result = provider.getFee()
|
||||
when (result) {
|
||||
is Result.Success -> return result
|
||||
is Result.Failure -> {
|
||||
if (result.error is IOException || result.error is HttpException) {
|
||||
changeProvider()
|
||||
return provider.getFee()
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendTransaction(transaction: String): SimpleResult {
|
||||
val result = provider.sendTransaction(transaction)
|
||||
when (result) {
|
||||
is SimpleResult.Success -> return result
|
||||
is SimpleResult.Failure -> {
|
||||
if (result.error is IOException || result.error is HttpException) {
|
||||
changeProvider()
|
||||
return provider.sendTransaction(transaction)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.tangem.blockchain.blockchains.litecoin
|
||||
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinTransactionBuilder
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager
|
||||
import com.tangem.blockchain.common.Amount
|
||||
import com.tangem.blockchain.common.TransactionSender
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import java.math.BigDecimal
|
||||
|
||||
class LitecoinWalletManager(
|
||||
cardId: String,
|
||||
wallet: Wallet,
|
||||
transactionBuilder: BitcoinTransactionBuilder,
|
||||
networkManager: LitecoinNetworkManager
|
||||
) : BitcoinWalletManager(cardId, wallet, transactionBuilder, networkManager), TransactionSender {
|
||||
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {
|
||||
val minimalFee = BigDecimal("0.00001")
|
||||
when (val result = super.getFee(amount, destination)) {
|
||||
is Result.Success -> {
|
||||
for (fee in result.data) {
|
||||
if (fee.value!! < minimalFee) fee.value = minimalFee
|
||||
}
|
||||
return result
|
||||
}
|
||||
is Result.Failure -> return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.blockchain.blockchains.tezos
|
||||
|
||||
import com.tangem.blockchain.common.AddressService
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import org.bitcoinj.core.Base58
|
||||
import org.spongycastle.jcajce.provider.digest.Blake2b
|
||||
|
||||
class TezosAddressService : AddressService {
|
||||
override fun makeAddress(walletPublicKey: ByteArray): String {
|
||||
val publicKeyHash = Blake2b.Blake2b160().digest(walletPublicKey)
|
||||
|
||||
val tz1Prefix = "06A19F".hexToBytes()
|
||||
val prefixedHash = tz1Prefix + publicKeyHash
|
||||
|
||||
val checksum = prefixedHash.calculateTezosChecksum()
|
||||
val prefixedHashWithChecksum = prefixedHash + checksum
|
||||
|
||||
return Base58.encode(prefixedHashWithChecksum)
|
||||
}
|
||||
|
||||
override fun validate(address: String): Boolean {
|
||||
val prefixedHashWithChecksum = Base58.decode(address)
|
||||
if (prefixedHashWithChecksum == null || prefixedHashWithChecksum.size != 27) return false
|
||||
|
||||
val prefixedHash = prefixedHashWithChecksum.copyOf(23)
|
||||
val checksum = prefixedHashWithChecksum.copyOfRange(23, 27)
|
||||
|
||||
val calculatedChecksum = prefixedHash.calculateTezosChecksum()
|
||||
|
||||
return calculatedChecksum.contentEquals(checksum)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun ByteArray.calculateTezosChecksum() = this.calculateSha256().calculateSha256().copyOfRange(0, 4)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.tangem.blockchain.blockchains.tezos
|
||||
|
||||
import com.tangem.blockchain.blockchains.tezos.TezosAddressService.Companion.calculateTezosChecksum
|
||||
import com.tangem.blockchain.blockchains.tezos.network.TezosOperationContent
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.bigIntegerValue
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import org.bitcoinj.core.Base58
|
||||
import org.spongycastle.jcajce.provider.digest.Blake2b
|
||||
|
||||
class TezosTransactionBuilder(private val walletPublicKey: ByteArray) {
|
||||
var counter: Long? = null
|
||||
|
||||
fun buildContents(transactionData: TransactionData,
|
||||
publicKeyRevealed: Boolean
|
||||
): Result<List<TezosOperationContent>> {
|
||||
if (counter == null) return Result.Failure(Exception("counter is null"))
|
||||
var counter = counter!!
|
||||
|
||||
val contents = arrayListOf<TezosOperationContent>()
|
||||
|
||||
if (!publicKeyRevealed) {
|
||||
counter++
|
||||
val revealOp = TezosOperationContent(
|
||||
kind = "reveal",
|
||||
source = transactionData.sourceAddress,
|
||||
fee = "1300",
|
||||
counter = counter.toString(),
|
||||
gas_limit = "10000",
|
||||
storage_limit = "0",
|
||||
public_key = encodePublicKey(walletPublicKey)
|
||||
)
|
||||
contents.add(revealOp)
|
||||
}
|
||||
|
||||
counter++
|
||||
val transactionOp = TezosOperationContent(
|
||||
kind = "transaction",
|
||||
source = transactionData.sourceAddress,
|
||||
fee = "1350",
|
||||
counter = counter.toString(),
|
||||
gas_limit = "10600",
|
||||
storage_limit = "277",
|
||||
destination = transactionData.destinationAddress,
|
||||
amount = transactionData.amount.bigIntegerValue().toString()
|
||||
)
|
||||
contents.add(transactionOp)
|
||||
|
||||
return Result.Success(contents)
|
||||
}
|
||||
|
||||
fun buildToSign(forgedContents: String): ByteArray {
|
||||
val genericOperationWatermark = "03"
|
||||
return Blake2b.Blake2b256().digest((genericOperationWatermark + forgedContents).hexToBytes())
|
||||
}
|
||||
|
||||
fun buildToSend(signature: ByteArray, forgedContents: String) = forgedContents + signature.toHexString()
|
||||
|
||||
private fun encodePublicKey(pkUncompressed: ByteArray): String {
|
||||
val edpkPrefix = "0D0F25D9".hexToBytes()
|
||||
val prefixedPubKey = edpkPrefix + pkUncompressed
|
||||
|
||||
val checksum = prefixedPubKey.calculateTezosChecksum()
|
||||
val prefixedHashWithChecksum = prefixedPubKey + checksum
|
||||
|
||||
return Base58.encode(prefixedHashWithChecksum)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
package com.tangem.blockchain.blockchains.tezos
|
||||
|
||||
import android.util.Log
|
||||
import com.tangem.blockchain.blockchains.tezos.network.TezosInfoResponse
|
||||
import com.tangem.blockchain.blockchains.tezos.network.TezosNetworkManager
|
||||
import com.tangem.blockchain.common.*
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import java.math.BigDecimal
|
||||
|
||||
class TezosWalletManager(
|
||||
cardId: String,
|
||||
wallet: Wallet,
|
||||
private val transactionBuilder: TezosTransactionBuilder,
|
||||
private val networkManager: TezosNetworkManager
|
||||
) : WalletManager(cardId, wallet), TransactionSender {
|
||||
|
||||
private val blockchain = wallet.blockchain
|
||||
private var publicKeyRevealed: Boolean? = null
|
||||
|
||||
override suspend fun update() {
|
||||
val response = networkManager.getInfo(wallet.address)
|
||||
when (response) {
|
||||
is Result.Success -> updateWallet(response.data)
|
||||
is Result.Failure -> updateError(response.error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWallet(response: TezosInfoResponse) {
|
||||
Log.d(this::class.java.simpleName, "Balance is ${response.balance}")
|
||||
wallet.amounts[AmountType.Coin]?.value = response.balance
|
||||
transactionBuilder.counter = response.counter
|
||||
}
|
||||
|
||||
private fun updateError(error: Throwable?) {
|
||||
Log.e(this::class.java.simpleName, error?.message ?: "")
|
||||
if (error != null) throw error
|
||||
}
|
||||
|
||||
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
|
||||
if (publicKeyRevealed == null) return SimpleResult.Failure(Exception("publicKeyRevealed is null"))
|
||||
|
||||
val contents =
|
||||
when (val response = transactionBuilder.buildContents(transactionData, publicKeyRevealed!!)) {
|
||||
is Result.Failure -> return SimpleResult.Failure(response.error)
|
||||
is Result.Success -> response.data
|
||||
}
|
||||
val header =
|
||||
when (val response = networkManager.getHeader()) {
|
||||
is Result.Failure -> return SimpleResult.Failure(response.error)
|
||||
is Result.Success -> response.data
|
||||
}
|
||||
val forgedContents = //TODO: CHANGE FOR PRODUCTION, this is potential security vulnerability, transaction should be forged locally
|
||||
when (val response = networkManager.forgeContents(header.hash, contents)) {
|
||||
is Result.Failure -> return SimpleResult.Failure(response.error)
|
||||
is Result.Success -> response.data
|
||||
}
|
||||
val dataToSign = transactionBuilder.buildToSign(forgedContents)
|
||||
|
||||
val signature = when (val signerResponse = signer.sign(arrayOf(dataToSign), cardId)) {
|
||||
is CompletionResult.Failure -> return SimpleResult.Failure(signerResponse.error)
|
||||
is CompletionResult.Success -> signerResponse.data.signature
|
||||
}
|
||||
|
||||
when (val response = networkManager.checkTransaction(header, contents, signature)) {
|
||||
is SimpleResult.Failure -> return response
|
||||
is SimpleResult.Success -> {
|
||||
val transactionToSend = transactionBuilder.buildToSend(signature,forgedContents)
|
||||
return networkManager.sendTransaction(transactionToSend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {
|
||||
var fee: BigDecimal = BigDecimal.valueOf(0.00135)
|
||||
var error: Result.Failure? = null
|
||||
|
||||
coroutineScope {
|
||||
val publicKeyRevealedDeferred = async { networkManager.isPublicKeyRevealed(wallet.address) }
|
||||
val destinationInfoDeferred = async { networkManager.getInfo(destination) }
|
||||
|
||||
when (val result = publicKeyRevealedDeferred.await()) {
|
||||
is Result.Failure -> error = result
|
||||
is Result.Success -> {
|
||||
publicKeyRevealed = result.data
|
||||
if (!publicKeyRevealed!!) {
|
||||
fee += BigDecimal.valueOf(0.0013)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
when (val result = destinationInfoDeferred.await()) {
|
||||
is Result.Failure -> error = result
|
||||
is Result.Success -> {
|
||||
if (result.data.balance.isZero()) {
|
||||
fee += BigDecimal.valueOf(0.257)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (error == null) Result.Success(listOf(Amount(fee, blockchain))) else error!!
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.blockchain.blockchains.tezos.network
|
||||
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface TezosApi {
|
||||
@GET("chains/main/blocks/head/context/contracts/{address}")
|
||||
suspend fun getAddressData(@Path("address") address: String): TezosAddressResponse
|
||||
|
||||
@GET("chains/main/blocks/head/header")
|
||||
suspend fun getHeader(): TezosHeaderResponse
|
||||
|
||||
@GET("chains/main/blocks/head/context/contracts/{address}/manager_key")
|
||||
suspend fun getManagerKey(@Path("address") address: String): String
|
||||
|
||||
@POST("chains/main/blocks/head/helpers/forge/operations")
|
||||
suspend fun forgeOperations(@Body tezosForgeBody: TezosForgeBody): String
|
||||
|
||||
@POST("chains/main/blocks/head/helpers/preapply/operations")
|
||||
suspend fun preapplyOperations(@Body tezosPreapplyBodyList: List<TezosPreapplyBody>)
|
||||
|
||||
@POST("injection/operation")
|
||||
suspend fun sendTransaction(@Body transaction: String)
|
||||
}
|
||||
|
||||
data class TezosForgeBody(
|
||||
val branch: String,
|
||||
val contents: List<TezosOperationContent>
|
||||
)
|
||||
|
||||
data class TezosOperationContent(
|
||||
val kind: String,
|
||||
val source: String,
|
||||
val fee: String,
|
||||
val counter: String,
|
||||
val gas_limit: String,
|
||||
val storage_limit: String,
|
||||
val public_key: String? = null,
|
||||
val destination: String? = null,
|
||||
val amount: String? = null
|
||||
)
|
||||
|
||||
data class TezosPreapplyBody(
|
||||
val protocol: String,
|
||||
val branch: String,
|
||||
val contents: List<TezosOperationContent>,
|
||||
val signature: String
|
||||
)
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
package com.tangem.blockchain.blockchains.tezos.network
|
||||
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.blockchain.network.API_TEZOS
|
||||
import com.tangem.blockchain.network.API_TEZOS_RESERVE
|
||||
import com.tangem.blockchain.network.createRetrofitInstance
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.math.BigDecimal
|
||||
|
||||
class TezosNetworkManager {
|
||||
private val tezosProvider by lazy {
|
||||
val api = createRetrofitInstance(API_TEZOS)
|
||||
.create(TezosApi::class.java)
|
||||
TezosProvider(api)
|
||||
}
|
||||
|
||||
private val tezosReserveProvider by lazy {
|
||||
val api = createRetrofitInstance(API_TEZOS_RESERVE)
|
||||
.create(TezosApi::class.java)
|
||||
TezosProvider(api)
|
||||
}
|
||||
|
||||
var provider = tezosProvider
|
||||
|
||||
private fun changeProvider() {
|
||||
provider = if (provider == tezosProvider) tezosReserveProvider else tezosProvider
|
||||
}
|
||||
|
||||
suspend fun getInfo(address: String): Result<TezosInfoResponse> {
|
||||
val result = provider.getInfo(address)
|
||||
when (result) {
|
||||
is Result.Success -> return result
|
||||
is Result.Failure -> {
|
||||
if (result.error is IOException || result.error is HttpException) {
|
||||
changeProvider()
|
||||
return provider.getInfo(address)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun isPublicKeyRevealed(address: String): Result<Boolean> {
|
||||
val result = provider.isPublicKeyRevealed(address)
|
||||
when (result) {
|
||||
is Result.Success -> return result
|
||||
is Result.Failure -> {
|
||||
if (result.error is IOException || result.error is HttpException) {
|
||||
changeProvider()
|
||||
return provider.isPublicKeyRevealed(address)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getHeader(): Result<TezosHeader> {
|
||||
val result = provider.getHeader()
|
||||
when (result) {
|
||||
is Result.Success -> return result
|
||||
is Result.Failure -> {
|
||||
if (result.error is IOException || result.error is HttpException) {
|
||||
changeProvider()
|
||||
return provider.getHeader()
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun forgeContents(headerHash: String, contents: List<TezosOperationContent>): Result<String> {
|
||||
val result = provider.forgeContents(headerHash, contents)
|
||||
when (result) {
|
||||
is Result.Success -> return result
|
||||
is Result.Failure -> {
|
||||
if (result.error is IOException || result.error is HttpException) {
|
||||
changeProvider()
|
||||
return provider.forgeContents(headerHash, contents)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkTransaction(
|
||||
header: TezosHeader,
|
||||
contents: List<TezosOperationContent>,
|
||||
signature: ByteArray
|
||||
): SimpleResult {
|
||||
val result = provider.checkTransaction(header, contents, signature)
|
||||
when (result) {
|
||||
is SimpleResult.Success -> return result
|
||||
is SimpleResult.Failure -> {
|
||||
if (result.error is IOException || result.error is HttpException) {
|
||||
changeProvider()
|
||||
return provider.checkTransaction(header, contents, signature)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendTransaction(transaction: String): SimpleResult {
|
||||
val result = provider.sendTransaction(transaction)
|
||||
when (result) {
|
||||
is SimpleResult.Success -> return result
|
||||
is SimpleResult.Failure -> {
|
||||
if (result.error is IOException || result.error is HttpException) {
|
||||
changeProvider()
|
||||
return provider.sendTransaction(transaction)
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class TezosInfoResponse(
|
||||
val balance: BigDecimal,
|
||||
val counter: Long
|
||||
)
|
||||
|
||||
data class TezosHeader(
|
||||
val hash: String,
|
||||
val protocol: String
|
||||
)
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package com.tangem.blockchain.blockchains.tezos.network
|
||||
|
||||
import com.tangem.blockchain.blockchains.tezos.TezosAddressService.Companion.calculateTezosChecksum
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.blockchain.extensions.retryIO
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import org.bitcoinj.core.Base58
|
||||
|
||||
class TezosProvider(private val api: TezosApi) {
|
||||
private val decimals = Blockchain.Tezos.decimals()
|
||||
|
||||
suspend fun getInfo(address: String): Result<TezosInfoResponse> {
|
||||
return try {
|
||||
val addressData = retryIO { api.getAddressData(address) }
|
||||
Result.Success(TezosInfoResponse(
|
||||
balance = addressData.balance!!.toBigDecimal().movePointLeft(decimals),
|
||||
counter = addressData.counter!!
|
||||
))
|
||||
} catch (exception: Exception) {
|
||||
Result.Failure(exception)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun isPublicKeyRevealed(address: String): Result<Boolean> {
|
||||
return try {
|
||||
retryIO { api.getManagerKey(address) }
|
||||
Result.Success(true)
|
||||
} catch (exception: Exception) { //TODO: check exception
|
||||
Result.Success(false)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getHeader(): Result<TezosHeader> {
|
||||
return try {
|
||||
val headerResponse = retryIO { api.getHeader() }
|
||||
Result.Success(TezosHeader(
|
||||
hash = headerResponse.hash!!,
|
||||
protocol = headerResponse.protocol!!
|
||||
))
|
||||
} catch (exception: Exception) {
|
||||
Result.Failure(exception)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun forgeContents(headerHash: String, contents: List<TezosOperationContent>): Result<String> {
|
||||
return try {
|
||||
val forgedContents = retryIO { api.forgeOperations(TezosForgeBody(headerHash, contents)) }
|
||||
Result.Success(forgedContents)
|
||||
} catch (exception: Exception) {
|
||||
Result.Failure(exception)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkTransaction(
|
||||
header: TezosHeader,
|
||||
contents: List<TezosOperationContent>,
|
||||
signature: ByteArray
|
||||
): SimpleResult {
|
||||
return try {
|
||||
val tezosPreapplyBody = TezosPreapplyBody(
|
||||
protocol = header.protocol,
|
||||
branch = header.hash,
|
||||
contents = contents,
|
||||
signature = encodeSignature(signature)
|
||||
)
|
||||
retryIO { api.preapplyOperations(listOf(tezosPreapplyBody)) }
|
||||
SimpleResult.Success
|
||||
} catch (exception: Exception) {
|
||||
SimpleResult.Failure(exception)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendTransaction(transaction: String): SimpleResult {
|
||||
return try {
|
||||
retryIO { api.sendTransaction(transaction) }
|
||||
SimpleResult.Success
|
||||
} catch (exception: Exception) {
|
||||
SimpleResult.Failure(exception)
|
||||
}
|
||||
}
|
||||
|
||||
private fun encodeSignature(signature: ByteArray): String {
|
||||
val edsigPrefix = "09F5CD8612".hexToBytes()
|
||||
val prefixedSignature = edsigPrefix + signature
|
||||
val checksum = prefixedSignature.calculateTezosChecksum()
|
||||
val prefixedSignatureWithChecksum = prefixedSignature + checksum
|
||||
|
||||
return Base58.encode(prefixedSignatureWithChecksum)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.blockchain.blockchains.tezos.network
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TezosAddressResponse(
|
||||
@Json(name = "balance")
|
||||
var balance: Long? = null,
|
||||
|
||||
@Json(name = "counter")
|
||||
var counter: Long? = null
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TezosHeaderResponse(
|
||||
@Json(name = "protocol")
|
||||
var protocol: String? = null,
|
||||
|
||||
@Json(name = "hash")
|
||||
var hash: String? = null
|
||||
)
|
||||
|
|
@ -6,6 +6,7 @@ import com.tangem.blockchain.blockchains.bitcoincash.BitcoinCashAddressService
|
|||
import com.tangem.blockchain.blockchains.cardano.CardanoAddressService
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumAddressService
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarAddressService
|
||||
import com.tangem.blockchain.blockchains.tezos.TezosAddressService
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpAddressService
|
||||
|
||||
enum class Blockchain(
|
||||
|
|
@ -17,17 +18,19 @@ enum class Blockchain(
|
|||
Bitcoin("BTC", "BTC", "Bitcoin"),
|
||||
BitcoinTestnet("BTC/test", "BTCt", "Bitcoin Testnet"),
|
||||
BitcoinCash("BCH", "BCH", "Bitcoin Cash"),
|
||||
Litecoin("LTC", "LTC", "Litecoin"),
|
||||
Ethereum("ETH", "ETH", "Ethereum"),
|
||||
RSK("RSK", "RBTC", "RSK"),
|
||||
Cardano("CARDANO", "ADA", "Cardano"),
|
||||
XRP("XRP", "XRP", "XRP Ledger"),
|
||||
Binance("BINANCE", "BNB", "Binance"),
|
||||
BinanceTestnet("BINANCE/test", "BNBt", "Binance"),
|
||||
Stellar("XLM", "XLM", "Stellar");
|
||||
Stellar("XLM", "XLM", "Stellar"),
|
||||
Tezos("TEZOS", "XTZ", "Tezos");
|
||||
|
||||
fun decimals(): Int = when (this) {
|
||||
Bitcoin, BitcoinTestnet, BitcoinCash, Binance, BinanceTestnet -> 8
|
||||
Cardano, XRP -> 6
|
||||
Bitcoin, BitcoinTestnet, BitcoinCash, Binance, BinanceTestnet, Litecoin -> 8
|
||||
Cardano, XRP, Tezos -> 6
|
||||
Ethereum, RSK -> 18
|
||||
Stellar -> 7
|
||||
Unknown -> 0
|
||||
|
|
@ -43,8 +46,7 @@ enum class Blockchain(
|
|||
|
||||
private fun getAddressService(): AddressService = when (this) {
|
||||
Unknown -> throw Exception("unsupported blockchain")
|
||||
Bitcoin -> BitcoinAddressService()
|
||||
BitcoinTestnet -> BitcoinAddressService(true)
|
||||
Bitcoin, BitcoinTestnet, Litecoin -> BitcoinAddressService(this)
|
||||
BitcoinCash -> BitcoinCashAddressService()
|
||||
Ethereum, RSK -> EthereumAddressService()
|
||||
Cardano -> CardanoAddressService()
|
||||
|
|
@ -52,6 +54,7 @@ enum class Blockchain(
|
|||
Binance -> BinanceAddressService()
|
||||
BinanceTestnet -> BinanceAddressService(true)
|
||||
Stellar -> StellarAddressService()
|
||||
Tezos -> TezosAddressService()
|
||||
}
|
||||
|
||||
fun getShareUri(address: String): String = when (this) {
|
||||
|
|
@ -63,9 +66,11 @@ enum class Blockchain(
|
|||
|
||||
fun getExploreUrl(address: String, token: Token? = null): String = when (this) {
|
||||
Binance -> "https://explorer.binance.org/address/$address"
|
||||
BinanceTestnet -> "https://testnet-explorer.binance.org/address/$address"
|
||||
Bitcoin -> "https://blockchain.info/address/$address"
|
||||
BitcoinTestnet -> "https://live.blockcypher.com/btc-testnet/address/$address"
|
||||
BitcoinCash -> "https://blockchair.com/bitcoin-cash/address/$address"
|
||||
Litecoin -> "https://live.blockcypher.com/ltc/address/$address"
|
||||
Cardano -> "https://cardanoexplorer.com/address/$address"
|
||||
Ethereum -> if (token == null) {
|
||||
"https://etherscan.io/address/"
|
||||
|
|
@ -81,7 +86,8 @@ enum class Blockchain(
|
|||
}
|
||||
Stellar -> "https://stellar.expert/explorer/public/account/$address"
|
||||
XRP -> "https://xrpscan.com/account/$address"
|
||||
else -> throw Exception("Explore URL not defined!")
|
||||
Tezos -> "https://tezblock.io/account/$address"
|
||||
Unknown -> throw Exception("unsupported blockchain")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -15,10 +15,15 @@ import com.tangem.blockchain.blockchains.cardano.network.CardanoNetworkManager
|
|||
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionBuilder
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
|
||||
import com.tangem.blockchain.blockchains.ethereum.network.EthereumNetworkManager
|
||||
import com.tangem.blockchain.blockchains.litecoin.LitecoinNetworkManager
|
||||
import com.tangem.blockchain.blockchains.litecoin.LitecoinWalletManager
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarNetworkManager
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarTransactionBuilder
|
||||
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarWalletManager
|
||||
import com.tangem.blockchain.blockchains.tezos.TezosTransactionBuilder
|
||||
import com.tangem.blockchain.blockchains.tezos.TezosWalletManager
|
||||
import com.tangem.blockchain.blockchains.tezos.network.TezosNetworkManager
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpWalletManager
|
||||
import com.tangem.blockchain.blockchains.xrp.network.XrpNetworkManager
|
||||
|
|
@ -42,24 +47,31 @@ object WalletManagerFactory {
|
|||
Blockchain.Bitcoin -> {
|
||||
return BitcoinWalletManager(
|
||||
cardId, wallet,
|
||||
BitcoinTransactionBuilder(walletPublicKey),
|
||||
BitcoinNetworkManager()
|
||||
BitcoinTransactionBuilder(walletPublicKey, blockchain),
|
||||
BitcoinNetworkManager(blockchain)
|
||||
)
|
||||
}
|
||||
Blockchain.BitcoinTestnet -> {
|
||||
return BitcoinWalletManager(
|
||||
cardId, wallet,
|
||||
BitcoinTransactionBuilder(walletPublicKey, true),
|
||||
BitcoinNetworkManager(true)
|
||||
BitcoinTransactionBuilder(walletPublicKey, blockchain),
|
||||
BitcoinNetworkManager(blockchain)
|
||||
)
|
||||
}
|
||||
Blockchain.BitcoinCash -> {
|
||||
return BitcoinCashWalletManager(
|
||||
cardId, wallet,
|
||||
BitcoinCashTransactionBuilder(walletPublicKey.toCompressedPublicKey()),
|
||||
BitcoinCashTransactionBuilder(walletPublicKey.toCompressedPublicKey(), blockchain),
|
||||
BitcoinCashNetworkManager()
|
||||
)
|
||||
}
|
||||
Blockchain.Litecoin -> {
|
||||
return LitecoinWalletManager(
|
||||
cardId, wallet,
|
||||
BitcoinTransactionBuilder(walletPublicKey, blockchain),
|
||||
LitecoinNetworkManager()
|
||||
)
|
||||
}
|
||||
Blockchain.Ethereum, Blockchain.RSK -> {
|
||||
return EthereumWalletManager(
|
||||
cardId, wallet,
|
||||
|
|
@ -104,7 +116,14 @@ object WalletManagerFactory {
|
|||
BinanceNetworkManager(true)
|
||||
)
|
||||
}
|
||||
else -> return null
|
||||
Blockchain.Tezos -> {
|
||||
return TezosWalletManager(
|
||||
cardId, wallet,
|
||||
TezosTransactionBuilder(walletPublicKey),
|
||||
TezosNetworkManager()
|
||||
)
|
||||
}
|
||||
Blockchain.Unknown -> throw Exception("unsupported blockchain")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import com.tangem.blockchain.common.Amount
|
|||
import java.math.BigInteger
|
||||
|
||||
fun Amount.bigIntegerValue(): BigInteger? {
|
||||
return this.value?.movePointRight(this.decimals.toInt())?.toBigInteger()
|
||||
return this.value?.movePointRight(this.decimals)?.toBigInteger()
|
||||
}
|
||||
|
||||
fun Amount.isAboveZero(): Boolean {
|
||||
|
|
|
|||
|
|
@ -53,4 +53,6 @@ const val API_ADALITE = "https://explorer3.adalite.io/"
|
|||
const val API_ADALITE_RESERVE = "https://nodes.southeastasia.cloudapp.azure.com/"
|
||||
const val API_RIPPLED = "https://s1.ripple.com:51234/"
|
||||
const val API_RIPPLED_RESERVE = "https://s2.ripple.com:51234/"
|
||||
const val API_BLOCKCHAIR = "https://api.blockchair.com/"
|
||||
const val API_BLOCKCHAIR = "https://api.blockchair.com/"
|
||||
const val API_TEZOS = "https://teznode.letzbake.com"
|
||||
const val API_TEZOS_RESERVE = "https://mainnet.tezrpc.me"
|
||||
|
|
@ -15,7 +15,8 @@ import java.math.RoundingMode
|
|||
class BlockchairProvider(private val api: BlockchairApi, blockchain: Blockchain) : BitcoinProvider {
|
||||
private val blockchainPath = when (blockchain) {
|
||||
Blockchain.BitcoinCash -> "bitcoin-cash"
|
||||
else -> throw Exception("${blockchain.fullName} blockchain is not supported by BlockchairProvider")
|
||||
Blockchain.Litecoin -> "litecoin"
|
||||
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
|
||||
}
|
||||
private val decimals = blockchain.decimals()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoin.network.api
|
||||
package com.tangem.blockchain.network.blockcypher
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherFee
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherResponse
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherTx
|
||||
import retrofit2.http.*
|
||||
|
||||
interface BlockcypherApi {
|
||||
|
|
@ -1,30 +1,35 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoin.network
|
||||
package com.tangem.blockchain.network.blockcypher
|
||||
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockcypherApi
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.api.BlockcypherBody
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherFee
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.response.BlockcypherResponse
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinAddressResponse
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinFee
|
||||
import com.tangem.blockchain.blockchains.bitcoin.network.BitcoinProvider
|
||||
import com.tangem.blockchain.network.blockcypher.BlockcypherApi
|
||||
import com.tangem.blockchain.network.blockcypher.BlockcypherBody
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.extensions.Result
|
||||
import com.tangem.blockchain.extensions.SimpleResult
|
||||
import com.tangem.blockchain.extensions.retryIO
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
|
||||
class BlockcypherProvider(private val api: BlockcypherApi, isTestNet: Boolean) : BitcoinProvider {
|
||||
class BlockcypherProvider(private val api: BlockcypherApi, blockchain: Blockchain) : BitcoinProvider {
|
||||
|
||||
private val blockchain = "btc"
|
||||
private val decimals = Blockchain.Bitcoin.decimals()
|
||||
|
||||
private val network = if (isTestNet) {
|
||||
BlockcypherNetwork.Test.network
|
||||
} else {
|
||||
BlockcypherNetwork.Main.network
|
||||
private val blockchainPath = when (blockchain) {
|
||||
Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> "btc"
|
||||
Blockchain.Litecoin -> "ltc"
|
||||
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
|
||||
}
|
||||
|
||||
private val network = when (blockchain) {
|
||||
Blockchain.BitcoinTestnet -> "test3"
|
||||
else -> "main"
|
||||
}
|
||||
|
||||
private val decimals = blockchain.decimals()
|
||||
|
||||
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
|
||||
try {
|
||||
val addressData: BlockcypherResponse = retryIO { api.getAddressData(blockchain, network, address) }
|
||||
val addressData: BlockcypherResponse = retryIO { api.getAddressData(blockchainPath, network, address) }
|
||||
val unspents = addressData.txrefs?.map {
|
||||
BitcoinUnspentOutput(
|
||||
it.amount!!.toBigDecimal().movePointLeft(decimals),
|
||||
|
|
@ -45,7 +50,7 @@ class BlockcypherProvider(private val api: BlockcypherApi, isTestNet: Boolean) :
|
|||
|
||||
override suspend fun getFee(): Result<BitcoinFee> {
|
||||
return try {
|
||||
val receivedFee: BlockcypherFee = retryIO { api.getFee(blockchain, network) }
|
||||
val receivedFee: BlockcypherFee = retryIO { api.getFee(blockchainPath, network) }
|
||||
Result.Success(
|
||||
BitcoinFee(receivedFee.minFeePerKb!!.toBigDecimal().movePointLeft(decimals),
|
||||
receivedFee.normalFeePerKb!!.toBigDecimal().movePointLeft(decimals),
|
||||
|
|
@ -60,7 +65,7 @@ class BlockcypherProvider(private val api: BlockcypherApi, isTestNet: Boolean) :
|
|||
return try {
|
||||
retryIO {
|
||||
api.sendTransaction(
|
||||
blockchain, network, BlockcypherBody(transaction), BlockcypherToken.getToken())
|
||||
blockchainPath, network, BlockcypherBody(transaction), BlockcypherToken.getToken())
|
||||
}
|
||||
SimpleResult.Success
|
||||
} catch (error: Exception) {
|
||||
|
|
@ -76,9 +81,4 @@ private object BlockcypherToken {
|
|||
"66a8a37c5e9d4d2c9bb191acfe7f93aa")
|
||||
|
||||
fun getToken(): String = tokens.random()
|
||||
}
|
||||
|
||||
private enum class BlockcypherNetwork(val network: String) {
|
||||
Main("main"),
|
||||
Test("test3")
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoin.network.response
|
||||
package com.tangem.blockchain.network.blockcypher
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
package com.tangem.blockchain.blockchains.bitcoin
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import org.junit.Test
|
||||
|
||||
class BitcoinAddressTest {
|
||||
|
||||
private val addressService = BitcoinAddressService()
|
||||
private val addressService = BitcoinAddressService(Blockchain.Bitcoin)
|
||||
|
||||
@Test
|
||||
fun makeAddressFromCorrectPublicKey() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
package com.tangem.blockchain.blockchains.litecoin
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressService
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import org.junit.Test
|
||||
|
||||
class LitecoinAddressTest {
|
||||
|
||||
private val addressService = BitcoinAddressService(Blockchain.Litecoin)
|
||||
|
||||
@Test
|
||||
fun makeAddressFromCorrectPublicKey() {
|
||||
val walletPublicKey = "044A76C9A70422160F515F956D0F50C71BBBA4F9862A22913817D63F0B1EF7C2FAF512E1C91B1BE827560EFE24FB1652B47337E296C778DFB1014D080CDD35EF65".hexToBytes()
|
||||
val expected = "LeweDi2SMmishGyCqQN2972qNByaSRdcfT"
|
||||
|
||||
Truth.assertThat(addressService.makeAddress(walletPublicKey))
|
||||
.isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateCorrectAddress() {
|
||||
val address = "LeweDi2SMmishGyCqQN2972qNByaSRdcfT"
|
||||
Truth.assertThat(addressService.validate(address))
|
||||
.isTrue()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package com.tangem.blockchain.blockchains.tezos
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import org.junit.Test
|
||||
|
||||
class TezosAddressTest {
|
||||
|
||||
private val addressService = TezosAddressService()
|
||||
|
||||
@Test
|
||||
fun makeAddressFromCorrectPublicKey() {
|
||||
val walletPublicKey = "98E0E504F3A5FDE704400302ABB0A2EFB0DF0F95C166C91D7F207DEDCE10CBA3".hexToBytes()
|
||||
val expected = "tz1hhRdWDAvGsgEioZ9GAp4bUVQkd9ng2MMR"
|
||||
|
||||
Truth.assertThat(addressService.makeAddress(walletPublicKey))
|
||||
.isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun validateCorrectAddress() {
|
||||
val address = "tz1hhRdWDAvGsgEioZ9GAp4bUVQkd9ng2MMR"
|
||||
Truth.assertThat(addressService.validate(address))
|
||||
.isTrue()
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,9 @@ import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager
|
|||
import com.tangem.blockchain.blockchains.bitcoincash.BitcoinCashWalletManager
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoWalletManager
|
||||
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
|
||||
import com.tangem.blockchain.blockchains.litecoin.LitecoinWalletManager
|
||||
import com.tangem.blockchain.blockchains.stellar.StellarWalletManager
|
||||
import com.tangem.blockchain.blockchains.tezos.TezosWalletManager
|
||||
import com.tangem.blockchain.blockchains.xrp.XrpWalletManager
|
||||
import com.tangem.commands.ReadCommand
|
||||
import com.tangem.common.apdu.ResponseApdu
|
||||
|
|
@ -21,7 +23,7 @@ internal class WalletManagerFactoryTest {
|
|||
val data = "0108bb00000000000304200754414e47454d00020102800a322e3432642053444b000341040876bdec26b89bd2159a668b9af3d9fe86370f318717c92b8d6c1186fb3648c32a5f9321998cc2d042901c91d40601e79a641e1cbcebe7a2358be6054e1b6e5d0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034254438640e17ceec48c5be36240c98019f95ad8b6e56acfebe60d11979c6279f715d607d76a860a137da8d109e805753f3f56b0130709f4bbf4cb9974b4c57b8469bf4b873041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b310008040000006407010009020bb8604104752a727e14bba5bd73b6714d72500f61ffd11026ad1196d2e1c54577cbeeac3d11fc68a64700f8d533f4e311964ea8fb3aa26c588295f2133868d69c3e62869362040000005c6304000000090f01009000"
|
||||
val responseApdu = ResponseApdu(data.hexToBytes())
|
||||
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card)
|
||||
|
||||
Truth.assertThat(walletManager)
|
||||
.isInstanceOf(BitcoinWalletManager::class.java)
|
||||
|
|
@ -32,7 +34,7 @@ internal class WalletManagerFactoryTest {
|
|||
val data = "0108bb00000000000536200754414e47454d00020102800a322e3432642053444b000341046c8aea0d5a850b0a608acf9a0c453c39ea86131e88bfa78800de3cfb5bf1007aeaa7b9ffc184212255758605c2461be343c0a661d73cabafa4c9c175b3f0e59a0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034554488640431b6244acfeac479becdff201a7f720a7d70a97edc4e019fb678596baf52dfe9d0e8faf08ceb4443b82d4e66815541f2dc8ec6dd3ff83eb42f06e5eab07f25f3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b3100080400000064070100090205dc60410464dddc3f356744aaecfa07427f9eb996ff537d65f20fb5be3abccf0354352a6b5f8a1942e0f8ddeea3a170eda78d060be8162ad60e94e4e91fbbdf0a7054785562040000005b6304000000090f01009000"
|
||||
val responseApdu = ResponseApdu(data.hexToBytes())
|
||||
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card)
|
||||
|
||||
Truth.assertThat(walletManager)
|
||||
.isInstanceOf(EthereumWalletManager::class.java)
|
||||
|
|
@ -43,7 +45,7 @@ internal class WalletManagerFactoryTest {
|
|||
val data = "0108bb00000000000379200754414e47454d00020102800a322e3432642053444b0003410487d7bb51b189213e3cedc3fcfa3fc047b3b71b7805b5b215e14639b3a8ebb1952c9dd5ea4354441b6ada4e8b8327674bb102ddae69df55be69643a2c916edf650a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b008403584c4d86409a4bc2baf0e5836887da21167cf33458d5249d1a610bced0e31dc053f23729ed24d715912bf89e6804669430dfe396ed83274e0031f6803e2bdb8c041fa993413041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc6020e078212d58b2b9d0edc9c936830d10081cd38b90c31778c56dfb1171027e294e62040000003863040000002c0f01009000"
|
||||
val responseApdu = ResponseApdu(data.hexToBytes())
|
||||
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card)
|
||||
|
||||
Truth.assertThat(walletManager)
|
||||
.isInstanceOf(StellarWalletManager::class.java)
|
||||
|
|
@ -54,7 +56,7 @@ internal class WalletManagerFactoryTest {
|
|||
val data = "0108bb00000000000502200754414e47454d00020102800a322e3432642053444b0003410402c1e39257d60583489da2d67d35d1cc2a1c005cc05c1021f44838edcaf25d5615cad7c9d11c2e23f5efa93e50904d33c88808d0e169060508df840992e31f4d0a04041e76310c658102ffff8a0101820407e30b0d830b54414e47454d2053444b00840743415244414e4f8640f24ef5c8c6eba0ff97560d5b013edb4a452594270db9647bd0a3543df8104dec75731d4db3ebe0fc493f2afee00195e560b51e3c41189b7c61ba7895d6434b9d3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc60208a71161cfdf1e0a85d8e7ff372aa4a01136046292aceb5f9ad7ebdb98d3f60a86204000000646304000000000f01009000"
|
||||
val responseApdu = ResponseApdu(data.hexToBytes())
|
||||
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card)
|
||||
|
||||
Truth.assertThat(walletManager)
|
||||
.isInstanceOf(CardanoWalletManager::class.java)
|
||||
|
|
@ -65,7 +67,7 @@ internal class WalletManagerFactoryTest {
|
|||
val data = "0108cb21000000002154200b534d4152542043415348000201028006322e31317200034104bdad63848f97c535da53cf8fd300d24fa33f0516d194aa78ec164a06994d00204bae243a424e316c6ec845e02d9b15eafae8c19018a926b0b7435e6e941cdadb0a0400007e210c5a81020028820407e30502830754414e47454d00840358525086400ed8734b877869722c7d0b37ffb154b9fef21c54bf2c6496feb1fb5c1fc28a2ac28e201dde84f27495fa7f08b3ca2be2fb4954bf0fe78af027d6cdc16c3eee923041048196aa4b410ac44a3b9cce18e7be226aea070acc83a9cf67540fac49af25129f6a538a28ad6341358e3c4f9963064f7e365372a651d374e5c23cdd37fd099bf2050a736563703235366b31000804000f4240070100090205dc604104d2b9fb288540d54e5b32ecaf0381cd571f97f6f1ecd036b66bb11aa52ffe9981110d883080e2e255c6b1640586f7765e6faa325d1340f49b56b83d9de56bc7ed6204000f42406304000000000f01009000"
|
||||
val responseApdu = ResponseApdu(data.hexToBytes())
|
||||
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card)
|
||||
|
||||
Truth.assertThat(walletManager)
|
||||
.isInstanceOf(XrpWalletManager::class.java)
|
||||
|
|
@ -76,7 +78,7 @@ internal class WalletManagerFactoryTest {
|
|||
val data = "0108BB00000000000015200754414E47454D00020102800A322E3432642053444B0003410446D4155890B08BE217F0B1FA7DCCB16138C24B3E825A27315D5E4BBD6CAF76A28C7902007052BC1347355A78D54BD73216C9431D555CED827B54FD9255EB3A830A04041E76310C658102FFFF8A0101820407E40410830B54414E47454D2053444B00840742494E414E4345864029F115878EDC7B0CB2A6F4A4009447DCB43BBE922D7629AEBD0C9A910AD1E3BF15AE409C4F579700951ED2FE4D775171A86CFA8E50009A05938CE210D6D4A2583041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A0070100604104E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376F62040001869F6304000000010F01009000"
|
||||
val responseApdu = ResponseApdu(data.hexToBytes())
|
||||
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card)
|
||||
|
||||
Truth.assertThat(walletManager)
|
||||
.isInstanceOf(BinanceWalletManager::class.java)
|
||||
|
|
@ -87,9 +89,31 @@ internal class WalletManagerFactoryTest {
|
|||
val data = "0108BB00000000000049200754414E47454D00020102800A322E3432642053444B00034104766A1586D164B436E5D420AED01FDAB41B2AE7EDF0C865D7AF1DA995D70AB297E5B94B761CFBB405084C21BC97C02B4A1EA9ED4F515576EAB4D83AD3A0DFAA8A0A04041E76310C618102FFFF8A0101820407E4041B830B54414E47454D2053444B00840342434886408058F0F628C2466B09ECEB13F2A8EFDD4558F5D2DBDA9BD0628EE8C8CC99A778FF0F1AECD35704B9F3518486EA5C1D20F9DFCBAA66184F4CCCD9282E2632882C3041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A0070100604104BE37CD5251C8999EDBBFC759D800EB41E4DCB718289601EB15819404E1B2F2ED90FE50C2A481D06EC790D1EF6184974EB655ABAE4BE56A6D1C9E1A17B1EFDF0262040001869A6304000000060F01009000"
|
||||
val responseApdu = ResponseApdu(data.hexToBytes())
|
||||
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card!!)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card)
|
||||
|
||||
Truth.assertThat(walletManager)
|
||||
.isInstanceOf(BitcoinCashWalletManager::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createLitecoinWalletManager() {
|
||||
val data = "0108BB00000000000023200754414E47454D00020102800A322E3432642053444B000341043539F86A40ADD04CE165764A761FD3E4D251028615D2A573B1C3AE652E60AFDBFAF02E3239E89EF2C43FA448A327557ADC5AF36376A0574570F6DBD20113514A0A04041E76310C618102FFFF8A0101820407E40414830B54414E47454D2053444B0084034C5443864004BDEAD0117544886346CB47F7CA84ABA8C34239502F23D28595A4B16CAD72F7DE506BA818B86A649C2BB945986D4574993B3B755B47CBEE31C4FB931F6748183041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A00701006041044A76C9A70422160F515F956D0F50C71BBBA4F9862A22913817D63F0B1EF7C2FAF512E1C91B1BE827560EFE24FB1652B47337E296C778DFB1014D080CDD35EF6562040001869D6304000000030F01009000"
|
||||
val responseApdu = ResponseApdu(data.hexToBytes())
|
||||
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card)
|
||||
|
||||
Truth.assertThat(walletManager)
|
||||
.isInstanceOf(LitecoinWalletManager::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createLTezosWalletManager() {
|
||||
val data = "0108BB00000000000080200754414E47454D00020102800A322E3432642053444B0003410436CFC5D0A11353AE6AFEEDC84A2D02B2635C044DEEE47F99913072B8D166D14E557230AC5FB5272F1A0E523332CCE1A744B51DB53102FF7D3FDE023DC3477C460A04041E76310C638102FFFF8A0101820407E40514830B54414E47454D2053444B00840554455A4F538640C752685B29333CFB0DB0A7347579A0AE763F2B5C4BB09FD68E0B81A06CD01EC51347001732815A3ECFFCD78DDE4E53877581B9E4914B069570629D0C40A771B93041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050865643235353139000804000186A0070100602098E0E504F3A5FDE704400302ABB0A2EFB0DF0F95C166C91D7F207DEDCE10CBA362040001869F6304000000010F01009000"
|
||||
val responseApdu = ResponseApdu(data.hexToBytes())
|
||||
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
|
||||
val walletManager = WalletManagerFactory.makeWalletManager(card)
|
||||
|
||||
Truth.assertThat(walletManager)
|
||||
.isInstanceOf(TezosWalletManager::class.java)
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
ext.versions = [
|
||||
kotlin : '1.3.72',
|
||||
build_gradle: '3.6.3',
|
||||
build_gradle: '4.0.0',
|
||||
]
|
||||
|
|
|
|||
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
|
||||
|
||||
}
|
||||
|
|
@ -284,7 +284,7 @@ public class TLV {
|
|||
return String.format("%s[%d]: %s (%s)", tag.name(), Value.length, Util.bytesToHex(Value), ProductMask.getDescription(iValue));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return String.format("%s[%d]: %s (%s)", tag.name(), Value.length, Util.bytesToHex(Value));
|
||||
return String.format("%s[%d]: %s", tag.name(), Value.length, Util.bytesToHex(Value));
|
||||
}
|
||||
} else {
|
||||
return String.format("%s[]: [[NULL]]", tag.name());
|
||||
|
|
|
|||
|
|
@ -8,11 +8,19 @@ group = "$jitpackSdk.group"
|
|||
version "$jitpackSdk.version"
|
||||
|
||||
dependencies {
|
||||
// kotlin
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
|
||||
|
||||
// crypto
|
||||
implementation "com.madgag.spongycastle:core:1.58.0.0"
|
||||
implementation "com.madgag.spongycastle:prov:1.58.0.0"
|
||||
implementation 'net.i2p.crypto:eddsa:0.3.0'
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
|
||||
|
||||
// misc
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
|
||||
// tests
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
|
||||
testImplementation "com.google.truth:truth:1.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import com.tangem.crypto.pbkdf2Hash
|
|||
*/
|
||||
interface CardSessionRunnable<T : CommandResponse> {
|
||||
|
||||
val performPreflightRead: Boolean
|
||||
|
||||
/**
|
||||
* The starting point for custom business logic.
|
||||
* Implement this interface and use [TangemSdk.startSessionWithRunnable] to run.
|
||||
|
|
@ -55,6 +57,8 @@ class CardSession(
|
|||
*/
|
||||
private var isBusy = false
|
||||
|
||||
private var performPreflightRead = true
|
||||
|
||||
/**
|
||||
* This metod starts a card session, performs preflight [ReadCommand],
|
||||
* invokes [CardSessionRunnable.run] and closes the session.
|
||||
|
|
@ -64,6 +68,8 @@ class CardSession(
|
|||
fun <T : CardSessionRunnable<R>, R : CommandResponse> startWithRunnable(
|
||||
runnable: T, callback: (result: CompletionResult<R>) -> Unit) {
|
||||
|
||||
performPreflightRead = runnable.performPreflightRead
|
||||
|
||||
start { session, error ->
|
||||
if (error != null) {
|
||||
callback(CompletionResult.Failure(error))
|
||||
|
|
@ -104,6 +110,11 @@ class CardSession(
|
|||
callback(this, error)
|
||||
}
|
||||
|
||||
if (!performPreflightRead) {
|
||||
callback(this, null)
|
||||
return
|
||||
}
|
||||
|
||||
preflightRead() { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Failure -> {
|
||||
|
|
@ -174,7 +185,7 @@ class CardSession(
|
|||
* Stops the current session on error.
|
||||
* @param error An error that will be shown.
|
||||
*/
|
||||
private fun stopWithError(error: Exception) {
|
||||
private fun stopWithError(error: TangemSdkError) {
|
||||
if (!isBusy) return
|
||||
|
||||
reader.closeSession()
|
||||
|
|
@ -187,7 +198,7 @@ class CardSession(
|
|||
}
|
||||
if (error !is TangemSdkError.UserCancelled) {
|
||||
Log.e(tag, "Finishing with error: $errorMessage")
|
||||
viewDelegate.onError(errorMessage)
|
||||
viewDelegate.onError(error)
|
||||
} else {
|
||||
Log.i(tag, "User cancelled NFC session")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ interface SessionViewDelegate {
|
|||
/**
|
||||
* It is called when some error occur during NFC session.
|
||||
*/
|
||||
fun onError(errorMessage: String)
|
||||
fun onError(error: TangemSdkError)
|
||||
|
||||
/**
|
||||
* It is called when a user is expected to enter pin code.
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class TangemSdk(
|
|||
* in the form of [SignResponse] if the task was performed successfully
|
||||
* or [TangemSdkError] in case of an error.
|
||||
*/
|
||||
fun sign(hashes: Array<ByteArray>, cardId: String, initialMessage: Message? = null,
|
||||
fun sign(hashes: Array<ByteArray>, cardId: String? = null, initialMessage: Message? = null,
|
||||
callback: (result: CompletionResult<SignResponse>) -> Unit) {
|
||||
startSessionWithRunnable(SignCommand(hashes), cardId, initialMessage, callback)
|
||||
}
|
||||
|
|
@ -88,7 +88,7 @@ class TangemSdk(
|
|||
* card response in the form of [ReadIssuerDataResponse] if the task was performed successfully
|
||||
* or [TangemSdkError] in case of an error.
|
||||
*/
|
||||
fun readIssuerData(cardId: String, initialMessage: Message? = null,
|
||||
fun readIssuerData(cardId: String? = null, initialMessage: Message? = null,
|
||||
callback: (result: CompletionResult<ReadIssuerDataResponse>) -> Unit) {
|
||||
startSessionWithRunnable(ReadIssuerDataCommand(config.issuerPublicKey), cardId, initialMessage, callback)
|
||||
}
|
||||
|
|
@ -107,7 +107,7 @@ class TangemSdk(
|
|||
* card response in the form of [ReadIssuerExtraDataResponse] if the task was performed successfully
|
||||
* or [TangemSdkError] in case of an error.
|
||||
*/
|
||||
fun readIssuerExtraData(cardId: String,
|
||||
fun readIssuerExtraData(cardId: String? = null,
|
||||
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit) {
|
||||
startSessionWithRunnable(ReadIssuerExtraDataCommand(config.issuerPublicKey), cardId, null, callback)
|
||||
}
|
||||
|
|
@ -128,7 +128,7 @@ class TangemSdk(
|
|||
* card response in the form of [WriteIssuerDataResponse] if the task was performed successfully
|
||||
* or [TangemSdkError] in case of an error.
|
||||
*/
|
||||
fun writeIssuerData(cardId: String,
|
||||
fun writeIssuerData(cardId: String? = null,
|
||||
issuerData: ByteArray,
|
||||
issuerDataSignature: ByteArray,
|
||||
issuerDataCounter: Int? = null,
|
||||
|
|
@ -166,7 +166,7 @@ class TangemSdk(
|
|||
* card response in the form of [WriteIssuerDataResponse] if the task was performed successfully
|
||||
* or [TangemSdkError] in case of an error.
|
||||
*/
|
||||
fun writeIssuerExtraData(cardId: String,
|
||||
fun writeIssuerExtraData(cardId: String? = null,
|
||||
issuerData: ByteArray,
|
||||
startingSignature: ByteArray,
|
||||
finalizingSignature: ByteArray,
|
||||
|
|
@ -195,7 +195,7 @@ class TangemSdk(
|
|||
* Writing of UserCounter and UserData is protected only by PIN1.
|
||||
*/
|
||||
fun writeUserData(
|
||||
cardId: String,
|
||||
cardId: String? = null,
|
||||
userData: ByteArray? = null,
|
||||
userCounter: Int? = null,
|
||||
initialMessage: Message? = null,
|
||||
|
|
@ -219,7 +219,7 @@ class TangemSdk(
|
|||
* UserProtectedCounter and UserProtectedData require PIN2 for confirmation.
|
||||
*/
|
||||
fun writeProtectedUserData(
|
||||
cardId: String,
|
||||
cardId: String? = null,
|
||||
userProtectedData: ByteArray? = null,
|
||||
userProtectedCounter: Int? = null,
|
||||
initialMessage: Message? = null,
|
||||
|
|
@ -248,7 +248,7 @@ class TangemSdk(
|
|||
* card response in the form of [ReadUserDataResponse] if the task was performed successfully
|
||||
* or [TangemSdkError] in case of an error.
|
||||
*/
|
||||
fun readUserData(cardId: String, initialMessage: Message? = null,
|
||||
fun readUserData(cardId: String? = null, initialMessage: Message? = null,
|
||||
callback: (result: CompletionResult<ReadUserDataResponse>) -> Unit) {
|
||||
startSessionWithRunnable(ReadUserDataCommand(), cardId, initialMessage, callback)
|
||||
}
|
||||
|
|
@ -270,7 +270,7 @@ class TangemSdk(
|
|||
* card response in the form of [CreateWalletResponse] if the task was performed successfully
|
||||
* or [TangemSdkError] in case of an error.
|
||||
*/
|
||||
fun createWallet(cardId: String, initialMessage: Message? = null,
|
||||
fun createWallet(cardId: String? = null, initialMessage: Message? = null,
|
||||
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
|
||||
startSessionWithRunnable(CreateWalletTask(), cardId, initialMessage, callback)
|
||||
}
|
||||
|
|
@ -289,7 +289,7 @@ class TangemSdk(
|
|||
* card response in the form of [PurgeWalletResponse] if the task was performed successfully
|
||||
* or [TangemSdkError] in case of an error.
|
||||
*/
|
||||
fun purgeWallet(cardId: String, initialMessage: Message? = null,
|
||||
fun purgeWallet(cardId: String? = null, initialMessage: Message? = null,
|
||||
callback: (result: CompletionResult<PurgeWalletResponse>) -> Unit) {
|
||||
startSessionWithRunnable(PurgeWalletCommand(), cardId, initialMessage, callback)
|
||||
}
|
||||
|
|
@ -307,7 +307,7 @@ class TangemSdk(
|
|||
* card response in the form of [DepersonalizeResponse] if the task was performed successfully
|
||||
* or [TangemSdkError] in case of an error.
|
||||
* */
|
||||
fun depersonalize(cardId: String, initialMessage: Message? = null,
|
||||
fun depersonalize(cardId: String? = null, initialMessage: Message? = null,
|
||||
callback: (result: CompletionResult<DepersonalizeResponse>) -> Unit) {
|
||||
startSessionWithRunnable(DepersonalizeCommand(), cardId, initialMessage, callback)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,10 @@ sealed class TangemSdkError(val code: Int) : Exception(code.toString()) {
|
|||
* Tangem cards can sign currently up to 10 hashes during one [com.tangem.commands.SignCommand].
|
||||
* This error is returned when a [com.tangem.commands.SignCommand] receives more than 10 hashes to sign.
|
||||
*/
|
||||
class TooManyhHashesInOneTransaction : TangemSdkError(40906)
|
||||
class TooManyHashesInOneTransaction : TangemSdkError(40906)
|
||||
|
||||
//Write Extra Issuer Data Errors
|
||||
class ExendedDataSizeTooLarge : TangemSdkError(41101)
|
||||
|
||||
//General Errors
|
||||
class NotPersonalized() : TangemSdkError(40001)
|
||||
|
|
@ -114,6 +117,7 @@ sealed class TangemSdkError(val code: Int) : Exception(code.toString()) {
|
|||
*/
|
||||
class VerificationFailed : TangemSdkError(40005)
|
||||
class DataSizeTooLarge : TangemSdkError(40006)
|
||||
|
||||
/**
|
||||
* This error is returned when [ReadIssuerDataTask] or [ReadIssuerExtraDataTask] expects a counter
|
||||
* (when the card's requires it), but the counter is missing.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ interface CommandResponse
|
|||
*/
|
||||
abstract class Command<T : CommandResponse> : CardSessionRunnable<T> {
|
||||
|
||||
override val performPreflightRead: Boolean = true
|
||||
|
||||
/**
|
||||
* Serializes data into an array of [com.tangem.common.tlv.Tlv],
|
||||
* then creates [CommandApdu] with this data.
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ class SignCommand(private val hashes: Array<ByteArray>)
|
|||
|
||||
private fun checkForErrors() {
|
||||
if (hashes.isEmpty()) throw TangemSdkError.EmptyHashes()
|
||||
if (hashes.size > 10) throw TangemSdkError.TooManyhHashesInOneTransaction()
|
||||
if (hashes.size > 10) throw TangemSdkError.TooManyHashesInOneTransaction()
|
||||
if (hashes.any { it.size != hashSizes }) throw TangemSdkError.HashSizeMustBeEqual()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ class WriteIssuerExtraDataCommand(
|
|||
return true
|
||||
}
|
||||
if (issuerData.size > MAX_SIZE) {
|
||||
callback(CompletionResult.Failure(TangemSdkError.DataSizeTooLarge()))
|
||||
callback(CompletionResult.Failure(TangemSdkError.ExendedDataSizeTooLarge()))
|
||||
return true
|
||||
}
|
||||
if (!isCounterValid(issuerDataCounter, card)) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package com.tangem.tangem_sdk_new.converter
|
||||
package com.tangem.commands.common
|
||||
|
||||
import com.google.gson.*
|
||||
import com.tangem.commands.*
|
||||
import com.tangem.common.extensions.print
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.tangem_sdk_new.extensions.print
|
||||
import java.lang.reflect.Type
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
|
@ -1,12 +1,8 @@
|
|||
package com.tangem.commands.personalization
|
||||
|
||||
import com.tangem.CardSession
|
||||
import com.tangem.SessionEnvironment
|
||||
import com.tangem.TangemSdkError
|
||||
import com.tangem.commands.CardStatus
|
||||
import com.tangem.commands.Command
|
||||
import com.tangem.commands.CommandResponse
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.apdu.CommandApdu
|
||||
import com.tangem.common.apdu.Instruction
|
||||
import com.tangem.common.apdu.ResponseApdu
|
||||
|
|
@ -21,17 +17,19 @@ data class DepersonalizeResponse(val success: Boolean) : CommandResponse
|
|||
*/
|
||||
class DepersonalizeCommand : Command<DepersonalizeResponse>() {
|
||||
|
||||
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<DepersonalizeResponse>) -> Unit): Boolean {
|
||||
if (session.environment.card?.status == CardStatus.NotPersonalized) {
|
||||
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
|
||||
return true
|
||||
}
|
||||
if (session.environment.card?.firmwareVersion?.contains("SDK") == false) {
|
||||
callback(CompletionResult.Failure(TangemSdkError.CannotBeDepersonalized()))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
override val performPreflightRead = false
|
||||
|
||||
// override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<DepersonalizeResponse>) -> Unit): Boolean {
|
||||
// if (session.environment.card?.status == CardStatus.NotPersonalized) {
|
||||
// callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
|
||||
// return true
|
||||
// }
|
||||
// if (session.environment.card?.firmwareVersion?.contains("SDK") == false) {
|
||||
// callback(CompletionResult.Failure(TangemSdkError.CannotBeDepersonalized()))
|
||||
// return true
|
||||
// }
|
||||
// return false
|
||||
// }
|
||||
|
||||
override fun serialize(environment: SessionEnvironment): CommandApdu {
|
||||
return CommandApdu(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.tangem_sdk_new.extensions
|
||||
package com.tangem.common.extensions
|
||||
|
||||
fun <T> List<T>.print(delimiter: String = ", ", wrap: Boolean = true): String {
|
||||
val builder = StringBuilder()
|
||||
|
|
@ -11,6 +11,8 @@ import com.tangem.common.CompletionResult
|
|||
|
||||
class CreateWalletTask : CardSessionRunnable<CreateWalletResponse> {
|
||||
|
||||
override val performPreflightRead = true
|
||||
|
||||
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
|
||||
val curve = session.environment.card?.curve
|
||||
if (curve == null) {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import com.tangem.common.CompletionResult
|
|||
*/
|
||||
internal class ScanTask : CardSessionRunnable<Card> {
|
||||
|
||||
override val performPreflightRead = true
|
||||
|
||||
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
|
||||
|
||||
val card = session.environment.card
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ android {
|
|||
applicationId "com.tangem.devkit"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 3
|
||||
versionName "1.1"
|
||||
versionCode 4
|
||||
versionName "1.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,14 @@
|
|||
package com.tangem.devkit.ucase.domain.actions
|
||||
|
||||
import com.tangem.devkit._arch.structure.Id
|
||||
import com.tangem.devkit._arch.structure.PayloadHolder
|
||||
import com.tangem.devkit._arch.structure.abstraction.findItem
|
||||
import com.tangem.devkit.ucase.domain.paramsManager.ActionCallback
|
||||
import com.tangem.devkit.ucase.variants.TlvId
|
||||
import com.tangem.devkit.ucase.domain.paramsManager.triggers.afterAction.AfterScanModifier
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class DepersonalizeAction : BaseAction() {
|
||||
override fun executeMainAction(payload: PayloadHolder, attrs: AttrForAction, callback: ActionCallback) {
|
||||
val item = attrs.itemList.findItem(TlvId.CardId) ?: return
|
||||
val cardId = item.viewModel.data as? String ?: return
|
||||
|
||||
attrs.tangemSdk.depersonalize(cardId) { handleResult(payload, it, null, attrs, callback) }
|
||||
}
|
||||
|
||||
override fun getActionByTag(payload: PayloadHolder, id: Id, attrs: AttrForAction): ((ActionCallback) -> Unit)? {
|
||||
return when (id) {
|
||||
TlvId.CardId -> { callback -> ScanAction().executeMainAction(payload, attrs, callback) }
|
||||
else -> null
|
||||
}
|
||||
attrs.tangemSdk.depersonalize { handleResult(payload, it, AfterScanModifier(), attrs, callback) }
|
||||
}
|
||||
}
|
||||
|
|
@ -10,13 +10,7 @@ import com.tangem.devkit.ucase.variants.TlvId
|
|||
*/
|
||||
class ScanItemsManager : BaseItemsManager(ScanAction())
|
||||
|
||||
class DepersonalizeItemsManager : BaseItemsManager(DepersonalizeAction()) {
|
||||
|
||||
init {
|
||||
setItemChangeConsequences(CardIdConsequence())
|
||||
setItems(listOf(EditTextItem(TlvId.CardId, null)))
|
||||
}
|
||||
}
|
||||
class DepersonalizeItemsManager : BaseItemsManager(DepersonalizeAction())
|
||||
|
||||
class SignItemsManager : BaseItemsManager(SignAction()) {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,18 @@
|
|||
package com.tangem.devkit.ucase.variants.depersonalize.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import com.tangem.commands.Card
|
||||
import com.tangem.devkit.R
|
||||
import com.tangem.devkit.ucase.domain.paramsManager.ItemsManager
|
||||
import com.tangem.devkit.ucase.domain.paramsManager.managers.DepersonalizeItemsManager
|
||||
import com.tangem.devkit.ucase.ui.BaseCardActionFragment
|
||||
import com.tangem.devkit.ucase.variants.responses.ui.ResponseFragment
|
||||
import ru.dev.gbixahue.eu4d.lib.android._android.views.dpToPx
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -10,4 +20,37 @@ import com.tangem.devkit.ucase.ui.BaseCardActionFragment
|
|||
class DepersonalizeActionFragment : BaseCardActionFragment() {
|
||||
|
||||
override val itemsManager: ItemsManager by lazy { DepersonalizeItemsManager() }
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val howToUseView = createHowToUse()
|
||||
contentContainer.layoutParams = FrameLayout.LayoutParams(-1, -1, Gravity.CENTER)
|
||||
contentContainer.addView(howToUseView)
|
||||
}
|
||||
|
||||
private fun createHowToUse(): View {
|
||||
val fl = FrameLayout(requireContext())
|
||||
fl.layoutParams = FrameLayout.LayoutParams(-1, -1, Gravity.CENTER)
|
||||
val tv = TextView(requireContext()).apply {
|
||||
val padding = dpToPx(16f).toInt()
|
||||
setPadding(padding, 0, padding, 0)
|
||||
gravity = Gravity.CENTER
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
|
||||
setText(R.string.htu_depersonalize_action)
|
||||
}
|
||||
fl.addView(tv)
|
||||
return fl
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
swrLayout.isEnabled = false
|
||||
actionFab.setOnClickListener { actionVM.invokeMainAction() }
|
||||
}
|
||||
|
||||
override fun handleResponseCardData(card: Card) {
|
||||
super.handleResponseCardData(card)
|
||||
val bundle = ResponseFragment.setTittle(R.string.fg_name_response_depersonalization)
|
||||
navigateTo(R.id.action_nav_card_action_to_response_screen, bundle, null)
|
||||
}
|
||||
}
|
||||
|
|
@ -38,10 +38,11 @@ class ItemTypes {
|
|||
)
|
||||
|
||||
val hiddenList = mutableListOf<Id>(
|
||||
CardNumberId.Series, CardNumberId.BatchId, PinsId.Pin3, SigningMethodId.SignExternal,
|
||||
CardNumberId.Series, CardNumberId.BatchId, SigningMethodId.SignExternal,
|
||||
SignHashExPropId.CryptoExKey, SignHashExPropId.CheckPin3, SettingsMaskId.OneApdu,
|
||||
SettingsMaskId.UseBlock, SettingsMaskId.ProtectIssuerDataAgainstReplay,
|
||||
SignHashExPropId.RequireTerminalCertSig, SignHashExPropId.RequireTerminalTxSig
|
||||
SignHashExPropId.RequireTerminalCertSig, SignHashExPropId.RequireTerminalTxSig,
|
||||
BlockId.Pins, PinsId.Pin, PinsId.Pin2, PinsId.Pin3, PinsId.Cvc
|
||||
)
|
||||
|
||||
val oftenUsedList = listOf<Id>(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package com.tangem.devkit.ucase.variants.responses.converter
|
||||
|
||||
import com.tangem.commands.common.ResponseFieldConverter
|
||||
import com.tangem.devkit._arch.structure.Id
|
||||
import com.tangem.devkit._arch.structure.abstraction.*
|
||||
import com.tangem.devkit.ucase.variants.responses.item.TextHeaderItem
|
||||
import com.tangem.tangem_sdk_new.converter.ResponseFieldConverter
|
||||
import ru.dev.gbixahue.eu4d.lib.kotlin.stringOf
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.core.os.bundleOf
|
|||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import com.tangem.commands.common.ResponseConverter
|
||||
import com.tangem.devkit.R
|
||||
import com.tangem.devkit._arch.widget.WidgetBuilder
|
||||
import com.tangem.devkit._main.MainViewModel
|
||||
|
|
@ -15,7 +16,6 @@ import com.tangem.devkit.extensions.shareText
|
|||
import com.tangem.devkit.ucase.ui.BaseFragment
|
||||
import com.tangem.devkit.ucase.variants.responses.ResponseViewModel
|
||||
import com.tangem.devkit.ucase.variants.responses.ui.widget.ResponseItemBuilder
|
||||
import com.tangem.tangem_sdk_new.converter.ResponseConverter
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
|
|||
|
|
@ -30,5 +30,6 @@
|
|||
<string name="info_action_user_write_data"> This command writes to the card User Data and User Counter fields.</string>
|
||||
<string name="info_action_user_write_protected_data"> This command writes to the card User Protected Data and User Protected Counter fields.</string>
|
||||
|
||||
<string name="htu_depersonalize_action">To depersonalize the card, press the button at the right bottom corner of the screen</string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -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 {
|
||||
|
|
@ -58,6 +58,4 @@ dependencies {
|
|||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test:runner:1.2.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
}
|
||||
|
|
@ -6,12 +6,10 @@ import android.view.HapticFeedbackConstants
|
|||
import android.view.View
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.tangem.Log
|
||||
import com.tangem.LoggerInterface
|
||||
import com.tangem.Message
|
||||
import com.tangem.SessionViewDelegate
|
||||
import com.tangem.*
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.tangem_sdk_new.extensions.hide
|
||||
import com.tangem.tangem_sdk_new.extensions.localizedDescription
|
||||
import com.tangem.tangem_sdk_new.extensions.show
|
||||
import com.tangem.tangem_sdk_new.nfc.NfcReader
|
||||
import com.tangem.tangem_sdk_new.ui.TouchCardAnimation
|
||||
|
|
@ -142,14 +140,17 @@ class DefaultSessionViewDelegate(private val reader: NfcReader) : SessionViewDel
|
|||
postUI(300) { readingDialog?.dismiss() }
|
||||
}
|
||||
|
||||
override fun onError(errorMessage: String) {
|
||||
override fun onError(error: TangemSdkError) {
|
||||
postUI {
|
||||
readingDialog?.lTouchCard?.hide()
|
||||
readingDialog?.flSecurityDelay?.hide()
|
||||
readingDialog?.flCompletion?.hide()
|
||||
readingDialog?.flError?.show()
|
||||
readingDialog?.tvTaskTitle?.text = activity.getText(R.string.dialog_error)
|
||||
readingDialog?.tvTaskText?.text = errorMessage
|
||||
readingDialog?.tvTaskText?.text = activity.getString(
|
||||
R.string.error_message,
|
||||
error.code.toString(), activity.getString(error.localizedDescription())
|
||||
)
|
||||
performHapticFeedback()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
package com.tangem.tangem_sdk_new.extensions
|
||||
|
||||
import com.tangem.TangemSdkError
|
||||
import com.tangem.tangem_sdk_new.R
|
||||
|
||||
fun TangemSdkError.localizedDescription(): Int {
|
||||
return when (this) {
|
||||
is TangemSdkError.TagLost -> R.string.error_tag_lost
|
||||
is TangemSdkError.ExtendedLengthNotSupported -> R.string.error_operation
|
||||
is TangemSdkError.SerializeCommandError -> R.string.error_operation
|
||||
is TangemSdkError.DeserializeApduFailed -> R.string.error_operation
|
||||
is TangemSdkError.EncodingFailedTypeMismatch -> R.string.error_operation
|
||||
is TangemSdkError.EncodingFailed -> R.string.error_operation
|
||||
is TangemSdkError.DecodingFailedMissingTag -> R.string.error_operation
|
||||
is TangemSdkError.DecodingFailedTypeMismatch -> R.string.error_operation
|
||||
is TangemSdkError.DecodingFailed -> R.string.error_operation
|
||||
is TangemSdkError.UnknownStatus -> R.string.error_operation
|
||||
is TangemSdkError.ErrorProcessingCommand -> R.string.error_operation
|
||||
is TangemSdkError.InvalidState -> R.string.error_operation
|
||||
is TangemSdkError.InsNotSupported -> R.string.error_operation
|
||||
is TangemSdkError.InvalidParams -> R.string.error_operation
|
||||
is TangemSdkError.NeedEncryption -> R.string.error_operation
|
||||
is TangemSdkError.AlreadyPersonalized -> R.string.error_already_personalized
|
||||
is TangemSdkError.CannotBeDepersonalized -> R.string.error_cannot_be_depersonalized
|
||||
is TangemSdkError.Pin1Required -> R.string.error_operation
|
||||
is TangemSdkError.AlreadyCreated -> R.string.error_already_created
|
||||
is TangemSdkError.PurgeWalletProhibited -> R.string.error_purge_prohibited
|
||||
is TangemSdkError.Pin1CannotBeChanged -> R.string.error_pin1_cannot_be_changed
|
||||
is TangemSdkError.Pin2CannotBeChanged -> R.string.error_pin2_cannot_be_changed
|
||||
is TangemSdkError.Pin1CannotBeDefault -> R.string.error_pin1_cannot_be_default
|
||||
is TangemSdkError.NoRemainingSignatures -> R.string.error_no_remaining_signatures
|
||||
is TangemSdkError.EmptyHashes -> R.string.error_empty_hashes
|
||||
is TangemSdkError.HashSizeMustBeEqual -> R.string.error_cannot_be_signed
|
||||
is TangemSdkError.CardIsEmpty -> R.string.error_card_is_empty
|
||||
is TangemSdkError.SignHashesNotAvailable -> R.string.error_cannot_be_signed
|
||||
is TangemSdkError.TooManyHashesInOneTransaction -> R.string.error_cannot_be_signed
|
||||
is TangemSdkError.NotPersonalized -> R.string.error_not_personalized
|
||||
is TangemSdkError.NotActivated -> R.string.error_not_activated
|
||||
is TangemSdkError.CardIsPurged -> R.string.error_purged
|
||||
is TangemSdkError.Pin2OrCvcRequired -> R.string.error_operation
|
||||
is TangemSdkError.VerificationFailed -> R.string.error_verification_failed
|
||||
is TangemSdkError.DataSizeTooLarge -> R.string.error_data_size_too_large
|
||||
is TangemSdkError.ExendedDataSizeTooLarge -> R.string.error_data_size_too_large_extended
|
||||
is TangemSdkError.MissingCounter -> R.string.error_missing_counter
|
||||
is TangemSdkError.OverwritingDataIsProhibited -> R.string.error_data_cannot_be_written
|
||||
is TangemSdkError.DataCannotBeWritten -> R.string.error_data_cannot_be_written
|
||||
is TangemSdkError.MissingIssuerPubicKey -> R.string.error_missing_issuer_public_key
|
||||
is TangemSdkError.UnknownError -> R.string.error_operation
|
||||
is TangemSdkError.UserCancelled -> R.string.error_user_cancelled
|
||||
is TangemSdkError.Busy -> R.string.error_busy
|
||||
is TangemSdkError.MissingPreflightRead -> R.string.error_operation
|
||||
is TangemSdkError.WrongCardNumber -> R.string.error_wrong_card_number
|
||||
is TangemSdkError.WrongCardType -> R.string.error_wrong_card_type
|
||||
is TangemSdkError.CardError -> R.string.error_card_error
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import com.tangem.TangemSdkError
|
|||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.apdu.CommandApdu
|
||||
import com.tangem.common.apdu.ResponseApdu
|
||||
import com.tangem.common.extensions.toHexString
|
||||
|
||||
/**
|
||||
* Provides NFC communication between an Android application and Tangem card.
|
||||
|
|
@ -76,7 +77,9 @@ class NfcReader : CardReader {
|
|||
val rawResponse: ByteArray?
|
||||
try {
|
||||
Log.i(this::class.simpleName!!, "Sending data to the card, size is ${data?.size}")
|
||||
Log.v(this::class.simpleName!!, "Raw data that is to be sent to the card: ${data?.toHexString()}")
|
||||
rawResponse = isoDep?.transceive(data)
|
||||
Log.v(this::class.simpleName!!, "Raw data that was received from the card: ${rawResponse?.toHexString()}")
|
||||
} catch (exception: TagLostException) {
|
||||
callback?.invoke(CompletionResult.Failure(TangemSdkError.TagLost()))
|
||||
isoDep = null
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue