Updated on 2026-08-14

This commit is contained in:
Tangem 2020-06-17 20:53:11 +03:00
commit 752977cd14
101 changed files with 2188 additions and 556 deletions

View file

@ -59,13 +59,13 @@ android {
}
packagingOptions {
pickFirst('META-INF/proguard/okhttp3.pro')
// for one.block:eosiojava:0.1.0
// for one.block:eosiojava:0.1.0
exclude 'lib/x86_64/darwin/libscrypt.dylib'
exclude 'lib/x86_64/freebsd/libscrypt.so'
exclude 'lib/x86_64/linux/libscrypt.so'
exclude 'org.slf4j:slf4j-jdk14:1.7.25'
}
buildToolsVersion '28.0.3'
buildToolsVersion '29.0.3'
}
repositories {
@ -85,15 +85,17 @@ dependencies {
implementation 'androidx.navigation:navigation-ui-ktx:2.2.2'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.fragment:fragment:1.2.4'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation "androidx.preference:preference:1.1.1"
implementation 'androidx.biometric:biometric:1.0.1'
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.core:core-ktx:1.3.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'com.google.dagger:dagger:2.24'
@ -132,6 +134,7 @@ dependencies {
implementation 'com.google.firebase:firebase-crashlytics:17.0.0-beta04'
implementation 'com.google.firebase:firebase-perf:19.0.6'
//dependencies for XRP
implementation files('libs/ripple-core-0.0.1.jar')
//4 dependencies for ripple-core TODO: move to module?
implementation 'net.i2p.crypto:eddsa:0.3.0'

View file

@ -2,6 +2,8 @@ package com.tangem.data;
import com.tangem.tangem_sdk.R;
import java.util.EnumSet;
/**
* Created by dvol on 06.08.2017.
*/
@ -20,7 +22,7 @@ public enum Blockchain {
Rootstock("RSK", "RBTC", 1.0, R.drawable.tangem2, "RSK"),
RootstockToken("RskToken", "RBTC", 1.0, R.drawable.tangem2, "RSK"),
Cardano("CARDANO", "ADA", 1000000.0, R.drawable.tangem2, "Cardano"),
Ripple("XRP", "XRP", 1000000.0, R.drawable.ic_logo_xrp, "XRP"),
Ripple("XRP", "XRP", 1000000.0, R.drawable.ic_logo_xrp, "XRP Ledger"),
Binance("BINANCE", "BNB", 100000000.0, R.drawable.ic_logo_binance, "Binance"),
BinanceTestNet("BINANCE/test", "BNB", 100000000.0, R.drawable.ic_logo_binance, "Binance Testnet"),
BinanceAsset("BinanceAsset", "BNB", 100000000.0, R.drawable.ic_logo_binance, "Binance"),
@ -36,6 +38,8 @@ public enum Blockchain {
FlowDemo("FLOW/demo", "", 1.0, R.drawable.tangem2, "Flow demo"),
TokenEmv("TTW", "ETH", 1.0, R.drawable.ic_logo_ethereum, "Ethereum");
static private EnumSet<Blockchain> payIdSupported = EnumSet.of(Blockchain.Ripple, Blockchain.Ethereum, Blockchain.Bitcoin);
Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) {
mID = ID;
mCurrency = currency;
@ -126,4 +130,8 @@ public enum Blockchain {
return scheme;
}
public Boolean isPayIdSupported() {
return payIdSupported.contains(this);
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.data.network;
import com.tangem.data.network.model.PayIdResponse;
import io.reactivex.Single;
import retrofit2.http.GET;
import retrofit2.http.Header;
import retrofit2.http.Headers;
import retrofit2.http.Path;
public interface PayIdApi {
@Headers("PayID-Version: 1.0")
@GET("{user}")
Single<PayIdResponse> getAddress(@Path("user") String user, @Header("Accept") String acceptHeader);
}

View file

@ -0,0 +1,59 @@
package com.tangem.data.network;
import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import com.tangem.data.Blockchain;
import com.tangem.data.network.model.PayIdResponse;
import com.tangem.tangem_card.util.Log;
import java.security.InvalidParameterException;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class ServerApiPayId {
private static String TAG = ServerApiPayId.class.getSimpleName();
private int requestsCount = 0;
public String getAcceptHeader(Blockchain blockchain) throws InvalidParameterException {
switch (blockchain) {
case Ripple: return "application/xrpl-mainnet+json";
case Bitcoin: return "application/btc-mainnet+json";
case Ethereum: return "application/eth-mainnet+json";
default: throw new InvalidParameterException("PayID is not supported for " + blockchain.getOfficialName());
}
}
public boolean isRequestsSequenceCompleted() {
Log.i(TAG, String.format("isRequestsSequenceCompleted: %s (%d requests left)", String.valueOf(requestsCount <= 0), requestsCount));
return requestsCount <= 0;
}
public void getAddress(String payID, Blockchain blockchain, SingleObserver<PayIdResponse> addressObserver) throws InvalidParameterException {
requestsCount++;
Log.i(TAG, "new getAddress request");
String[] addressParts = payID.split("\\$");
String user = addressParts[0];
String domain = addressParts[1];
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://" + domain + "/")
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
PayIdApi api = retrofit.create(PayIdApi.class);
Single<PayIdResponse> addressSingle = api.getAddress(user, getAcceptHeader(blockchain))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((object, throwable) -> requestsCount--);
addressSingle.subscribe(addressObserver);
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.data.network.model
import com.google.gson.annotations.SerializedName
data class PayIdResponse(
@SerializedName("addresses")
var addresses: List<PayIdAddress>? = null
)
data class PayIdAddress(
@SerializedName("paymentNetwork")
var paymentNetwork: String? = null,
@SerializedName("environment")
var environment: String? = null,
@SerializedName("addressDetails")
var addressDetails: PayIdAddressDetails? = null
)
data class PayIdAddressDetails(
@SerializedName("address")
var address: String? = null
)

View file

@ -91,9 +91,15 @@ abstract class BaseFragment : Fragment() {
protected fun navigateBackWithResult(resultCode: Int, data: Bundle? = null,
@IdRes destination: Int = DESTINATION_NOT_SET) {
(requireActivity() as MainActivity).viewModel.navigationResult =
try {
(requireActivity() as MainActivity).viewModel.navigationResult =
NavigationResult(requestCode, resultCode, data)
return navigateUp(destination)
return navigateUp(destination)
} catch (e: IllegalArgumentException) {
Log.w(this::class.java.simpleName, e.message)
} catch (e: IllegalStateException) {
Log.w(this::class.java.simpleName, e.message)
}
}
companion object {

View file

@ -64,7 +64,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.File
import java.util.*
import kotlin.coroutines.CoroutineContext
class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.ReaderCallback,
@ -74,6 +73,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
companion object {
fun newInstance() = MainFragment()
val TAG: String = MainFragment::class.java.simpleName
const val CARD_SHOP_URI = "https://shop.tangem.com/?afmc=1i&utm_campaign=1i&utm_source=leaddyno&utm_medium=affiliate"
}
override val layoutId = R.layout.fragment_main
@ -102,7 +102,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
// navigateToDestination(R.id.action_main_to_emptyIdFragment)
// init NFC Antenna
nfcDeviceAntenna = NfcDeviceAntennaLocation(context!!, ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna.init()
// set phone name
@ -136,7 +136,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
tvBuyCards?.setText(spannable, TextView.BufferType.SPANNABLE)
llShoppingView?.setOnClickListener {
val uri = Uri.parse("https://www.tangemcards.com")
val uri = Uri.parse(CARD_SHOP_URI)
val intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(intent)
}
@ -342,7 +342,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
} else {
if (cardProtocol.error is CardProtocol.TangemException_ExtendedLengthNotSupported)
if (!NoExtendedLengthSupportDialog.allReadyShowed)
NoExtendedLengthSupportDialog().show(activity!!.supportFragmentManager, NoExtendedLengthSupportDialog.TAG)
NoExtendedLengthSupportDialog().show(requireActivity().supportFragmentManager, NoExtendedLengthSupportDialog.TAG)
lastTag = null
ReadCardInfoTask.resetLastReadInfo()
@ -444,15 +444,15 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
}
override fun onReadWait(msec: Int) {
WaitSecurityDelayDialog.onReadWait(Objects.requireNonNull(activity) as AppCompatActivity?, msec)
WaitSecurityDelayDialog.onReadWait(requireActivity() as AppCompatActivity?, msec)
}
override fun onReadBeforeRequest(timeout: Int) {
WaitSecurityDelayDialog.onReadBeforeRequest(Objects.requireNonNull(activity) as AppCompatActivity?, timeout)
WaitSecurityDelayDialog.onReadBeforeRequest(requireActivity() as AppCompatActivity?, timeout)
}
override fun onReadAfterRequest() {
WaitSecurityDelayDialog.onReadAfterRequest(Objects.requireNonNull(activity))
WaitSecurityDelayDialog.onReadAfterRequest(requireActivity())
}
override fun onMenuItemClick(item: MenuItem?): Boolean {

View file

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

View file

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

View file

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

View file

@ -1,15 +1,19 @@
package com.tangem.ui.fragment.wallet
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.*
import com.tangem.data.network.ServerApiCommon
import com.tangem.data.network.ServerApiCommon.RateInfoListener
import com.tangem.data.network.model.RateInfoResponse
import com.tangem.server_android.PayIdResponse
import com.tangem.server_android.PayIdService
import com.tangem.server_android.Result
import com.tangem.server_android.SetPayIdResponse
import com.tangem.wallet.TangemContext
import kotlinx.coroutines.CoroutineExceptionHandler
class LoadedWalletViewModel : ViewModel() {
private var serverApiCommon: ServerApiCommon = ServerApiCommon()
private val serverApiCommon: ServerApiCommon = ServerApiCommon()
private val payIdService = PayIdService()
private lateinit var ctx: TangemContext
private val rate: MutableLiveData<Float> by lazy {
MutableLiveData<Float>().also {
@ -17,6 +21,10 @@ class LoadedWalletViewModel : ViewModel() {
}
}
private val scope = viewModelScope.coroutineContext + CoroutineExceptionHandler { _, ex ->
throw ex
}
fun getRateInfo(): LiveData<Float> {
return rate
}
@ -28,7 +36,7 @@ class LoadedWalletViewModel : ViewModel() {
}
private fun loadRateInfo() {
serverApiCommon.setRateInfoListener(object: RateInfoListener{
serverApiCommon.setRateInfoListener(object : RateInfoListener {
override fun onSuccess(rateInfoResponse: RateInfoResponse?) {
rate.value = rateInfoResponse!!.data!!.quote!!.usd!!.price
}
@ -38,4 +46,31 @@ class LoadedWalletViewModel : ViewModel() {
}
})
}
fun getPayId(cardId: String, publicKey: String): LiveData<Result<PayIdResponse>> {
return liveData(viewModelScope.coroutineContext) {
emit(
payIdService.getPayId(
cardId,
publicKey
)
)
}
}
fun setPayId(
cardId: String,
publicKey: String,
payId: String,
address: String,
network: String
): LiveData<Result<SetPayIdResponse>> {
return liveData(scope) {
emit(
payIdService.setPayId(
cardId, publicKey, payId, address, network
)
)
}
}
}

View file

@ -22,6 +22,8 @@ public class BtcData extends CoinData {
//for blockchain.info
private boolean hasUnconfirmed = false;
private String resolvedPayIdAddress = null;
public Unspents getUnspentInputsDescription() {
try {
int gatheredUnspents = 0;
@ -90,6 +92,8 @@ public class BtcData extends CoinData {
}
if (B.containsKey("UseBlockcypher")) useBlockcypher = B.getBoolean("UseBlockcypher");
if (B.containsKey("HasUnconfirmed")) useBlockcypher = B.getBoolean("HasUnconfirmed");
if (B.containsKey("ResolvedPayIdAddress")) resolvedPayIdAddress = B.getString("ResolvedPayIdAddress");
else resolvedPayIdAddress = null;
}
@Override
@ -107,6 +111,7 @@ public class BtcData extends CoinData {
if (balanceUnconfirmed != null) B.putLong("BalanceUnconfirmed", balanceUnconfirmed);
if (useBlockcypher) B.putBoolean("UseBlockcypher", true);
if (hasUnconfirmed) B.putBoolean("HasUnconfirmed", true);
if (resolvedPayIdAddress != null) B.putString("ResolvedPayIdAddress", resolvedPayIdAddress);
} catch (Exception e) {
Log.e("Can't save to bundle ", e.getMessage());
}
@ -119,6 +124,7 @@ public class BtcData extends CoinData {
balanceUnconfirmed = null;
unspentTransactions = null;
hasUnconfirmed = false;
resolvedPayIdAddress = null;
}
public CoinEngine.InternalAmount getBalanceInInternalUnits() {
@ -160,4 +166,12 @@ public class BtcData extends CoinData {
public void setHasUnconfirmed(boolean hasUnconfirmed) {
this.hasUnconfirmed = hasUnconfirmed;
}
public String getResolvedPayIdAddress() {
return resolvedPayIdAddress;
}
public void setResolvedPayIdAddress(String resolvedPayIdAddress) {
this.resolvedPayIdAddress = resolvedPayIdAddress;
}
}

View file

@ -11,6 +11,7 @@ import com.tangem.data.network.Server;
import com.tangem.data.network.ServerApiBlockchainInfo;
import com.tangem.data.network.ServerApiBlockcypher;
import com.tangem.data.network.ServerApiCommon;
import com.tangem.data.network.ServerApiPayId;
import com.tangem.data.network.model.BlockchainInfoAddress;
import com.tangem.data.network.model.BlockchainInfoAddressAndUnspents;
import com.tangem.data.network.model.BlockchainInfoInput;
@ -21,6 +22,8 @@ import com.tangem.data.network.model.BlockcypherFee;
import com.tangem.data.network.model.BlockcypherResponse;
import com.tangem.data.network.model.BlockcypherTx;
import com.tangem.data.network.model.BlockcypherTxref;
import com.tangem.data.network.model.PayIdAddress;
import com.tangem.data.network.model.PayIdResponse;
import com.tangem.tangem_card.data.TangemCard;
import com.tangem.tangem_card.reader.CardProtocol;
import com.tangem.tangem_card.tasks.SignTask;
@ -47,6 +50,7 @@ import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.net.URL;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
@ -54,8 +58,10 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import io.reactivex.CompletableObserver;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.observers.DisposableCompletableObserver;
import io.reactivex.observers.DisposableSingleObserver;
import okhttp3.ResponseBody;
@ -154,6 +160,21 @@ public class BtcEngine extends CoinEngine {
return false;
}
if (address.contains("$")) { // PayID
String[] addressParts = address.split("\\$");
if (addressParts.length != 2) {
return false;
}
String addressURL = "https://" + addressParts[1] + "/" + addressParts[0];
try {
new URL(addressURL).toURI();
return true;
} catch (Exception e) {
return false;
}
}
if (address.startsWith("1") || address.startsWith("2") || address.startsWith("3") || address.startsWith("n") || address.startsWith("m")) {
if (address.length() < 25) {
return false;
@ -458,6 +479,14 @@ public class BtcEngine extends CoinEngine {
String myAddress = ctx.getCoinData().getWallet();
byte[] pbKey = ctx.getCard().getWalletPublicKey();
String destination;
//PayID
if (coinData.getResolvedPayIdAddress() != null) {
destination = coinData.getResolvedPayIdAddress();
} else {
destination = targetAddress;
}
// /blockcypher/
// for (BtcData.UnspentTransaction utxo : coinData.getUnspentTransactions()) {
// unspentOutputs.add(new UnspentOutputInfo(BTCUtils.fromHex(utxo.txID), new Transaction.Script(BTCUtils.fromHex(utxo.script)), utxo.amount, utxo.outputN, -1, utxo.txID, null));
@ -502,7 +531,7 @@ public class BtcEngine extends CoinEngine {
final byte[][] bodyHash = new byte[unspentOutputs.size()][];
for (int i = 0; i < unspentOutputs.size(); ++i) {
txForSign[i] = BTCUtils.buildTXForSign(myAddress, targetAddress, myAddress, unspentOutputs, i, amount, change);
txForSign[i] = BTCUtils.buildTXForSign(myAddress, destination, myAddress, unspentOutputs, i, amount, change);
bodyHash[i] = Util.calculateSHA256(txForSign[i]);
bodyDoubleHash[i] = Util.calculateSHA256(bodyHash[i]);
}
@ -555,7 +584,7 @@ public class BtcEngine extends CoinEngine {
unspentOutputs.get(i).scriptForBuild = DerEncodingUtil.packSignDer(r, s, pbKey);
}
byte[] txForSend = BTCUtils.buildTXForSend(targetAddress, myAddress, unspentOutputs, amountFinal, changeFinal);
byte[] txForSend = BTCUtils.buildTXForSend(destination, myAddress, unspentOutputs, amountFinal, changeFinal);
notifyOnNeedSendTransaction(txForSend);
return txForSend;
}
@ -877,8 +906,6 @@ public class BtcEngine extends CoinEngine {
@Override
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) throws Exception {
final int calcSize = calculateEstimatedTransactionSize(targetAddress, amount.toValueString());
Log.e(TAG, String.format("Estimated tx size %d", calcSize));
coinData.minFee = null;
coinData.maxFee = null;
coinData.normalFee = null;
@ -886,6 +913,26 @@ public class BtcEngine extends CoinEngine {
if (!coinData.isUseBlockcypher()) {
final ServerApiCommon serverApiCommon = new ServerApiCommon();
final Integer[] calcSize = new Integer[1];
CompletableObserver payIdObserver = new DisposableCompletableObserver() {
@Override
public void onComplete() {
calcSize[0] = calculateEstimatedTransactionSize(coinData.getResolvedPayIdAddress(), amount.toValueString());
Log.e(TAG, String.format("Estimated tx size %d", calcSize[0]));
serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_PRIORITY);
serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_NORMAL);
serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_MINIMAL);
}
@Override
public void onError(Throwable e) {
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
final ServerApiCommon.EstimatedFeeListener estimatedFeeListener = new ServerApiCommon.EstimatedFeeListener() {
@Override
public void onSuccess(int blockCount, String estimateFeeResponse) {
@ -898,8 +945,8 @@ public class BtcEngine extends CoinEngine {
return;
}
if (calcSize != 0) {
fee = fee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024), BigDecimal.ROUND_DOWN); // per Kb -> per byte
if (calcSize[0] != 0) {
fee = fee.multiply(new BigDecimal(calcSize[0])).divide(new BigDecimal(1024), BigDecimal.ROUND_DOWN); // per Kb -> per byte
} else {
if (blockchainRequestsCallbacks.allowAdvance()) {
serverApiCommon.requestBtcEstimatedFee(blockCount);
@ -948,13 +995,38 @@ public class BtcEngine extends CoinEngine {
};
serverApiCommon.setBtcEstimatedFeeListener(estimatedFeeListener);
serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_PRIORITY);
serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_NORMAL);
serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_MINIMAL);
if (targetAddress.contains("$")) { // PayID
resolvePayID(targetAddress, payIdObserver);
} else {
calcSize[0] = calculateEstimatedTransactionSize(targetAddress, amount.toValueString());
Log.e(TAG, String.format("Estimated tx size %d", calcSize[0]));
serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_PRIORITY);
serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_NORMAL);
serverApiCommon.requestBtcEstimatedFee(ServerApiCommon.ESTIMATE_FEE_MINIMAL);
}
} else {
final ServerApiBlockcypher serverApiBlockcypher = new ServerApiBlockcypher();
final Integer[] calcSize = new Integer[1];
CompletableObserver payIdObserver = new DisposableCompletableObserver() {
@Override
public void onComplete() {
calcSize[0] = calculateEstimatedTransactionSize(coinData.getResolvedPayIdAddress(), amount.toValueString());
Log.e(TAG, String.format("Estimated tx size %d", calcSize[0]));
serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_FEE, "", "");
}
@Override
public void onError(Throwable e) {
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
ServerApiBlockcypher.ResponseListener blockcypherListener = new ServerApiBlockcypher.ResponseListener() {
@Override
public void onSuccess(String method, BlockcypherResponse blockcypherResponse) {
@ -970,9 +1042,9 @@ public class BtcEngine extends CoinEngine {
BigDecimal normalByteFee = new BigDecimal(blockcypherFee.getMedium_fee_per_kb()).divide(BigDecimal.valueOf(1024));
BigDecimal maxByteFee = new BigDecimal(blockcypherFee.getHigh_fee_per_kb()).divide(BigDecimal.valueOf(1024));
CoinEngine.InternalAmount minIntAmount = new CoinEngine.InternalAmount(minByteFee.multiply(BigDecimal.valueOf(calcSize)), "satoshi");
CoinEngine.InternalAmount normalIntAmount = new CoinEngine.InternalAmount(normalByteFee.multiply(BigDecimal.valueOf(calcSize)), "satoshi");
CoinEngine.InternalAmount maxIntAmount = new CoinEngine.InternalAmount(maxByteFee.multiply(BigDecimal.valueOf(calcSize)), "satoshi");
CoinEngine.InternalAmount minIntAmount = new CoinEngine.InternalAmount(minByteFee.multiply(BigDecimal.valueOf(calcSize[0])), "satoshi");
CoinEngine.InternalAmount normalIntAmount = new CoinEngine.InternalAmount(normalByteFee.multiply(BigDecimal.valueOf(calcSize[0])), "satoshi");
CoinEngine.InternalAmount maxIntAmount = new CoinEngine.InternalAmount(maxByteFee.multiply(BigDecimal.valueOf(calcSize[0])), "satoshi");
coinData.minFee = convertToAmount(minIntAmount);
coinData.normalFee = convertToAmount(normalIntAmount);
@ -999,10 +1071,57 @@ public class BtcEngine extends CoinEngine {
serverApiBlockcypher.setResponseListener(blockcypherListener);
serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_FEE, "", "");
if (targetAddress.contains("$")) { // PayID
resolvePayID(targetAddress, payIdObserver);
} else {
calcSize[0] = calculateEstimatedTransactionSize(targetAddress, amount.toValueString());
Log.e(TAG, String.format("Estimated tx size %d", calcSize[0]));
serverApiBlockcypher.requestData(ctx.getBlockchain().getID(), ServerApiBlockcypher.BLOCKCYPHER_FEE, "", "");
}
}
}
private void resolvePayID(String targetAddress, CompletableObserver observer) {
final ServerApiPayId serverApiPayId = new ServerApiPayId();
SingleObserver<PayIdResponse> payIdObserver = new DisposableSingleObserver<PayIdResponse>() {
@Override
public void onSuccess(PayIdResponse payIdResponse) {
try {
String resolvedAddress = null;
for (PayIdAddress address : payIdResponse.getAddresses()) {
if (address.getPaymentNetwork().equals("BTC") &&
address.getEnvironment().equals("MAINNET")) {
resolvedAddress = address.getAddressDetails().getAddress();
break;
}
}
if (validateAddress(resolvedAddress)) {
if (resolvedAddress.equals(coinData.getWallet())) {
observer.onError(new Exception(ctx.getString(R.string.prepare_transaction_error_same_address)));
} else {
coinData.setResolvedPayIdAddress(resolvedAddress);
observer.onComplete();
}
} else {
observer.onError(new Exception("Unknown address format in PayID response"));
}
} catch (Exception e) {
observer.onError(new Exception("Unknown response format on PayID request"));
}
}
@Override
public void onError(Throwable e) {
Log.i(TAG, "onFail: " + "payID" + " " + e.getMessage());
observer.onError(new Exception("PayID error:" + e.getMessage()));
}
};
serverApiPayId.getAddress(targetAddress, ctx.getBlockchain(), payIdObserver);
}
@Override
public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) throws Exception {

View file

@ -14,6 +14,8 @@ public class EthData extends CoinData {
private BigInteger countConfirmedTX = null;
private BigInteger countUnconfirmedTX = BigInteger.valueOf(0);
private String resolvedPayIdAddress = null;
public BigInteger getConfirmedTXCount() {
if (countConfirmedTX == null) {
countConfirmedTX = BigInteger.valueOf(0);
@ -46,6 +48,7 @@ public class EthData extends CoinData {
public void clearInfo() {
super.clearInfo();
balance = null;
resolvedPayIdAddress = null;
}
public CoinEngine.InternalAmount getBalanceInInternalUnits() {
@ -57,6 +60,13 @@ public class EthData extends CoinData {
balance = value;
}
public String getResolvedPayIdAddress() {
return resolvedPayIdAddress;
}
public void setResolvedPayIdAddress(String resolvedPayIdAddress) {
this.resolvedPayIdAddress = resolvedPayIdAddress;
}
@Override
public void loadFromBundle(Bundle B) {
@ -73,6 +83,8 @@ public class EthData extends CoinData {
countConfirmedTX = new BigInteger(B.getString("confirmTx"), 16);
if (B.containsKey("unconfirmTx"))
countUnconfirmedTX = new BigInteger(B.getString("unconfirmTx"), 16);
if (B.containsKey("ResolvedPayIdAddress")) resolvedPayIdAddress = B.getString("ResolvedPayIdAddress");
else resolvedPayIdAddress = null;
}
@Override
@ -86,6 +98,7 @@ public class EthData extends CoinData {
B.putString("confirmTx", getConfirmedTXCount().toString(16));
B.putString("unconfirmTx", getUnconfirmedTXCount().toString(16));
if (resolvedPayIdAddress != null) B.putString("ResolvedPayIdAddress", resolvedPayIdAddress);
} catch (Exception e) {
Log.e("Can't save to bundle ", e.getMessage());

View file

@ -9,10 +9,13 @@ import com.tangem.Constant;
import com.tangem.data.Blockchain;
import com.tangem.data.network.ServerApiBlockcypher;
import com.tangem.data.network.ServerApiInfura;
import com.tangem.data.network.ServerApiPayId;
import com.tangem.data.network.model.BlockcypherFee;
import com.tangem.data.network.model.BlockcypherResponse;
import com.tangem.data.network.model.BlockcypherTxref;
import com.tangem.data.network.model.InfuraResponse;
import com.tangem.data.network.model.PayIdAddress;
import com.tangem.data.network.model.PayIdResponse;
import com.tangem.tangem_card.data.TangemCard;
import com.tangem.tangem_card.tasks.SignTask;
import com.tangem.util.CryptoUtil;
@ -33,8 +36,12 @@ import org.bitcoinj.core.ECKey;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.net.URL;
import java.util.Arrays;
import io.reactivex.SingleObserver;
import io.reactivex.observers.DisposableSingleObserver;
/**
* Created by Ilia on 15.02.2018.
*/
@ -137,6 +144,21 @@ public class EthEngine extends CoinEngine {
return false;
}
if (address.contains("$")) { // PayID
String[] addressParts = address.split("\\$");
if (addressParts.length != 2) {
return false;
}
String addressURL = "https://" + addressParts[1] + "/" + addressParts[0];
try {
new URL(addressURL).toURI();
return true;
} catch (Exception e) {
return false;
}
}
if (!address.startsWith("0x") && !address.startsWith("0X")) {
return false;
}
@ -266,7 +288,7 @@ public class EthEngine extends CoinEngine {
@Override
public boolean checkNewTransactionAmount(Amount amount) {
if( BuildConfig.FLAVOR== Constant.FLAVOR_TANGEM_CARDANO ) {
if (BuildConfig.FLAVOR == Constant.FLAVOR_TANGEM_CARDANO) {
return true;
}
Amount balance = getBalance();
@ -387,6 +409,14 @@ public class EthEngine extends CoinEngine {
Log.e(TAG, "Construct transaction " + amountValue.toString() + " with fee " + feeValue.toString() + (IncFee ? " including" : " excluding"));
String destination;
//PayID
if (coinData.getResolvedPayIdAddress() != null) {
destination = coinData.getResolvedPayIdAddress();
} else {
destination = targetAddress;
}
BigInteger nonceValue = coinData.getConfirmedTXCount();
byte[] pbKey = ctx.getCard().getWalletPublicKey();
@ -401,13 +431,12 @@ public class EthEngine extends CoinEngine {
BigInteger gasLimit = BigInteger.valueOf(21000);
Integer chainId = this.getChainIdNum();
String to = targetAddress;
if (to.startsWith("0x") || to.startsWith("0X")) {
to = to.substring(2);
if (destination.startsWith("0x") || destination.startsWith("0X")) {
destination = destination.substring(2);
}
final EthTransaction tx = EthTransaction.create(to, weiAmount, nonceValue, gasPrice, gasLimit, chainId);
final EthTransaction tx = EthTransaction.create(destination, weiAmount, nonceValue, gasPrice, gasLimit, chainId);
return new SignTask.TransactionToSign() {
@Override
@ -510,7 +539,7 @@ public class EthEngine extends CoinEngine {
break;
}
if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) {
if (serverApiInfura.isRequestsSequenceCompleted() && serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
@ -521,7 +550,7 @@ public class EthEngine extends CoinEngine {
public void onFail(String method, String message) {
Log.e(TAG, "onFail: " + method + " " + message);
ctx.setError(message);
if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) {
if (serverApiInfura.isRequestsSequenceCompleted() && serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(false);
} else {
blockchainRequestsCallbacks.onProgress();
@ -556,7 +585,7 @@ public class EthEngine extends CoinEngine {
Log.e(TAG, "FAIL BLOCKCYPHER_ADDRESS Exception");
}
if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) {
if (serverApiInfura.isRequestsSequenceCompleted() && serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
@ -565,7 +594,7 @@ public class EthEngine extends CoinEngine {
public void onSuccess(String method, BlockcypherFee blockcypherFee) {
Log.e(TAG, "Wrong response type for requestBalanceAndUnspentTransactions");
if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) {
if (serverApiInfura.isRequestsSequenceCompleted() && serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
@ -575,7 +604,7 @@ public class EthEngine extends CoinEngine {
@Override
public void onFail(String method, String message) {
Log.i(TAG, "onFail: " + method + " " + message);
if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) {
if (serverApiInfura.isRequestsSequenceCompleted() && serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
@ -593,6 +622,8 @@ public class EthEngine extends CoinEngine {
@Override
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) {
ServerApiInfura serverApiInfura = new ServerApiInfura(ctx.getBlockchain());
final ServerApiPayId serverApiPayId = new ServerApiPayId();
// request requestData eth gasPrice listener
ServerApiInfura.ResponseListener responseListener = new ServerApiInfura.ResponseListener() {
@Override
@ -623,7 +654,11 @@ public class EthEngine extends CoinEngine {
Log.i(TAG, "normal fee: " + coinData.normalFee.toString());
Log.i(TAG, "max fee : " + coinData.maxFee.toString());
blockchainRequestsCallbacks.onComplete(true);
if (serverApiInfura.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
@Override
@ -634,6 +669,57 @@ public class EthEngine extends CoinEngine {
};
serverApiInfura.setResponseListener(responseListener);
if (targetAddress.contains("$")) { // PayID
SingleObserver<PayIdResponse> observer = new DisposableSingleObserver<PayIdResponse>() {
@Override
public void onSuccess(PayIdResponse payIdResponse) {
try {
String resolvedAddress = null;
for (PayIdAddress address : payIdResponse.getAddresses()) {
if (address.getPaymentNetwork().equals("ETH") &&
address.getEnvironment().equals("MAINNET")) {
resolvedAddress = address.getAddressDetails().getAddress();
break;
}
}
if (validateAddress(resolvedAddress)) {
if (resolvedAddress.equals(coinData.getWallet())) {
ctx.setError(R.string.prepare_transaction_error_same_address);
blockchainRequestsCallbacks.onComplete(false);
} else {
coinData.setResolvedPayIdAddress(resolvedAddress);
if (serverApiInfura.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
} else {
ctx.setError("Unknown address format in PayID response");
blockchainRequestsCallbacks.onComplete(false);
}
} catch (Exception e) {
ctx.setError("Unknown response format on PayID request");
blockchainRequestsCallbacks.onComplete(false);
}
if (serverApiInfura.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
@Override
public void onError(Throwable e) {
Log.i(TAG, "onFail: " + "payID" + " " + e.getMessage());
ctx.setError("PayID error:" + e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
serverApiPayId.getAddress(targetAddress, ctx.getBlockchain(), observer);
}
serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_GAS_PRICE, 67, coinData.getWallet(), "", "");
}
@ -648,7 +734,7 @@ public class EthEngine extends CoinEngine {
@Override
public void onSuccess(String method, InfuraResponse infuraResponse) {
if (method.equals(ServerApiInfura.INFURA_ETH_SEND_RAW_TRANSACTION)) {
if (infuraResponse.getResult()==null || infuraResponse.getResult().isEmpty()) {
if (infuraResponse.getResult() == null || infuraResponse.getResult().isEmpty()) {
ctx.setError("Rejected by node: " + infuraResponse.getError());
blockchainRequestsCallbacks.onComplete(false);
} else {
@ -675,7 +761,9 @@ public class EthEngine extends CoinEngine {
serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_SEND_RAW_TRANSACTION, 67, coinData.getWallet(), "", txStr);
}
public int pendingTransactionTimeoutInSeconds() { return 10; }
public int pendingTransactionTimeoutInSeconds() {
return 10;
}
@Override
public boolean needMultipleLinesForBalance() {

View file

@ -1,115 +0,0 @@
package com.tangem.wallet.xrp;
import java.math.BigInteger;
/**
* Created by Ilia on 15.02.2018.
*/
public class XrpBase58 {
private static final char[] BASE58 = "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz".toCharArray();
private static final int BASE58_CHUNK_DIGITS = 10;//how many base 58 digits fits in long
private static final BigInteger BASE58_CHUNK_MOD = BigInteger.valueOf(0x5fa8624c7fba400L); //58^BASE58_CHUNK_DIGITS
private static final byte[] BASE58_VALUES = new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, -1, -1, -1, -1, -1,
-1, 9, 10, 11, 12, 13, 14, 15, 16, -1, 17, 18, 19, 20, 21, -1,
22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1,
-1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, 46,
47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1};
public static byte[] decodeBase58(String input) {
if (input == null) {
return null;
}
input = input.trim();
if (input.length() == 0) {
return new byte[0];
}
BigInteger resultNum = BigInteger.ZERO;
int nLeadingZeros = 0;
while (nLeadingZeros < input.length() && input.charAt(nLeadingZeros) == BASE58[0]) {
nLeadingZeros++;
}
long acc = 0;
int nDigits = 0;
int p = nLeadingZeros;
while (p < input.length()) {
int v = BASE58_VALUES[input.charAt(p) & 0xff];
if (v >= 0) {
acc *= 58;
acc += v;
nDigits++;
if (nDigits == BASE58_CHUNK_DIGITS) {
resultNum = resultNum.multiply(BASE58_CHUNK_MOD).add(BigInteger.valueOf(acc));
acc = 0;
nDigits = 0;
}
p++;
} else {
break;
}
}
if (nDigits > 0) {
long mul = 58;
while (--nDigits > 0) {
mul *= 58;
}
resultNum = resultNum.multiply(BigInteger.valueOf(mul)).add(BigInteger.valueOf(acc));
}
final int BASE58_SPACE = -2;
while (p < input.length() && BASE58_VALUES[input.charAt(p) & 0xff] == BASE58_SPACE) {
p++;
}
if (p < input.length()) {
return null;
}
byte[] plainNumber = resultNum.toByteArray();
int plainNumbersOffs = plainNumber[0] == 0 ? 1 : 0;
byte[] result = new byte[nLeadingZeros + plainNumber.length - plainNumbersOffs];
System.arraycopy(plainNumber, plainNumbersOffs, result, nLeadingZeros, plainNumber.length - plainNumbersOffs);
return result;
}
public static String encodeBase58(byte[] input) {
if (input == null) {
return null;
}
StringBuilder str = new StringBuilder((input.length * 350) / 256 + 1);
BigInteger bn = new BigInteger(1, input);
long rem;
while (true) {
BigInteger[] divideAndRemainder = bn.divideAndRemainder(BASE58_CHUNK_MOD);
bn = divideAndRemainder[0];
rem = divideAndRemainder[1].longValue();
if (bn.compareTo(BigInteger.ZERO) == 0) {
break;
}
for (int i = 0; i < BASE58_CHUNK_DIGITS; i++) {
str.append(BASE58[(int) (rem % 58)]);
rem /= 58;
}
}
while (rem != 0) {
str.append(BASE58[(int) (rem % 58)]);
rem /= 58;
}
str.reverse();
int nLeadingZeros = 0;
while (nLeadingZeros < input.length && input[nLeadingZeros] == 0) {
str.insert(0, BASE58[0]);
nLeadingZeros++;
}
return str.toString();
}
}

View file

@ -19,6 +19,8 @@ public class XrpData extends CoinData {
private Boolean accountNotFound, targetAccountCreated = false;
private String resolvedPayIdAddress = null;
@Override
public void loadFromBundle(Bundle B) {
super.loadFromBundle(B);
@ -35,6 +37,8 @@ public class XrpData extends CoinData {
else accountNotFound = false;
if (B.containsKey("TargetAccountCreated")) targetAccountCreated = B.getBoolean("TargetAccountCreated");
else targetAccountCreated = false;
if (B.containsKey("ResolvedPayIdAddress")) resolvedPayIdAddress = B.getString("ResolvedPayIdAddress");
else resolvedPayIdAddress = null;
}
@Override
@ -47,6 +51,7 @@ public class XrpData extends CoinData {
if (reserve != null) B.putLong("Reserve", reserve);
if (accountNotFound != null) B.putBoolean("AccoundNotFound", accountNotFound);
if (targetAccountCreated != null) B.putBoolean("TargetAccountCreated", targetAccountCreated);
if (resolvedPayIdAddress != null) B.putString("ResolvedPayIdAddress", resolvedPayIdAddress);
} catch (Exception e) {
Log.e("Can't save to bundle ", e.getMessage());
}
@ -61,6 +66,7 @@ public class XrpData extends CoinData {
reserve = 20000000L;
accountNotFound = false;
targetAccountCreated = false;
resolvedPayIdAddress = null;
}
// balanceUnconfirmed is just the latest balance, it equals balanceConfirmed if no unconfirmed transaction present
@ -124,4 +130,12 @@ public class XrpData extends CoinData {
public boolean hasUnconfirmed() {
return hasBalanceInfo() && !balanceConfirmed.equals(balanceUnconfirmed);
}
public String getResolvedPayIdAddress() {
return resolvedPayIdAddress;
}
public void setResolvedPayIdAddress(String resolvedPayIdAddress) {
this.resolvedPayIdAddress = resolvedPayIdAddress;
}
}

View file

@ -10,7 +10,10 @@ import com.ripple.crypto.ecdsa.ECDSASignature;
import com.ripple.encodings.addresses.Addresses;
import com.ripple.utils.HashUtils;
import com.tangem.App;
import com.tangem.data.network.ServerApiPayId;
import com.tangem.data.network.ServerApiRipple;
import com.tangem.data.network.model.PayIdAddress;
import com.tangem.data.network.model.PayIdResponse;
import com.tangem.data.network.model.RippleResponse;
import com.tangem.tangem_card.data.TangemCard;
import com.tangem.tangem_card.reader.CardProtocol;
@ -27,8 +30,12 @@ import com.tangem.wallet.TangemContext;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.util.Arrays;
import io.reactivex.SingleObserver;
import io.reactivex.observers.DisposableSingleObserver;
public class XrpEngine extends CoinEngine {
private static final String TAG = XrpEngine.class.getSimpleName();
@ -116,25 +123,26 @@ public class XrpEngine extends CoinEngine {
if (address == null || address.isEmpty()) {
return false;
}
if (address.contains("$")) { // PayID
String[] addressParts = address.split("\\$");
if (address.length() < 25) {
return false;
}
if (address.length() > 35) {
return false;
}
if (!address.startsWith("r")) {
return false;
if (addressParts.length != 2) {
return false;
}
String addressURL = "https://" + addressParts[1] + "/" + addressParts[0];
try {
new URL(addressURL).toURI();
return true;
} catch (Exception e) {
return false;
}
}
try {
Addresses.decodeAccountID(address);
return true;
} catch (Exception e) {
return false;
return XrpXAddressService.Companion.validate(address);
}
return true;
}
@Override
@ -342,7 +350,22 @@ public class XrpEngine extends CoinEngine {
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception {
checkBlockchainDataExists();
String amount, fee;
String amount, fee, destination;
Integer destinationTag = null;
//PayID
if (coinData.getResolvedPayIdAddress() != null) {
destination = coinData.getResolvedPayIdAddress();
} else {
destination = targetAddress;
}
//X-address
XrpXAddressDecoded decodedXAddress = XrpXAddressService.Companion.decode(destination);
if (decodedXAddress != null) {
destination = decodedXAddress.getAddress();
destinationTag = decodedXAddress.getDestinationTag();
}
if (IncFee) {
amount = convertToInternalAmount(amountValue).subtract(convertToInternalAmount(feeValue)).setScale(0).toPlainString();
@ -363,10 +386,13 @@ public class XrpEngine extends CoinEngine {
// Put `as` AccountID field Account, `Object` o
payment.as(AccountID.Account, coinData.getWallet());
payment.as(AccountID.Destination, targetAddress);
payment.as(AccountID.Destination, destination);
payment.as(com.ripple.core.coretypes.Amount.Amount, amount);
payment.as(UInt32.Sequence, coinData.getSequence());
payment.as(com.ripple.core.coretypes.Amount.Fee, fee);
if (destinationTag != null) {
payment.as(UInt32.DestinationTag, destinationTag);
}
XrpSignedTransaction signedTx = payment.prepare(canonisePubKey(ctx.getCard().getWalletPublicKeyRar()));
@ -525,13 +551,14 @@ public class XrpEngine extends CoinEngine {
@Override
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) {
final ServerApiRipple serverApiRipple = new ServerApiRipple();
final ServerApiPayId serverApiPayId = new ServerApiPayId();
ServerApiRipple.ResponseListener rippleListener = new ServerApiRipple.ResponseListener() {
@Override
public void onSuccess(String method, RippleResponse rippleResponse) {
Log.i(TAG, "onSuccess: " + method);
switch (method) {
case ServerApiRipple.RIPPLE_ACCOUNT_INFO: {
case ServerApiRipple.RIPPLE_ACCOUNT_INFO: { //check if target account is created
try {
if (rippleResponse.getResult().getError_code().equals(19)) { // "Account not found"
coinData.setTargetAccountCreated(false);
@ -542,7 +569,7 @@ public class XrpEngine extends CoinEngine {
coinData.setTargetAccountCreated(true); //expected behaviour, if account exists, there should be no error code -> null pointer
}
if (serverApiRipple.isRequestsSequenceCompleted()) {
if (serverApiRipple.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
@ -566,7 +593,7 @@ public class XrpEngine extends CoinEngine {
ctx.setError(e.getMessage());
}
if (serverApiRipple.isRequestsSequenceCompleted()) {
if (serverApiRipple.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
@ -585,9 +612,71 @@ public class XrpEngine extends CoinEngine {
};
serverApiRipple.setResponseListener(rippleListener);
serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, targetAddress, "");
serverApiRipple.requestData(ServerApiRipple.RIPPLE_FEE, "", "");
if (targetAddress.contains("$")) { // PayID
SingleObserver<PayIdResponse> observer = new DisposableSingleObserver<PayIdResponse>() {
@Override
public void onSuccess(PayIdResponse payIdResponse) {
try {
String resolvedAddress = null;
for (PayIdAddress address : payIdResponse.getAddresses()) {
if (address.getPaymentNetwork().equals("XRPL") &&
address.getEnvironment().equals("MAINNET")) {
resolvedAddress = address.getAddressDetails().getAddress();
break;
}
}
if (validateAddress(resolvedAddress)) {
coinData.setResolvedPayIdAddress(resolvedAddress);
XrpXAddressDecoded xAddressDecoded = XrpXAddressService.Companion.decode(resolvedAddress);
if (xAddressDecoded == null) { // classic address
if (resolvedAddress.equals(coinData.getWallet())) {
ctx.setError(R.string.prepare_transaction_error_same_address);
blockchainRequestsCallbacks.onComplete(false);
} else {
serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, resolvedAddress, "");
}
} else { // X-address
if (xAddressDecoded.getAddress().equals(coinData.getWallet())) {
ctx.setError(R.string.prepare_transaction_error_same_address);
blockchainRequestsCallbacks.onComplete(false);
} else {
serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, xAddressDecoded.getAddress(), "");
}
}
} else {
ctx.setError("Unknown address format in PayID response");
}
} catch (Exception e) {
ctx.setError("Unknown response format on PayID request");
}
if (serverApiRipple.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
@Override
public void onError(Throwable e) {
Log.i(TAG, "onFail: " + "payID" + " " + e.getMessage());
ctx.setError("PayID error:" + e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
serverApiPayId.getAddress(targetAddress, ctx.getBlockchain(), observer);
} else {
XrpXAddressDecoded xAddressDecoded = XrpXAddressService.Companion.decode(targetAddress);
if (xAddressDecoded == null) { // classic address
serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, targetAddress, "");
} else { // X-address
serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, xAddressDecoded.getAddress(), "");
}
}
}
@Override

View file

@ -0,0 +1,53 @@
package com.tangem.wallet.xrp
import com.ripple.encodings.addresses.Addresses
import com.ripple.encodings.base58.B58
import org.kethereum.extensions.toBigInteger
class XrpXAddressService {
companion object {
private val xrpBase58 = B58("rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz")
private val xrpMainnetPrefix = byteArrayOf(0x05, 0x44)
private val zeroTagBytes = ByteArray(4) { 0 }
fun validate(address: String): Boolean {
return decode(address) != null
}
fun decode(address: String): XrpXAddressDecoded? {
try {
val addressBytes = xrpBase58.decodeChecked(address)
if (addressBytes.size != 31) return null
val prefix = addressBytes.slice(0..1).toByteArray()
if (!prefix.contentEquals(xrpMainnetPrefix)) return null
val accountBytes = addressBytes.slice(2..21).toByteArray()
val classicAddress = Addresses.encodeAccountID(accountBytes)
val flag = addressBytes[22]
val tagBytes = addressBytes.slice(23..26).toByteArray()
val reservedTagBytes = addressBytes.slice(27..30).toByteArray()
if (!reservedTagBytes.contentEquals(zeroTagBytes)) return null
var tag: Int? = null
when (flag) {
0.toByte() -> if (!tagBytes.contentEquals(zeroTagBytes)) return null
1.toByte() -> {
tag = tagBytes.reversedArray().toBigInteger().toInt()
}
else -> return null
}
return XrpXAddressDecoded(classicAddress, tag)
} catch (e: Exception) {
return null
}
}
}
}
data class XrpXAddressDecoded(
val address: String,
val destinationTag: Int?
)

View file

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

View file

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

View file

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

View file

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

View file

@ -202,16 +202,15 @@ class ConfirmTransactionFragment : BaseFragment(), NavigationResultListener, Nfc
object : CoinEngine.BlockchainRequestsCallbacks {
override fun onComplete(success: Boolean) {
if (success) {
onProgress()
progressBar?.visibility = View.INVISIBLE
dtVerified = Date()
doSetFee(rgFee?.checkedRadioButtonId ?: R.id.rbNormalFee)
} else {
finishWithError(Activity.RESULT_CANCELED, ctx.error)
}
}
override fun onProgress() {
doSetFee(rgFee?.checkedRadioButtonId ?: R.id.rbNormalFee)
}
override fun allowAdvance(): Boolean {

View file

@ -2,7 +2,6 @@ package com.tangem.ui
import android.app.Activity
import android.content.Context
import android.net.Uri
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Build
@ -13,7 +12,6 @@ import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import com.tangem.Constant
import com.tangem.data.Blockchain
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.fragment.qr.CameraPermissionManager
@ -24,7 +22,6 @@ import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import kotlinx.android.synthetic.tangemAccess.fragment_prepare_transaction.*
import java.io.IOException
import java.util.*
class PrepareTransactionFragment : BaseFragment(), NavigationResultListener, NfcAdapter.ReaderCallback {
companion object {
@ -48,6 +45,8 @@ class PrepareTransactionFragment : BaseFragment(), NavigationResultListener, Nfc
Html.fromHtml(engine!!.balanceHTML)
tvBalance.text = html
if (ctx.blockchain.isPayIdSupported) etWallet.hint = getString(R.string.prepare_transaction_hint_address_or_pay_id)
if (!engine.allowSelectFeeInclusion()) {
rgIncFee.visibility = View.INVISIBLE
} else {
@ -86,7 +85,7 @@ class PrepareTransactionFragment : BaseFragment(), NavigationResultListener, Nfc
}
btnVerify.setOnClickListener {
if (!UtilHelper.isOnline(context!!)) {
if (!UtilHelper.isOnline(requireContext())) {
Toast.makeText(context, R.string.general_error_no_connection, Toast.LENGTH_LONG).show()
return@setOnClickListener
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package com.tangem.blockchain.blockchains.bitcoin.network.api
package com.tangem.blockchain.blockchains.bitcoin.network.blockchaininfo
import retrofit2.http.GET

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ buildscript {
maven { url 'https://maven.fabric.io/public' }
}
dependencies {
classpath "com.android.tools.build:gradle:$versions.build_gradle"
classpath "com.android.tools.build:gradle:${versions.build_gradle}"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
classpath "com.github.dcendents:android-maven-gradle-plugin:2.1"
classpath 'com.google.gms:google-services:4.3.3'

View file

@ -1,4 +1,4 @@
ext.versions = [
kotlin : '1.3.72',
build_gradle: '3.6.3',
build_gradle: '4.0.0',
]

View file

@ -1,4 +1,4 @@
#Mon Mar 30 09:15:33 MSK 2020
#Thu Jun 11 11:33:17 MSK 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

View file

@ -33,10 +33,13 @@ dependencies {
// implementation 'com.github.TangemCash.card_android-common:card_android-android:0.1.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.squareup.retrofit2:converter-gson:2.6.0'
implementation 'com.squareup.retrofit2:retrofit:2.6.0'
implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:converter-gson:2.6.2'
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.7"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

View file

@ -0,0 +1,40 @@
package com.tangem.server_android
import kotlinx.coroutines.delay
import java.io.IOException
suspend fun <T> retryIO(
times: Int = 3,
initialDelay: Long = 100,
maxDelay: Long = 1000,
factor: Double = 2.0,
block: suspend () -> T): T
{
var currentDelay = initialDelay
repeat(times - 1) {
try {
return block()
} catch (e: IOException) {
// you can log an error here and/or make a more finer-grained
// analysis of the cause to see if retry is needed
}
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
return block()
}
sealed class Result<out T> {
data class Success<out T>(val data: T) : Result<T>()
data class Failure(val error: Throwable?) : Result<Nothing>()
}
suspend fun <T>performRequest(block: suspend () -> T): Result<T> {
return try {
val result = retryIO { block() }
Result.Success(result)
} catch (exception: Exception) {
Result.Failure(exception)
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.server_android
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
class PayIdService {
private val tangemApi: TangemApi by lazy {
provideRetrofit()
.create(TangemApi::class.java)
}
suspend fun getPayId(cardId: String, publicKey: String): Result<PayIdResponse> {
return performRequest { tangemApi.getPayId(cardId, publicKey) }
}
suspend fun setPayId(cardId: String, publicKey: String, payId: String, address: String, network: String): Result<SetPayIdResponse> {
return performRequest { tangemApi.setPayId(cardId, publicKey, payId, address, network) }
}
}
data class PayIdResponse(
val payId: String
)
data class SetPayIdResponse(
val success: Boolean
)
fun provideRetrofit(): Retrofit {
val builder = Retrofit.Builder()
.baseUrl("https://tangem.com/")
.addConverterFactory(GsonConverterFactory.create())
if (BuildConfig.DEBUG)
builder.client(createOkHttpClient())
return builder.build()
}
private fun createOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder().addInterceptor(createHttpLoggingInterceptor()).build()
}
private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor {
val logging = HttpLoggingInterceptor()
logging.level = HttpLoggingInterceptor.Level.BODY
return logging
}

View file

@ -12,6 +12,7 @@ public class Server {
static final String VERIFY = URL_TANGEM + "verify";
static final String VERIFY_AND_GET_INFO = URL_TANGEM + "card/verify-and-get-info";
static final String ARTWORK = URL_TANGEM + "card/artwork";
static final String PAY_ID = ServerURL.API_PAY_ID_TANGEM;
}
}
}

View file

@ -2,4 +2,5 @@ package com.tangem.server_android;
class ServerURL {
static final String API_TANGEM = "https://verify.tangem.com/";
static final String API_PAY_ID_TANGEM = "https://payid.tangem.com/";
}

View file

@ -1,21 +0,0 @@
package com.tangem.server_android;
import com.tangem.server_android.model.CardVerifyAndGetInfo;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.Headers;
import retrofit2.http.POST;
import retrofit2.http.Query;
public interface TangemApi {
@Headers("Content-Type: application/json")
@POST(Server.ApiTangem.Method.VERIFY_AND_GET_INFO)
Call<CardVerifyAndGetInfo.Response> getCardVerifyAndGetInfo(@Body CardVerifyAndGetInfo.Request requestBody);
@GET(Server.ApiTangem.Method.ARTWORK)
Call<ResponseBody> getArtwork(@Query("artworkId") String artworkId, @Query("CID") String CID, @Query("publicKey") String publicKey);
}

View file

@ -0,0 +1,35 @@
package com.tangem.server_android
import com.tangem.server_android.model.CardVerifyAndGetInfo
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.http.*
interface TangemApi {
@Headers("Content-Type: application/json")
@POST(Server.ApiTangem.Method.VERIFY_AND_GET_INFO)
fun getCardVerifyAndGetInfo(@Body requestBody: CardVerifyAndGetInfo.Request?): Call<CardVerifyAndGetInfo.Response>
@GET(Server.ApiTangem.Method.ARTWORK)
fun getArtwork(
@Query("artworkId") artworkId: String?,
@Query("CID") CID: String?,
@Query("publicKey") publicKey: String?
): Call<ResponseBody>
@GET(Server.ApiTangem.Method.PAY_ID)
suspend fun getPayId(
@Query("cid") cardId: String,
@Query("key") publicKey: String
): PayIdResponse
@POST(Server.ApiTangem.Method.PAY_ID)
suspend fun setPayId(
@Query("cid") cardId: String,
@Query("key") publicKey: String,
@Query("payid") payId: String,
@Query("address") address: String,
@Query("network") network: String
): SetPayIdResponse
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,7 +24,7 @@ android {
versionNameSuffix "-beta"
}
}
buildToolsVersion '28.0.3'
buildToolsVersion '29.0.2'
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8

View file

@ -10,7 +10,7 @@ version "$jitpackSdk.version"
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
buildToolsVersion "29.0.3"
defaultConfig {
@ -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'
}

View file

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

View file

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

View file

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