Updated on 2026-08-14

This commit is contained in:
Tangem 2020-07-30 17:42:38 +03:00
commit adcfde4126
350 changed files with 1290 additions and 16122 deletions

7
.idea/dictionaries/.xml generated Normal file
View file

@ -0,0 +1,7 @@
<component name="ProjectDictionaryState">
<dictionary name="Денис">
<words>
<w>blockchains</w>
</words>
</dictionary>
</component>

View file

@ -2,8 +2,6 @@ package com.tangem.data;
import com.tangem.tangem_sdk.R;
import java.util.EnumSet;
/**
* Created by dvol on 06.08.2017.
*/
@ -38,8 +36,6 @@ 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.Token);
Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) {
mID = ID;
mCurrency = currency;
@ -129,9 +125,4 @@ public enum Blockchain {
}
return scheme;
}
public Boolean isPayIdSupported() {
return payIdSupported.contains(this);
}
}

View file

@ -0,0 +1,33 @@
package com.tangem.data
import java.util.*
private val payIdSupported = EnumSet.of(
Blockchain.Ripple,
Blockchain.Ethereum,
Blockchain.Bitcoin,
Blockchain.Token,
Blockchain.Litecoin,
Blockchain.Stellar,
Blockchain.StellarAsset,
Blockchain.Cardano,
Blockchain.Ducatus,
Blockchain.BitcoinCash,
Blockchain.Binance,
Blockchain.BinanceAsset,
Blockchain.Rootstock,
Blockchain.RootstockToken
)
fun Blockchain.isPayIdSupported(): Boolean {
return payIdSupported.contains(this)
}
fun Blockchain.getPayIdNetwork(): String {
return when (this) {
Blockchain.Ripple -> "XRPL"
Blockchain.Rootstock, Blockchain.RootstockToken -> "RSK"
else -> this.currency
}
}

View file

@ -23,9 +23,22 @@ public class ServerApiPayId {
switch (blockchain) {
case Ripple: return "application/xrpl-mainnet+json";
case Bitcoin: return "application/btc-mainnet+json";
case Litecoin: return "application/ltc-mainnet+json";
case Cardano: return "application/ada-mainnet+json";
case Ducatus: return "application/duc-mainnet+json";
case BitcoinCash: return "application/bch-mainnet+json";
case Ethereum:
case Token:
return "application/eth-mainnet+json";
case Stellar:
case StellarAsset:
return "application/xlm-mainnet+json";
case Binance:
case BinanceAsset:
return "application/bnb-mainnet+json";
case Rootstock:
case RootstockToken:
return "application/rsk-mainnet+json";
default: throw new InvalidParameterException("PayID is not supported for " + blockchain.getOfficialName());
}
}

View file

@ -25,6 +25,8 @@ import com.tangem.App
import com.tangem.Constant
import com.tangem.data.Blockchain
import com.tangem.data.dp.PrefsManager
import com.tangem.data.getPayIdNetwork
import com.tangem.data.isPayIdSupported
import com.tangem.server_android.Result
import com.tangem.server_android.ServerApiTangem
import com.tangem.server_android.model.CardVerifyAndGetInfo
@ -49,6 +51,7 @@ import com.tangem.ui.fragment.pin.PinSwapFragment
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.LOG
import com.tangem.util.UtilHelper
import com.tangem.util.extensions.isStart2CoinCard
import com.tangem.wallet.*
import kotlinx.android.synthetic.main.dialog_pay_id.view.*
import kotlinx.android.synthetic.main.fr_loaded_wallet.*
@ -91,7 +94,7 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
resources.getColorStateList(R.color.btn_dark)
}
private val activeColor: ColorStateList by lazy {
val color = if ((Util.bytesToHex(ctx.card?.cid)?.startsWith("10") == true)) {
val color = if (ctx.card?.isStart2CoinCard() == true) {
R.color.start2coin_orange
} else {
R.color.colorAccent
@ -155,7 +158,7 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
getString(R.string.loaded_wallet_load_via_qr)
)
if (ctx.blockchain.isPayIdSupported) {
if (ctx.blockchain.isPayIdSupported() && !ctx.card.isStart2CoinCard()) {
ivPayId.visibility = View.VISIBLE
ivPayId.imageAlpha = 100
ivPayId.setOnClickListener { createPayIdDialog() }
@ -175,7 +178,7 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
if (Util.bytesToHex(ctx.card?.cid)?.startsWith("10") == true) {
if (ctx.card?.isStart2CoinCard() == true) {
btnLoad?.visibility = View.GONE
}
@ -325,7 +328,7 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
}
private fun getPayIdIfApplicable() {
if (ctx.blockchain.isPayIdSupported) {
if (ctx.blockchain.isPayIdSupported() && !ctx.card.isStart2CoinCard()) {
viewModel.getPayId(
Util.byteArrayToHexString(ctx.card.cid!!),
Util.byteArrayToHexString(ctx.card.cardPublicKey!!))
@ -375,13 +378,11 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
}
private fun createPayId(payId: String) {
val network = if (ctx.blockchain == Blockchain.Ripple) "XRPL" else ctx.blockchain.currency
viewModel.setPayId(
Util.byteArrayToHexString(ctx.card.cid!!),
Util.byteArrayToHexString(ctx.card.cardPublicKey!!),
payId,
ctx.coinData.wallet,
network
ctx.coinData.wallet, ctx.blockchain.getPayIdNetwork()
)
.observe(viewLifecycleOwner, Observer { result ->
when (result) {
@ -960,5 +961,4 @@ class LoadedWalletFragment : BaseFragment(), NavigationResultListener, NfcAdapte
clipboard.primaryClip = ClipData.newPlainText(text, text)
Toast.makeText(activity, R.string.loaded_wallet_toast_copied, Toast.LENGTH_LONG).show()
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.util.extensions
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_card.util.Util
fun TangemCard.isStart2CoinCard(): Boolean = (Util.bytesToHex(this.cid).startsWith("1"))

View file

@ -58,6 +58,9 @@ public abstract class CoinData {
if (B.containsKey("sentTransactionsCount")) {
sentTransactionsCount = B.getInt("sentTransactionsCount");
}
if (B.containsKey("ResolvedPayIdAddress")) resolvedPayIdAddress = B.getString("ResolvedPayIdAddress");
else resolvedPayIdAddress = null;
}
public void saveToBundle(Bundle B) {
@ -76,6 +79,8 @@ public abstract class CoinData {
B.putString("validationNodeDescription", validationNodeDescription);
B.putInt("sentTransactionsCount", sentTransactionsCount);
if (resolvedPayIdAddress != null) B.putString("ResolvedPayIdAddress", resolvedPayIdAddress);
} catch (Exception e) {
Log.e("Can't save to bundle ", e.getMessage());
}
@ -155,6 +160,7 @@ public abstract class CoinData {
rate = 0f;
rateAlter = 0f;
sentTransactionsCount = 0;
resolvedPayIdAddress = null;
}
// private AtomicInteger failedBalanceRequestCounter;
@ -212,4 +218,15 @@ public abstract class CoinData {
public void incSentTransactionsCount() {
sentTransactionsCount++;
}
private String resolvedPayIdAddress = null;
public String getResolvedPayIdAddress() {
return resolvedPayIdAddress;
}
public void setResolvedPayIdAddress(String resolvedPayIdAddress) {
this.resolvedPayIdAddress = resolvedPayIdAddress;
}
}

View file

@ -11,6 +11,7 @@ import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.net.URL;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.text.DecimalFormat;
@ -392,4 +393,18 @@ public abstract class CoinEngine {
public int pendingTransactionTimeoutInSeconds() { return 30; }
protected boolean validatePayId(String payId) {
String[] addressParts = payId.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;
}
}
}

View file

@ -6,11 +6,14 @@ import android.util.Log;
import com.tangem.App;
import com.tangem.data.network.ServerApiBlockchair;
import com.tangem.data.network.ServerApiPayId;
import com.tangem.data.network.model.BlockchairAddressData;
import com.tangem.data.network.model.BlockchairAddressResponse;
import com.tangem.data.network.model.BlockchairStatsResponse;
import com.tangem.data.network.model.BlockchairTransactionResponse;
import com.tangem.data.network.model.BlockchairUnspentOutput;
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;
@ -130,6 +133,10 @@ public class BtcCashEngine extends CoinEngine {
@Override
public boolean validateAddress(String address) {
if (address != null && address.contains("$")) { // PayID
return validatePayId(address);
}
return CashAddr.isValidCashAddress(address);
}
@ -373,8 +380,16 @@ public class BtcCashEngine extends CoinEngine {
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception {
checkBlockchainDataExists();
String destination;
//PayID
if (coinData.getResolvedPayIdAddress() != null) {
destination = coinData.getResolvedPayIdAddress();
} else {
destination = targetAddress;
}
String srcLegacyAddress = convertToLegacyAddress(ctx.getCoinData().getWallet());
String destLegacyAddress = convertToLegacyAddress(targetAddress);
String destLegacyAddress = convertToLegacyAddress(destination);
byte[] pbKey = ctx.getCard().getWalletPublicKeyRar(); //ALWAYS USING COMPRESS KEY
final ArrayList<UnspentOutputInfo> unspentOutputs = new ArrayList<>();
@ -576,8 +591,38 @@ public class BtcCashEngine 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));
CompletableObserver payIdObserver = new DisposableCompletableObserver() {
@Override
public void onComplete() {
int calcSize = calculateEstimatedTransactionSize(coinData.getResolvedPayIdAddress(), amount.toValueString());
Log.e(TAG, String.format("Estimated tx size %d", calcSize));
checkFee(blockchainRequestsCallbacks, calcSize);
blockchainRequestsCallbacks.onComplete(true);
}
@Override
public void onError(Throwable e) {
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
if (targetAddress.contains("$")) { // PayID
resolvePayID(targetAddress, payIdObserver);
} else {
int calcSize = calculateEstimatedTransactionSize(targetAddress, amount.toValueString());
Log.e(TAG, String.format("Estimated tx size %d", calcSize));
checkFee(blockchainRequestsCallbacks, calcSize);
blockchainRequestsCallbacks.onComplete(true);
}
}
private void checkFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, int calcSize) {
coinData.minFee = null;
coinData.maxFee = null;
coinData.normalFee = null;
@ -618,6 +663,46 @@ public class BtcCashEngine extends CoinEngine {
serverApiBlockchair.getStats(statsObserver);
}
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(ctx.getBlockchain().getCurrency()) &&
address.getEnvironment().equals("MAINNET")) {
resolvedAddress = address.getAddressDetails().getAddress();
break;
}
}
if (validateAddress(resolvedAddress)) {
if (!resolvedAddress.equals(coinData.getWallet())) {
coinData.setResolvedPayIdAddress(resolvedAddress);
observer.onComplete();
} else {
observer.onError(new Exception("Resolved PayID address equals source address"));
}
} 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 {
final ServerApiBlockchair serverApiBlockchair = new ServerApiBlockchair(ctx.getBlockchain());

View file

@ -11,7 +11,10 @@ import com.tangem.data.Blockchain;
import com.tangem.data.network.BinanceApi;
import com.tangem.data.network.Server;
import com.tangem.data.network.ServerApiBinance;
import com.tangem.data.network.ServerApiPayId;
import com.tangem.data.network.model.BinanceFees;
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;
@ -44,6 +47,10 @@ import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import io.reactivex.CompletableObserver;
import io.reactivex.SingleObserver;
import io.reactivex.observers.DisposableCompletableObserver;
import io.reactivex.observers.DisposableSingleObserver;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -154,6 +161,10 @@ public class BinanceAssetEngine extends CoinEngine {
return false;
}
if (address.contains("$")) { // PayID
return validatePayId(address);
}
try {
Crypto.decodeAddress(address);
} catch (Exception e) {
@ -380,6 +391,14 @@ public class BinanceAssetEngine extends CoinEngine {
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception {
checkBlockchainDataExists();
String destination;
//PayID
if (coinData.getResolvedPayIdAddress() != null) {
destination = coinData.getResolvedPayIdAddress();
} else {
destination = targetAddress;
}
String amount;
if (IncFee && amountValue.getCurrency().equals(getFeeCurrency())) { //Coin transfer only
@ -398,7 +417,7 @@ public class BinanceAssetEngine extends CoinEngine {
Transfer transfer = new Transfer();
transfer.setCoin(amountValue.getCurrency().equals(getFeeCurrency()) ? getFeeCurrency() : ctx.getCard().getContractAddress());
transfer.setFromAddress(ctx.getCoinData().getWallet());
transfer.setToAddress(targetAddress);
transfer.setToAddress(destination);
transfer.setAmount(amount);
TransactionOption options = TransactionOption.DEFAULT_INSTANCE;
@ -490,6 +509,27 @@ public class BinanceAssetEngine extends CoinEngine {
}
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) {
CompletableObserver payIdObserver = new DisposableCompletableObserver() {
@Override
public void onComplete() {
checkFee(blockchainRequestsCallbacks);
}
@Override
public void onError(Throwable e) {
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
if (targetAddress.contains("$")) { // PayID
resolvePayID(targetAddress, payIdObserver);
} else {
checkFee(blockchainRequestsCallbacks);
}
}
private void checkFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks) {
try {
String baseUrl = Server.ApiBinance.Method.API_V1;
@ -535,6 +575,46 @@ public class BinanceAssetEngine extends CoinEngine {
}
}
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(ctx.getBlockchain().getCurrency()) &&
address.getEnvironment().equals("MAINNET")) {
resolvedAddress = address.getAddressDetails().getAddress();
break;
}
}
if (validateAddress(resolvedAddress)) {
if (!resolvedAddress.equals(coinData.getWallet())) {
coinData.setResolvedPayIdAddress(resolvedAddress);
observer.onComplete();
} else {
observer.onError(new Exception("Resolved PayID address equals source address"));
}
} 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);
}
public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) {
// RequestBody requestBody = TransactionRequestAssemblerExtSign.createRequestBody(txForSend);
try {

View file

@ -11,7 +11,10 @@ import com.tangem.data.Blockchain;
import com.tangem.data.network.BinanceApi;
import com.tangem.data.network.Server;
import com.tangem.data.network.ServerApiBinance;
import com.tangem.data.network.ServerApiPayId;
import com.tangem.data.network.model.BinanceFees;
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;
@ -44,6 +47,10 @@ import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import io.reactivex.CompletableObserver;
import io.reactivex.SingleObserver;
import io.reactivex.observers.DisposableCompletableObserver;
import io.reactivex.observers.DisposableSingleObserver;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -147,6 +154,10 @@ public class BinanceEngine extends CoinEngine {
return false;
}
if (address.contains("$")) { // PayID
return validatePayId(address);
}
try {
Crypto.decodeAddress(address);
} catch (Exception e) {
@ -350,6 +361,14 @@ public class BinanceEngine extends CoinEngine {
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception {
checkBlockchainDataExists();
String destination;
//PayID
if (coinData.getResolvedPayIdAddress() != null) {
destination = coinData.getResolvedPayIdAddress();
} else {
destination = targetAddress;
}
String amount;
if (IncFee) {
@ -368,7 +387,7 @@ public class BinanceEngine extends CoinEngine {
Transfer transfer = new Transfer();
transfer.setCoin("BNB");
transfer.setFromAddress(ctx.getCoinData().getWallet());
transfer.setToAddress(targetAddress);
transfer.setToAddress(destination);
transfer.setAmount(amount);
TransactionOption options = TransactionOption.DEFAULT_INSTANCE;
@ -466,6 +485,27 @@ public class BinanceEngine extends CoinEngine {
}
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) {
CompletableObserver payIdObserver = new DisposableCompletableObserver() {
@Override
public void onComplete() {
checkFee(blockchainRequestsCallbacks);
}
@Override
public void onError(Throwable e) {
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
if (targetAddress.contains("$")) { // PayID
resolvePayID(targetAddress, payIdObserver);
} else {
checkFee(blockchainRequestsCallbacks);
}
}
private void checkFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks) {
try {
String baseUrl;
@ -519,6 +559,46 @@ public class BinanceEngine extends CoinEngine {
}
}
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(ctx.getBlockchain().getCurrency()) &&
address.getEnvironment().equals("MAINNET")) {
resolvedAddress = address.getAddressDetails().getAddress();
break;
}
}
if (validateAddress(resolvedAddress)) {
if (!resolvedAddress.equals(coinData.getWallet())) {
coinData.setResolvedPayIdAddress(resolvedAddress);
observer.onComplete();
} else {
observer.onError(new Exception("Resolved PayID address equals source address"));
}
} 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);
}
public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) {
// RequestBody requestBody = TransactionRequestAssemblerExtSign.createRequestBody(txForSend);
try {

View file

@ -22,8 +22,6 @@ public class BtcData extends CoinData {
//for blockchain.info
private boolean hasUnconfirmed = false;
private String resolvedPayIdAddress = null;
public Unspents getUnspentInputsDescription() {
try {
int gatheredUnspents = 0;
@ -92,8 +90,6 @@ 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
@ -111,7 +107,6 @@ 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());
}
@ -124,7 +119,6 @@ public class BtcData extends CoinData {
balanceUnconfirmed = null;
unspentTransactions = null;
hasUnconfirmed = false;
resolvedPayIdAddress = null;
}
public CoinEngine.InternalAmount getBalanceInInternalUnits() {
@ -166,12 +160,4 @@ 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

@ -161,18 +161,7 @@ public class BtcEngine extends CoinEngine {
}
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;
}
return validatePayId(address);
}
if (address.startsWith("1") || address.startsWith("2") || address.startsWith("3") || address.startsWith("n") || address.startsWith("m")) {
@ -1098,11 +1087,11 @@ public class BtcEngine extends CoinEngine {
}
}
if (validateAddress(resolvedAddress)) {
if (resolvedAddress.equals(coinData.getWallet())) {
observer.onError(new Exception(ctx.getString(R.string.prepare_transaction_error_same_address)));
} else {
if (!resolvedAddress.equals(coinData.getWallet())) {
coinData.setResolvedPayIdAddress(resolvedAddress);
observer.onComplete();
} else {
observer.onError(new Exception("Resolved PayID address equals source address"));
}
} else {
observer.onError(new Exception("Unknown address format in PayID response"));

View file

@ -9,10 +9,13 @@ import com.tangem.App;
import com.tangem.Constant;
import com.tangem.data.local.PendingTransactionsStorage;
import com.tangem.data.network.ServerApiAdalite;
import com.tangem.data.network.ServerApiPayId;
import com.tangem.data.network.model.AdaliteResponse;
import com.tangem.data.network.model.AdaliteResponseUtxo;
import com.tangem.data.network.model.AdaliteTxData;
import com.tangem.data.network.model.AdaliteUtxoData;
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.tangem_card.util.Util;
@ -46,6 +49,10 @@ import co.nstant.in.cbor.model.Array;
import co.nstant.in.cbor.model.ByteString;
import co.nstant.in.cbor.model.DataItem;
import co.nstant.in.cbor.model.UnsignedInteger;
import io.reactivex.CompletableObserver;
import io.reactivex.SingleObserver;
import io.reactivex.observers.DisposableCompletableObserver;
import io.reactivex.observers.DisposableSingleObserver;
import static com.tangem.wallet.Base58.decodeBase58;
import static com.tangem.wallet.Base58.encodeBase58;
@ -143,6 +150,10 @@ public class CardanoEngine extends CoinEngine {
return false;
}
if (address.contains("$")) { // PayID
return validatePayId(address);
}
byte[] decAddress = Base58.decodeBase58(address);
if (decAddress == null || decAddress.length == 0) {
@ -399,6 +410,14 @@ public class CardanoEngine extends CoinEngine {
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception {
checkBlockchainDataExists();
String destination;
//PayID
if (coinData.getResolvedPayIdAddress() != null) {
destination = coinData.getResolvedPayIdAddress();
} else {
destination = targetAddress;
}
String myAddress = ctx.getCoinData().getWallet();
byte[] pbKey = ctx.getCard().getWalletPublicKey();
List<CardanoData.UnspentOutput> utxoList = coinData.getUnspentOutputs();
@ -457,7 +476,7 @@ public class CardanoEngine extends CoinEngine {
}
//1st output
DataItem targetAddressItem = new CborDecoder(new ByteArrayInputStream(decodeBase58(targetAddress))).decode().get(0);
DataItem targetAddressItem = new CborDecoder(new ByteArrayInputStream(decodeBase58(destination))).decode().get(0);
outputsArray
.addArray()
.add(targetAddressItem)
@ -697,9 +716,40 @@ public class CardanoEngine extends CoinEngine {
@Override
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) {
// int calcSize = calculateEstimatedTransactionSize(targetAddress, amount);
CompletableObserver payIdObserver = new DisposableCompletableObserver() {
@Override
public void onComplete() {
int calcSize = calculateEstimatedTransactionSize(coinData.getResolvedPayIdAddress(), amount);
Log.e(TAG, String.format("Estimated tx size %d", calcSize));
calculateFee(calcSize);
blockchainRequestsCallbacks.onComplete(true);
}
@Override
public void onError(Throwable e) {
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
if (targetAddress.contains("$")) { // PayID
resolvePayID(targetAddress, payIdObserver);
} else {
int calcSize = calculateEstimatedTransactionSize(targetAddress, amount);
Log.e(TAG, String.format("Estimated tx size %d", calcSize));
calculateFee(calcSize);
blockchainRequestsCallbacks.onComplete(true);
}
}
private void calculateFee(int calcSize) {
final double A = 0.155381;
final double B = 0.000043946;
int calcSize = calculateEstimatedTransactionSize(targetAddress, amount);
double fee = A + B * calcSize;
Amount feeAmount = new Amount(new BigDecimal(fee).setScale(getDecimals(), RoundingMode.UP), getFeeCurrency());
@ -707,8 +757,46 @@ public class CardanoEngine extends CoinEngine {
coinData.minFee = feeAmount;
coinData.normalFee = feeAmount;
coinData.maxFee = feeAmount;
}
blockchainRequestsCallbacks.onComplete(true);
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(ctx.getBlockchain().getCurrency()) &&
address.getEnvironment().equals("MAINNET")) {
resolvedAddress = address.getAddressDetails().getAddress();
break;
}
}
if (validateAddress(resolvedAddress)) {
if (!resolvedAddress.equals(coinData.getWallet())) {
coinData.setResolvedPayIdAddress(resolvedAddress);
observer.onComplete();
} else {
observer.onError(new Exception("Resolved PayID address equals source address"));
}
} 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
@ -732,15 +820,8 @@ public class CardanoEngine extends CoinEngine {
@Override
public void onSuccess(String method, String stringResponse) {
if (method.equals(ServerApiAdalite.ADALITE_SEND)) {
if (stringResponse.equals("\"Transaction sent successfully!\"")) {
ctx.setError(null);
blockchainRequestsCallbacks.onComplete(true);
} else { // TODO: Make check for a valid send response
ctx.setError(stringResponse);
blockchainRequestsCallbacks.onComplete(false);
}
}
ctx.setError(null);
blockchainRequestsCallbacks.onComplete(true);
}

View file

@ -6,9 +6,12 @@ import android.util.Log;
import com.tangem.App;
import com.tangem.data.network.ServerApiBitcore;
import com.tangem.data.network.ServerApiPayId;
import com.tangem.data.network.model.BitcoreBalanceAndUnspents;
import com.tangem.data.network.model.BitcoreSendResponse;
import com.tangem.data.network.model.BitcoreUtxo;
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;
@ -37,7 +40,9 @@ import java.security.NoSuchProviderException;
import java.util.ArrayList;
import java.util.Arrays;
import io.reactivex.CompletableObserver;
import io.reactivex.SingleObserver;
import io.reactivex.observers.DisposableCompletableObserver;
import io.reactivex.observers.DisposableSingleObserver;
public class DucatusEngine extends BtcEngine {
@ -131,6 +136,10 @@ public class DucatusEngine extends BtcEngine {
return false;
}
if (address.contains("$")) { // PayID
return validatePayId(address);
}
if (address.length() < 25) {
return false;
}
@ -397,6 +406,14 @@ public class DucatusEngine extends BtcEngine {
ArrayList<UnspentOutputInfo> unspentOutputs = new ArrayList<>();
checkBlockchainDataExists();
String destination;
//PayID
if (coinData.getResolvedPayIdAddress() != null) {
destination = coinData.getResolvedPayIdAddress();
} else {
destination = targetAddress;
}
String myAddress = ctx.getCoinData().getWallet();
byte[] pbKey = ctx.getCard().getWalletPublicKey();
@ -430,7 +447,7 @@ public class DucatusEngine extends BtcEngine {
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]);
}
@ -483,7 +500,7 @@ public class DucatusEngine extends BtcEngine {
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;
}
@ -535,14 +552,80 @@ public class DucatusEngine extends BtcEngine {
@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));
CompletableObserver payIdObserver = new DisposableCompletableObserver() {
@Override
public void onComplete() {
int calcSize = calculateEstimatedTransactionSize(coinData.getResolvedPayIdAddress(), amount.toValueString());
Log.e(TAG, String.format("Estimated tx size %d", calcSize));
calculateFee(calcSize);
blockchainRequestsCallbacks.onComplete(true);
}
@Override
public void onError(Throwable e) {
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
if (targetAddress.contains("$")) { // PayID
resolvePayID(targetAddress, payIdObserver);
} else {
int calcSize = calculateEstimatedTransactionSize(targetAddress, amount.toValueString());
Log.e(TAG, String.format("Estimated tx size %d", calcSize));
calculateFee(calcSize);
blockchainRequestsCallbacks.onComplete(true);
}
}
private void calculateFee(int calcSize) {
coinData.minFee = new Amount(BigDecimal.valueOf(calcSize).multiply(BigDecimal.valueOf(0.00000089)), ctx.getBlockchain().getCurrency()); //fee for byte from Ducatus wallet for android
coinData.normalFee = new Amount(BigDecimal.valueOf(calcSize).multiply(BigDecimal.valueOf(0.00000144)), ctx.getBlockchain().getCurrency());
coinData.maxFee = new Amount(BigDecimal.valueOf(calcSize).multiply(BigDecimal.valueOf(0.00000350)), ctx.getBlockchain().getCurrency());
}
blockchainRequestsCallbacks.onComplete(true);
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(ctx.getBlockchain().getCurrency()) &&
address.getEnvironment().equals("MAINNET")) {
resolvedAddress = address.getAddressDetails().getAddress();
break;
}
}
if (validateAddress(resolvedAddress)) {
if (!resolvedAddress.equals(coinData.getWallet())) {
coinData.setResolvedPayIdAddress(resolvedAddress);
observer.onComplete();
} else {
observer.onError(new Exception("Resolved PayID address equals source address"));
}
} 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

View file

@ -14,8 +14,6 @@ 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);
@ -48,7 +46,6 @@ public class EthData extends CoinData {
public void clearInfo() {
super.clearInfo();
balance = null;
resolvedPayIdAddress = null;
}
public CoinEngine.InternalAmount getBalanceInInternalUnits() {
@ -60,14 +57,6 @@ 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) {
super.loadFromBundle(B);
@ -83,8 +72,6 @@ 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
@ -98,7 +85,6 @@ 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

@ -145,18 +145,7 @@ public class EthEngine extends CoinEngine {
}
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;
}
return validatePayId(address);
}
if (!address.startsWith("0x") && !address.startsWith("0X")) {
@ -685,7 +674,7 @@ public class EthEngine extends CoinEngine {
}
if (validateAddress(resolvedAddress)) {
if (resolvedAddress.equals(coinData.getWallet())) {
ctx.setError(R.string.prepare_transaction_error_same_address);
ctx.setError("Resolved PayID address equals source address");
blockchainRequestsCallbacks.onComplete(false);
} else {
coinData.setResolvedPayIdAddress(resolvedAddress);

View file

@ -8,10 +8,13 @@ import com.tangem.App;
import com.tangem.data.local.PendingTransactionsStorage;
import com.tangem.data.network.Server;
import com.tangem.data.network.ServerApiBlockcypher;
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.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;
@ -42,6 +45,11 @@ import java.security.NoSuchProviderException;
import java.util.ArrayList;
import java.util.Arrays;
import io.reactivex.CompletableObserver;
import io.reactivex.SingleObserver;
import io.reactivex.observers.DisposableCompletableObserver;
import io.reactivex.observers.DisposableSingleObserver;
public class LtcEngine extends BtcEngine {
private static final String TAG = LtcEngine.class.getSimpleName();
public BtcData coinData = null;
@ -133,6 +141,10 @@ public class LtcEngine extends BtcEngine {
return false;
}
if (address.contains("$")) { // PayID
return validatePayId(address);
}
if (address.length() < 25) {
return false;
}
@ -403,6 +415,14 @@ public class LtcEngine extends BtcEngine {
ArrayList<UnspentOutputInfo> unspentOutputs = new ArrayList<>();
checkBlockchainDataExists();
String destination;
//PayID
if (coinData.getResolvedPayIdAddress() != null) {
destination = coinData.getResolvedPayIdAddress();
} else {
destination = targetAddress;
}
String myAddress = ctx.getCoinData().getWallet();
byte[] pbKey = ctx.getCard().getWalletPublicKey();
@ -436,7 +456,7 @@ public class LtcEngine extends BtcEngine {
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]);
}
@ -489,7 +509,7 @@ public class LtcEngine extends BtcEngine {
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;
}
@ -632,8 +652,38 @@ public class LtcEngine extends BtcEngine {
@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));
CompletableObserver payIdObserver = new DisposableCompletableObserver() {
@Override
public void onComplete() {
int calcSize = calculateEstimatedTransactionSize(coinData.getResolvedPayIdAddress(), amount.toValueString());
Log.e(TAG, String.format("Estimated tx size %d", calcSize));
checkFee(blockchainRequestsCallbacks, calcSize);
blockchainRequestsCallbacks.onComplete(true);
}
@Override
public void onError(Throwable e) {
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
if (targetAddress.contains("$")) { // PayID
resolvePayID(targetAddress, payIdObserver);
} else {
int calcSize = calculateEstimatedTransactionSize(targetAddress, amount.toValueString());
Log.e(TAG, String.format("Estimated tx size %d", calcSize));
checkFee(blockchainRequestsCallbacks, calcSize);
blockchainRequestsCallbacks.onComplete(true);
}
}
private void checkFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, int calcSize) {
coinData.minFee = null;
coinData.maxFee = null;
coinData.normalFee = null;
@ -692,6 +742,46 @@ public class LtcEngine extends BtcEngine {
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(ctx.getBlockchain().getCurrency()) &&
address.getEnvironment().equals("MAINNET")) {
resolvedAddress = address.getAddressDetails().getAddress();
break;
}
}
if (validateAddress(resolvedAddress)) {
if (!resolvedAddress.equals(coinData.getWallet())) {
coinData.setResolvedPayIdAddress(resolvedAddress);
observer.onComplete();
} else {
observer.onError(new Exception("Resolved PayID address equals source address"));
}
} 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 {
final String txStr = BTCUtils.toHex(txForSend);

View file

@ -4,8 +4,11 @@ import android.net.Uri;
import android.util.Log;
import com.tangem.data.Blockchain;
import com.tangem.data.network.ServerApiPayId;
import com.tangem.data.network.ServerApiRootstock;
import com.tangem.data.network.model.InfuraResponse;
import com.tangem.data.network.model.PayIdAddress;
import com.tangem.data.network.model.PayIdResponse;
import com.tangem.wallet.BTCUtils;
import com.tangem.wallet.CoinEngine;
import com.tangem.wallet.EthTransaction;
@ -15,6 +18,10 @@ import com.tangem.wallet.eth.EthEngine;
import java.math.BigInteger;
import io.reactivex.CompletableObserver;
import io.reactivex.SingleObserver;
import io.reactivex.observers.DisposableSingleObserver;
public class RskEngine extends EthEngine {
private static final String TAG = RskEngine.class.getSimpleName();
@ -132,6 +139,7 @@ public class RskEngine extends EthEngine {
@Override
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) {
ServerApiRootstock serverApiRootstock = new ServerApiRootstock();
final ServerApiPayId serverApiPayId = new ServerApiPayId();
// request requestData gasPrice listener
ServerApiRootstock.ResponseListener responseListener = new ServerApiRootstock.ResponseListener() {
@Override
@ -162,7 +170,11 @@ public class RskEngine extends EthEngine {
Log.i(TAG, "normal fee: " + coinData.normalFee.toString());
Log.i(TAG, "max fee : " + coinData.maxFee.toString());
blockchainRequestsCallbacks.onComplete(true);
if (serverApiRootstock.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
@Override
@ -173,6 +185,57 @@ public class RskEngine extends EthEngine {
};
serverApiRootstock.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("RSK") &&
address.getEnvironment().equals("MAINNET")) {
resolvedAddress = address.getAddressDetails().getAddress();
break;
}
}
if (validateAddress(resolvedAddress)) {
if (resolvedAddress.equals(coinData.getWallet())) {
ctx.setError("Resolved PayID address equals source address");
blockchainRequestsCallbacks.onComplete(false);
} else {
coinData.setResolvedPayIdAddress(resolvedAddress);
if (serverApiRootstock.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 (serverApiRootstock.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);
}
serverApiRootstock.requestData(ServerApiRootstock.ROOTSTOCK_ETH_GAS_PRICE, 67, coinData.getWallet(), "", "");
}

View file

@ -4,8 +4,11 @@ import android.net.Uri;
import android.util.Log;
import com.tangem.data.Blockchain;
import com.tangem.data.network.ServerApiPayId;
import com.tangem.data.network.ServerApiRootstock;
import com.tangem.data.network.model.InfuraResponse;
import com.tangem.data.network.model.PayIdAddress;
import com.tangem.data.network.model.PayIdResponse;
import com.tangem.wallet.BTCUtils;
import com.tangem.wallet.CoinEngine;
import com.tangem.wallet.EthTransaction;
@ -15,6 +18,10 @@ import com.tangem.wallet.token.TokenEngine;
import java.math.BigInteger;
import io.reactivex.CompletableObserver;
import io.reactivex.SingleObserver;
import io.reactivex.observers.DisposableSingleObserver;
public class RskTokenEngine extends TokenEngine {
private static final String TAG = RskTokenEngine.class.getSimpleName();
@ -158,6 +165,7 @@ public class RskTokenEngine extends TokenEngine {
@Override
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) {
ServerApiRootstock serverApiRootstock = new ServerApiRootstock();
final ServerApiPayId serverApiPayId = new ServerApiPayId();
// request requestData gasPrice listener
ServerApiRootstock.ResponseListener responseListener = new ServerApiRootstock.ResponseListener() {
@Override
@ -193,7 +201,11 @@ public class RskTokenEngine extends TokenEngine {
} catch (Exception e) {
e.printStackTrace();
}
blockchainRequestsCallbacks.onComplete(true);
if (serverApiRootstock.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
@Override
@ -204,6 +216,57 @@ public class RskTokenEngine extends TokenEngine {
};
serverApiRootstock.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("RSK") &&
address.getEnvironment().equals("MAINNET")) {
resolvedAddress = address.getAddressDetails().getAddress();
break;
}
}
if (validateAddress(resolvedAddress)) {
if (resolvedAddress.equals(coinData.getWallet())) {
ctx.setError("Resolved PayID address equals source address");
blockchainRequestsCallbacks.onComplete(false);
} else {
coinData.setResolvedPayIdAddress(resolvedAddress);
if (serverApiRootstock.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 (serverApiRootstock.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);
}
serverApiRootstock.requestData(ServerApiRootstock.ROOTSTOCK_ETH_GAS_PRICE, 67, coinData.getWallet(), "", "");
}

View file

@ -209,7 +209,7 @@ public class TokenEngine extends CoinEngine {
if (coinData == null) return false;
if (coinData.getBalanceInInternalUnits() == null && coinData.getBalanceAlterInInternalUnits() == null)
return false;
return (coinData.getBalanceInInternalUnits() != null && coinData.getBalanceInInternalUnits().notZero() ) ||
return (coinData.getBalanceInInternalUnits() != null && coinData.getBalanceInInternalUnits().notZero()) ||
(coinData.getBalanceAlterInInternalUnits() != null && coinData.getBalanceAlterInInternalUnits().notZero());
}
@ -576,7 +576,10 @@ public class TokenEngine extends CoinEngine {
int gasLimitInt = 60000;
if (amountValue.getCurrency().equals("DGX") || amountValue.getCurrency().equals("CGT")) {
if (amountValue.getCurrency().equals("DGX") ||
amountValue.getCurrency().equals("CGT") ||
amountValue.getCurrency().equals("AWG")
) {
gasLimitInt = 300000;
}
@ -740,7 +743,7 @@ public class TokenEngine 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();
@ -775,7 +778,7 @@ public class TokenEngine 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();
@ -874,7 +877,7 @@ public class TokenEngine extends CoinEngine {
}
if (validateAddress(resolvedAddress)) {
if (resolvedAddress.equals(coinData.getWallet())) {
ctx.setError(R.string.prepare_transaction_error_same_address);
ctx.setError("Resolved PayID address equals source address");
blockchainRequestsCallbacks.onComplete(false);
} else {
coinData.setResolvedPayIdAddress(resolvedAddress);
@ -923,7 +926,7 @@ public class TokenEngine 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 {
@ -961,5 +964,7 @@ public class TokenEngine extends CoinEngine {
return getBalance().getCurrency().equals(Blockchain.Ethereum.getCurrency());
}
public int pendingTransactionTimeoutInSeconds() { return 10; }
public int pendingTransactionTimeoutInSeconds() {
return 10;
}
}

View file

@ -5,8 +5,11 @@ import android.text.InputFilter;
import android.util.Log;
import com.tangem.App;
import com.tangem.data.network.ServerApiPayId;
import com.tangem.data.network.ServerApiStellar;
import com.tangem.data.network.StellarRequest;
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.tangem_card.util.Util;
@ -31,6 +34,11 @@ import org.stellar.sdk.responses.operations.OperationResponse;
import java.io.IOException;
import java.math.BigDecimal;
import io.reactivex.CompletableObserver;
import io.reactivex.SingleObserver;
import io.reactivex.observers.DisposableCompletableObserver;
import io.reactivex.observers.DisposableSingleObserver;
/**
* Created by dvol on 7.01.2019.
* <p>
@ -116,7 +124,7 @@ public class XlmAssetEngine extends CoinEngine {
@Override
public boolean hasBalanceInfo() {
if (coinData == null) return false;
return (coinData.getXlmBalance() != null && coinData.getAssetBalance() != null) || (coinData.isError404());
return coinData.getXlmBalance() != null || coinData.isError404();
}
@ -143,6 +151,10 @@ public class XlmAssetEngine extends CoinEngine {
@Override
public boolean validateAddress(String address) {
if (address != null && address.contains("$")) { // PayID
return validatePayId(address);
}
try {
KeyPair kp = KeyPair.fromAccountId(address);
// TODO is it possible to check address testNet or not
@ -379,6 +391,14 @@ public class XlmAssetEngine extends CoinEngine {
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception {
checkBlockchainDataExists();
String destination;
//PayID
if (coinData.getResolvedPayIdAddress() != null) {
destination = coinData.getResolvedPayIdAddress();
} else {
destination = targetAddress;
}
if (coinData.isAssetBalanceZero() && IncFee) {
amountValue = new Amount(amountValue.subtract(feeValue), amountValue.getCurrency());
}
@ -388,12 +408,12 @@ public class XlmAssetEngine extends CoinEngine {
operation = new ChangeTrustOperation.Builder(Asset.createNonNativeAsset(ctx.getCard().getTokenSymbol(), KeyPair.fromAccountId(ctx.getCard().getContractAddress())), "900000000000.0000000").build();
} else {
if (!coinData.isAssetBalanceZero()) {
operation = new PaymentOperation.Builder(KeyPair.fromAccountId(targetAddress), Asset.createNonNativeAsset(ctx.getCard().getTokenSymbol(), KeyPair.fromAccountId(ctx.getCard().getContractAddress())), amountValue.toValueString()).build();
operation = new PaymentOperation.Builder(KeyPair.fromAccountId(destination), Asset.createNonNativeAsset(ctx.getCard().getTokenSymbol(), KeyPair.fromAccountId(ctx.getCard().getContractAddress())), amountValue.toValueString()).build();
} else {
if (coinData.isTargetAccountCreated())
operation = new PaymentOperation.Builder(KeyPair.fromAccountId(targetAddress), new AssetTypeNative(), amountValue.toValueString()).build();
operation = new PaymentOperation.Builder(KeyPair.fromAccountId(destination), new AssetTypeNative(), amountValue.toValueString()).build();
else
operation = new CreateAccountOperation.Builder(KeyPair.fromAccountId(targetAddress), amountValue.toValueString()).build();
operation = new CreateAccountOperation.Builder(KeyPair.fromAccountId(destination), amountValue.toValueString()).build();
}
}
TransactionEx transaction = TransactionEx.buildEx(120, coinData.getAccountResponse(), operation);
@ -564,9 +584,66 @@ public class XlmAssetEngine extends CoinEngine {
@Override
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) throws Exception {
// TODO: get fee stats?
coinData.minFee = coinData.normalFee = coinData.maxFee = coinData.getBaseFee();
checkTargetAccountCreated(blockchainRequestsCallbacks, targetAddress, amount);
CompletableObserver payIdObserver = new DisposableCompletableObserver() {
@Override
public void onComplete() {
checkTargetAccountCreated(blockchainRequestsCallbacks, coinData.getResolvedPayIdAddress(), amount);
}
@Override
public void onError(Throwable e) {
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
if (targetAddress.contains("$")) { // PayID
resolvePayID(targetAddress, payIdObserver);
} else {
checkTargetAccountCreated(blockchainRequestsCallbacks, targetAddress, amount);
}
}
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(ctx.getBlockchain().getCurrency()) &&
address.getEnvironment().equals("MAINNET")) {
resolvedAddress = address.getAddressDetails().getAddress();
break;
}
}
if (validateAddress(resolvedAddress)) {
if (!resolvedAddress.equals(coinData.getWallet())) {
coinData.setResolvedPayIdAddress(resolvedAddress);
observer.onComplete();
} else {
observer.onError(new Exception("Resolved PayID address equals source address"));
}
} 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

View file

@ -7,8 +7,11 @@ import android.util.Log;
import com.tangem.App;
import com.tangem.data.Blockchain;
import com.tangem.data.network.ServerApiPayId;
import com.tangem.data.network.ServerApiStellar;
import com.tangem.data.network.StellarRequest;
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.tangem_card.util.Util;
@ -31,6 +34,11 @@ import org.stellar.sdk.responses.operations.OperationResponse;
import java.io.IOException;
import java.math.BigDecimal;
import io.reactivex.CompletableObserver;
import io.reactivex.SingleObserver;
import io.reactivex.observers.DisposableCompletableObserver;
import io.reactivex.observers.DisposableSingleObserver;
/**
* Created by dvol on 7.01.2019.
* <p>
@ -123,6 +131,10 @@ public class XlmEngine extends CoinEngine {
@Override
public boolean validateAddress(String address) {
if (address != null && address.contains("$")) { // PayID
return validatePayId(address);
}
try {
KeyPair kp = KeyPair.fromAccountId(address);
// TODO is it possible to check address testNet or not
@ -356,18 +368,26 @@ public class XlmEngine extends CoinEngine {
StrictMode.setThreadPolicy(policy);
String destination;
//PayID
if (coinData.getResolvedPayIdAddress() != null) {
destination = coinData.getResolvedPayIdAddress();
} else {
destination = targetAddress;
}
if (IncFee) {
amountValue = new Amount(amountValue.subtract(feeValue), amountValue.getCurrency());
}
Operation operation;
if (coinData.isTargetAccountCreated()) {
operation = new PaymentOperation.Builder(KeyPair.fromAccountId(targetAddress), new AssetTypeNative(), amountValue.toValueString()).build();
operation = new PaymentOperation.Builder(KeyPair.fromAccountId(destination), new AssetTypeNative(), amountValue.toValueString()).build();
} else {
if (amountValue.compareTo(coinData.getReserve()) < 0) {
throw new IllegalArgumentException("Target account is not created. Send " + coinData.getReserve().toDescriptionString(getDecimals()) + " or more to create");
}
operation = new CreateAccountOperation.Builder(KeyPair.fromAccountId(targetAddress), amountValue.toValueString()).build();
operation = new CreateAccountOperation.Builder(KeyPair.fromAccountId(destination), amountValue.toValueString()).build();
}
TransactionEx transaction = TransactionEx.buildEx(120, coinData.getAccountResponse(), operation);
@ -541,7 +561,65 @@ public class XlmEngine extends CoinEngine {
@Override
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) throws Exception {
coinData.minFee = coinData.normalFee = coinData.maxFee = coinData.getBaseFee();
checkTargetAccountCreated(blockchainRequestsCallbacks, targetAddress, amount); //TODO: move?
CompletableObserver payIdObserver = new DisposableCompletableObserver() {
@Override
public void onComplete() {
checkTargetAccountCreated(blockchainRequestsCallbacks, coinData.getResolvedPayIdAddress(), amount);
}
@Override
public void onError(Throwable e) {
ctx.setError(e.getMessage());
blockchainRequestsCallbacks.onComplete(false);
}
};
if (targetAddress.contains("$")) { // PayID
resolvePayID(targetAddress, payIdObserver);
} else {
checkTargetAccountCreated(blockchainRequestsCallbacks, targetAddress, amount);
}
}
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(ctx.getBlockchain().getCurrency()) &&
address.getEnvironment().equals("MAINNET")) {
resolvedAddress = address.getAddressDetails().getAddress();
break;
}
}
if (validateAddress(resolvedAddress)) {
if (!resolvedAddress.equals(coinData.getWallet())) {
coinData.setResolvedPayIdAddress(resolvedAddress);
observer.onComplete();
} else {
observer.onError(new Exception("Resolved PayID address equals source address"));
}
} 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

View file

@ -19,7 +19,7 @@ public class XrpData extends CoinData {
private Boolean accountNotFound, targetAccountCreated = false;
private String resolvedPayIdAddress, resolvedPayIdTag = null;
private String resolvedPayIdTag = null;
@Override
public void loadFromBundle(Bundle B) {
@ -37,8 +37,6 @@ 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;
if (B.containsKey("ResolvedPayIdTag")) resolvedPayIdTag = B.getString("ResolvedPayIdTag");
else resolvedPayIdTag = null;
}
@ -53,7 +51,6 @@ 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);
if (resolvedPayIdTag != null) B.putString("ResolvedPayIdTag", resolvedPayIdTag);
} catch (Exception e) {
Log.e("Can't save to bundle ", e.getMessage());
@ -69,7 +66,6 @@ public class XrpData extends CoinData {
reserve = 20000000L;
accountNotFound = false;
targetAccountCreated = false;
resolvedPayIdAddress = null;
resolvedPayIdTag = null;
}
@ -135,14 +131,6 @@ public class XrpData extends CoinData {
return hasBalanceInfo() && !balanceConfirmed.equals(balanceUnconfirmed);
}
public String getResolvedPayIdAddress() {
return resolvedPayIdAddress;
}
public void setResolvedPayIdAddress(String resolvedPayIdAddress) {
this.resolvedPayIdAddress = resolvedPayIdAddress;
}
public String getResolvedPayIdTag() {
return resolvedPayIdTag;
}

View file

@ -124,18 +124,7 @@ public class XrpEngine 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;
}
return validatePayId(address);
}
try {
Addresses.decodeAccountID(address);
@ -646,14 +635,14 @@ public class XrpEngine extends CoinEngine {
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);
ctx.setError("Resolved PayID address equals source 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);
ctx.setError("Resolved PayID address equals source address");
blockchainRequestsCallbacks.onComplete(false);
} else {
serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, xAddressDecoded.getAddress(), "");

View file

@ -12,11 +12,13 @@ import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import com.tangem.Constant
import com.tangem.data.isPayIdSupported
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.fragment.qr.CameraPermissionManager
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.UtilHelper
import com.tangem.util.extensions.isStart2CoinCard
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
@ -45,7 +47,9 @@ 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 (ctx.blockchain.isPayIdSupported() && !ctx.card.isStart2CoinCard()) {
etWallet.hint = getString(R.string.prepare_transaction_hint_address_or_pay_id)
}
if (!engine.allowSelectFeeInclusion()) {
rgIncFee.visibility = View.INVISIBLE

View file

@ -45,10 +45,11 @@ android {
}
dependencies {
implementation project(':tangem-core')
implementation project(':tangem-sdk')
implementation project(':blockchain')
implementation 'com.tangem:core:0.10.4'
implementation 'com.tangem:sdk:0.10.4'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'

View file

@ -40,8 +40,7 @@ android {
dependencies {
// implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':tangem-core')
implementation project(':tangem-sdk')
implementation 'com.tangem:core:1.13'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation 'androidx.appcompat:appcompat:1.1.0'

View file

@ -1,15 +1,18 @@
package com.tangem.blockchain.blockchains.bitcoin
import com.tangem.blockchain.blockchains.ducatus.DucatusMainNetParams
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.*
import org.bitcoinj.core.Base58
import org.bitcoinj.core.LegacyAddress
import org.bitcoinj.core.NetworkParameters
import org.bitcoinj.core.SegwitAddress
import org.bitcoinj.params.MainNetParams
import org.bitcoinj.params.TestNet3Params
import java.security.MessageDigest
class BitcoinAddressService(private val blockchain: Blockchain) : AddressService {
@ -17,6 +20,7 @@ class BitcoinAddressService(private val blockchain: Blockchain) : AddressService
Blockchain.Bitcoin -> MainNetParams()
Blockchain.BitcoinTestnet -> TestNet3Params()
Blockchain.Litecoin -> LitecoinMainNetParams()
Blockchain.Ducatus -> DucatusMainNetParams()
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
}
@ -52,6 +56,7 @@ class BitcoinAddressService(private val blockchain: Blockchain) : AddressService
Blockchain.Bitcoin -> LegacyAddress.fromBase58(MainNetParams(), address)
Blockchain.BitcoinTestnet -> LegacyAddress.fromBase58(TestNet3Params(), address)
Blockchain.Litecoin -> LegacyAddress.fromBase58(LitecoinMainNetParams(), address)
Blockchain.Ducatus -> LegacyAddress.fromBase58(DucatusMainNetParams(), address)
else -> return false
}
true

View file

@ -1,5 +1,6 @@
package com.tangem.blockchain.blockchains.bitcoin
import com.tangem.blockchain.blockchains.ducatus.DucatusMainNetParams
import com.tangem.blockchain.blockchains.litecoin.LitecoinMainNetParams
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
@ -23,6 +24,7 @@ open class BitcoinTransactionBuilder(
Blockchain.Bitcoin, Blockchain.BitcoinCash -> MainNetParams()
Blockchain.BitcoinTestnet -> TestNet3Params()
Blockchain.Litecoin -> LitecoinMainNetParams()
Blockchain.Ducatus -> DucatusMainNetParams()
else -> throw Exception("${blockchain.fullName} blockchain is not supported by ${this::class.simpleName}")
}
var unspentOutputs: List<BitcoinUnspentOutput>? = null
@ -64,7 +66,7 @@ open class BitcoinTransactionBuilder(
}
fun calculateChange(transactionData: TransactionData, unspentOutputs: List<BitcoinUnspentOutput>): BigDecimal {
val fullAmount = unspentOutputs!!.map { it.amount }.reduce { acc, number -> acc + number }
val fullAmount = unspentOutputs.map { it.amount }.reduce { acc, number -> acc + number }
return fullAmount - (transactionData.amount.value!! + (transactionData.fee?.value
?: 0.toBigDecimal()))
}

View file

@ -13,7 +13,7 @@ import java.math.BigDecimal
open class BitcoinWalletManager(
cardId: String,
wallet: Wallet,
private val transactionBuilder: BitcoinTransactionBuilder,
protected val transactionBuilder: BitcoinTransactionBuilder,
private val networkManager: BitcoinProvider
) : WalletManager(cardId, wallet), TransactionSender {

View file

@ -0,0 +1,69 @@
package com.tangem.blockchain.blockchains.ducatus;
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 DucatusMainNetParams 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 DucatusMainNetParams() {
super();
id = "org.bitcoinj.ducatus_mainnet";
// Genesis hash is 12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2
packetMagic = 0xfbc0b6db;
maxTarget = Utils.decodeCompactBits(0x1e0fffffL);
port = 9333;
addressHeader = 49;
p2shHeader = 51; //TODO: is this right? Haven't seen Ducatus p2sh address ever
segwitAddressHrp = "duc"; //TODO: does Ducatus even have bech32 addresses? At least other blockchain's addresses won't pass
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 DucatusMainNetParams instance;
public static synchronized DucatusMainNetParams get() {
if (instance == null) {
instance = new DucatusMainNetParams();
}
return instance;
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.blockchain.blockchains.ducatus
import com.tangem.blockchain.blockchains.bitcoin.BitcoinTransactionBuilder
import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager
import com.tangem.blockchain.blockchains.ducatus.network.DucatusNetworkManager
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.extensions.Result
import java.math.BigDecimal
class DucatusWalletManager(
cardId: String,
wallet: Wallet,
transactionBuilder: BitcoinTransactionBuilder,
networkManager: DucatusNetworkManager
) : BitcoinWalletManager(cardId, wallet, transactionBuilder, networkManager), TransactionSender {
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {
val feeValue = BigDecimal.ONE.movePointLeft(blockchain.decimals())
val sizeResult = transactionBuilder.getEstimateSize(
TransactionData(amount, Amount(amount, feeValue), wallet.address, destination)
)
return when (sizeResult) {
is Result.Failure -> sizeResult
is Result.Success -> {
val transactionSize = sizeResult.data.toBigDecimal()
val minFee = BigDecimal.valueOf(0.00000089).multiply(transactionSize)
val normalFee = BigDecimal.valueOf(0.00000144).multiply(transactionSize)
val priorityFee = BigDecimal.valueOf(0.00000350).multiply(transactionSize)
val fees = listOf(
Amount(minFee, blockchain),
Amount(normalFee, blockchain),
Amount(priorityFee, blockchain)
)
Result.Success(fees)
}
}
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.blockchain.blockchains.ducatus.network
import com.tangem.blockchain.blockchains.ducatus.network.bitcore.BitcoreApi
import com.tangem.blockchain.blockchains.ducatus.network.bitcore.BitcoreProvider
import com.tangem.blockchain.network.API_DUCATUS
import com.tangem.blockchain.network.createRetrofitInstance
class DucatusNetworkManager() : BitcoreProvider(createRetrofitInstance(API_DUCATUS).create(BitcoreApi::class.java))

View file

@ -0,0 +1,21 @@
package com.tangem.blockchain.blockchains.ducatus.network.bitcore
import com.squareup.moshi.JsonClass
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path
interface BitcoreApi {
@GET("api/DUC/mainnet/address/{address}/balance")
suspend fun getBalance(@Path("address") address: String): BitcoreBalance
@GET("api/DUC/mainnet/address/{address}/?unspent=true")
suspend fun getUnspents(@Path("address") address: String): List<BitcoreUtxo>
@POST("api/DUC/mainnet/tx/send")
suspend fun sendTransaction(@Body body: BitcoreSendBody): BitcoreSendResponse
}
@JsonClass(generateAdapter = true)
data class BitcoreSendBody(val rawTx: List<String>)

View file

@ -0,0 +1,64 @@
package com.tangem.blockchain.blockchains.ducatus.network.bitcore
import com.tangem.blockchain.blockchains.bitcoin.BitcoinUnspentOutput
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
import com.tangem.blockchain.extensions.retryIO
import com.tangem.common.extensions.hexToBytes
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
open class BitcoreProvider(private val api: BitcoreApi) : BitcoinProvider{
private val decimals = Blockchain.Ducatus.decimals()
override suspend fun getInfo(address: String): Result<BitcoinAddressResponse> {
return try {
coroutineScope {
val balanceDeferred = retryIO { async { api.getBalance(address) } }
val unspentsDeferred = retryIO { async { api.getUnspents(address) } }
val balanceData = balanceDeferred.await()
val unspents = unspentsDeferred.await()
val unspentTransactions = unspents.map {
BitcoinUnspentOutput(
amount = it.amount!!.toBigDecimal().movePointLeft(decimals),
outputIndex = it.index!!.toLong(),
transactionHash = it.transactionHash!!.hexToBytes(),
outputScript = it.script!!.hexToBytes()
)
}
Result.Success(BitcoinAddressResponse(
balance = balanceData.confirmed!!.toBigDecimal().movePointLeft(decimals),//only confirmed balance is returned right
hasUnconfirmed = balanceData.unconfirmed != null,
unspentOutputs = unspentTransactions
))
}
} catch (error: Exception) {
Result.Failure(error)
}
}
override suspend fun getFee(): Result<BitcoinFee> {
TODO("Not yet implemented")// bitcore is used only in ducatus and fee is hardcoded there
}
override suspend fun sendTransaction(transaction: String): SimpleResult {
return try {
val response = retryIO { api.sendTransaction(BitcoreSendBody(listOf(transaction))) }
if (response.txid != null) {
SimpleResult.Success
} else {
SimpleResult.Failure(Exception("Unknown send transaction error"))
}
} catch (error: Exception) {
SimpleResult.Failure(error)
}
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.blockchain.blockchains.ducatus.network.bitcore
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class BitcoreBalance(
@Json(name = "confirmed")
var confirmed: Long? = null,
@Json(name = "unconfirmed")
var unconfirmed: Long? = null
)
@JsonClass(generateAdapter = true)
data class BitcoreUtxo(
@Json(name = "mintTxid")
var transactionHash: String? = null,
@Json(name = "mintIndex")
var index: Int? = null,
@Json(name = "value")
var amount: Long? = null,
@Json(name = "script")
var script: String? = null
)
@JsonClass(generateAdapter = true)
data class BitcoreSendResponse(
@Json(name = "txid")
var txid: String? = null
)

View file

@ -1,8 +1,8 @@
package com.tangem.blockchain.blockchains.ethereum
import android.util.Log
import com.tangem.blockchain.blockchains.ethereum.network.EthereumNetworkManager
import com.tangem.blockchain.blockchains.ethereum.network.EthereumInfoResponse
import com.tangem.blockchain.blockchains.ethereum.network.EthereumNetworkManager
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
@ -24,7 +24,11 @@ class EthereumWalletManager(
override suspend fun update() {
val result = networkManager.getInfo(wallet.address, wallet.amounts[AmountType.Token]?.address)
val result = networkManager.getInfo(
wallet.address,
wallet.amounts[AmountType.Token]?.address,
wallet.amounts[AmountType.Token]?.decimals
)
when (result) {
is Result.Failure -> updateError(result.error)
is Result.Success -> updateWallet(result.data)

View file

@ -10,7 +10,6 @@ import com.tangem.blockchain.network.createRetrofitInstance
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import org.kethereum.ETH_IN_WEI
import java.math.BigDecimal
import java.math.BigInteger
import java.math.RoundingMode
@ -35,6 +34,7 @@ class EthereumNetworkManager(blockchain: Blockchain) {
}
private val provider: EthereumProvider by lazy { EthereumProvider(api, apiKey) }
private val decimals = Blockchain.Ethereum.decimals()
suspend fun sendTransaction(transaction: String): SimpleResult {
return try {
@ -59,19 +59,20 @@ class EthereumNetworkManager(blockchain: Blockchain) {
}
}
suspend fun getInfo(address: String, contractAddress: String? = null): Result<EthereumInfoResponse> {
suspend fun getInfo(address: String, contractAddress: String? = null, tokenDecimals: Int? = null)
: Result<EthereumInfoResponse> {
return try {
coroutineScope {
val balanceResponse = retryIO { async { provider.getBalance(address) } }
val txCountResponse = retryIO { async { provider.getTxCount(address) } }
val pendingTxCountResponse = retryIO { async { provider.getPendingTxCount(address) } }
var tokenBalanceResponse: Deferred<EthereumResponse>? = null
if (contractAddress != null) {
if (contractAddress != null && tokenDecimals != null) {
tokenBalanceResponse = retryIO { async { provider.getTokenBalance(address, contractAddress) } }
}
Result.Success(EthereumInfoResponse(
balanceResponse.await().result!!.parseAmount(),
tokenBalanceResponse?.await()?.result?.parseAmount(),
balanceResponse.await().result!!.parseAmount(decimals),
tokenBalanceResponse?.await()?.result?.parseAmount(tokenDecimals!!),
txCountResponse.await().result?.responseToNumber()?.toLong() ?: 0,
pendingTxCountResponse.await().result?.responseToNumber()?.toLong() ?: 0
))
@ -87,20 +88,19 @@ class EthereumNetworkManager(blockchain: Blockchain) {
val normalFee = minFee.multiply(BigDecimal(1.2)).setScale(0, RoundingMode.HALF_UP)
val priorityFee = minFee.multiply(BigDecimal(1.5)).setScale(0, RoundingMode.HALF_UP)
return listOf(
minFee.convertFeeToEth(),
normalFee.convertFeeToEth(),
priorityFee.convertFeeToEth()
minFee.movePointLeft(decimals),
normalFee.movePointLeft(decimals),
priorityFee.movePointLeft(decimals)
)
}
private fun String.responseToNumber(): BigInteger = this.substring(2).toBigInteger(16)
private fun String.parseAmount(): BigDecimal =
this.responseToNumber().toBigDecimal().divide(ETH_IN_WEI.toBigDecimal())
private fun String.parseAmount(decimals: Int): BigDecimal =
this.responseToNumber().toBigDecimal().movePointLeft(decimals)
private fun BigDecimal.convertFeeToEth(): BigDecimal {
return this.divide(ETH_IN_WEI.toBigDecimal())
.setScale(12, BigDecimal.ROUND_DOWN).stripTrailingZeros()
return this.movePointLeft(decimals).setScale(decimals, BigDecimal.ROUND_DOWN).stripTrailingZeros()
}
}

View file

@ -19,6 +19,7 @@ enum class Blockchain(
BitcoinTestnet("BTC/test", "BTCt", "Bitcoin Testnet"),
BitcoinCash("BCH", "BCH", "Bitcoin Cash"),
Litecoin("LTC", "LTC", "Litecoin"),
Ducatus("DUC", "DUC", "Ducatus"),
Ethereum("ETH", "ETH", "Ethereum"),
RSK("RSK", "RBTC", "RSK"),
Cardano("CARDANO", "ADA", "Cardano"),
@ -29,7 +30,7 @@ enum class Blockchain(
Tezos("TEZOS", "XTZ", "Tezos");
fun decimals(): Int = when (this) {
Bitcoin, BitcoinTestnet, BitcoinCash, Binance, BinanceTestnet, Litecoin -> 8
Bitcoin, BitcoinTestnet, BitcoinCash, Binance, BinanceTestnet, Litecoin, Ducatus -> 8
Cardano, XRP, Tezos -> 6
Ethereum, RSK -> 18
Stellar -> 7
@ -45,8 +46,7 @@ enum class Blockchain(
fun validateAddress(address: String): Boolean = getAddressService().validate(address)
private fun getAddressService(): AddressService = when (this) {
Unknown -> throw Exception("unsupported blockchain")
Bitcoin, BitcoinTestnet, Litecoin -> BitcoinAddressService(this)
Bitcoin, BitcoinTestnet, Litecoin, Ducatus -> BitcoinAddressService(this)
BitcoinCash -> BitcoinCashAddressService()
Ethereum, RSK -> EthereumAddressService()
Cardano -> CardanoAddressService()
@ -55,6 +55,7 @@ enum class Blockchain(
BinanceTestnet -> BinanceAddressService(true)
Stellar -> StellarAddressService()
Tezos -> TezosAddressService()
Unknown -> throw Exception("unsupported blockchain")
}
fun getShareUri(address: String): String = when (this) {
@ -71,9 +72,10 @@ enum class Blockchain(
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"
Ducatus -> "https://insight.ducatus.io/#/DUC/mainnet/address/$address"
Cardano -> "https://cardanoexplorer.com/address/$address"
Ethereum -> if (token == null) {
"https://etherscan.io/address/"
"https://etherscan.io/address/$address"
} else {
"https://etherscan.io/token/${token.contractAddress}?a=$address"
}

View file

@ -32,7 +32,7 @@ abstract class WalletManager(val cardId: String, var wallet: Wallet) {
}
private fun validateAmount(amount: Amount): Boolean {
return !amount.isAboveZero() &&
return amount.isAboveZero() &&
wallet.fundsAvailable(amount.type) >= amount.value
}
}

View file

@ -12,6 +12,8 @@ import com.tangem.blockchain.blockchains.bitcoincash.BitcoinCashWalletManager
import com.tangem.blockchain.blockchains.cardano.CardanoTransactionBuilder
import com.tangem.blockchain.blockchains.cardano.CardanoWalletManager
import com.tangem.blockchain.blockchains.cardano.network.CardanoNetworkManager
import com.tangem.blockchain.blockchains.ducatus.DucatusWalletManager
import com.tangem.blockchain.blockchains.ducatus.network.DucatusNetworkManager
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionBuilder
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.ethereum.network.EthereumNetworkManager
@ -19,7 +21,6 @@ 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
@ -72,6 +73,13 @@ object WalletManagerFactory {
LitecoinNetworkManager()
)
}
Blockchain.Ducatus -> {
return DucatusWalletManager(
cardId, wallet,
BitcoinTransactionBuilder(walletPublicKey, blockchain),
DucatusNetworkManager()
)
}
Blockchain.Ethereum, Blockchain.RSK -> {
return EthereumWalletManager(
cardId, wallet,

View file

@ -55,4 +55,5 @@ 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_TEZOS = "https://teznode.letzbake.com"
const val API_TEZOS_RESERVE = "https://mainnet.tezrpc.me"
const val API_TEZOS_RESERVE = "https://mainnet.tezrpc.me"
const val API_DUCATUS = "https://ducapi.rocknblock.io/"

View file

@ -0,0 +1,28 @@
package com.tangem.blockchain.blockchains.ducatus
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 DucatusAddressTest {
private val addressService = BitcoinAddressService(Blockchain.Ducatus)
@Test
fun makeAddressFromCorrectPublicKey() {
val walletPublicKey = "0485D520C8B907F0BC5E03FCBBAC212CCD270764BBFF4990A28653A2FB0D656C342DF143C4D52C43582289E20A81D5D014C1384A1FFFEA1D121903AD7ED35A01EA".hexToBytes()
val expected = "Ly3SZetcgr5gkZMwiNwVrts2z2r3jYieAG"
Truth.assertThat(addressService.makeAddress(walletPublicKey))
.isEqualTo(expected)
}
@Test
fun validateCorrectAddress() {
val address = "Ly3SZetcgr5gkZMwiNwVrts2z2r3jYieAG"
Truth.assertThat(addressService.validate(address))
.isTrue()
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.blockchains.binance.BinanceWalletManager
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.ducatus.DucatusWalletManager
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.litecoin.LitecoinWalletManager
import com.tangem.blockchain.blockchains.stellar.StellarWalletManager
@ -18,11 +19,13 @@ import org.junit.Test
internal class WalletManagerFactoryTest {
private val sessionEnvironment = SessionEnvironment()
@Test
fun createBitcoinWalletManager() {
val data = "0108bb00000000000304200754414e47454d00020102800a322e3432642053444b000341040876bdec26b89bd2159a668b9af3d9fe86370f318717c92b8d6c1186fb3648c32a5f9321998cc2d042901c91d40601e79a641e1cbcebe7a2358be6054e1b6e5d0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034254438640e17ceec48c5be36240c98019f95ad8b6e56acfebe60d11979c6279f715d607d76a860a137da8d109e805753f3f56b0130709f4bbf4cb9974b4c57b8469bf4b873041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b310008040000006407010009020bb8604104752a727e14bba5bd73b6714d72500f61ffd11026ad1196d2e1c54577cbeeac3d11fc68a64700f8d533f4e311964ea8fb3aa26c588295f2133868d69c3e62869362040000005c6304000000090f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -33,7 +36,7 @@ internal class WalletManagerFactoryTest {
fun createEthereumWalletManager() {
val data = "0108bb00000000000536200754414e47454d00020102800a322e3432642053444b000341046c8aea0d5a850b0a608acf9a0c453c39ea86131e88bfa78800de3cfb5bf1007aeaa7b9ffc184212255758605c2461be343c0a661d73cabafa4c9c175b3f0e59a0a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b0084034554488640431b6244acfeac479becdff201a7f720a7d70a97edc4e019fb678596baf52dfe9d0e8faf08ceb4443b82d4e66815541f2dc8ec6dd3ff83eb42f06e5eab07f25f3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd26050a736563703235366b3100080400000064070100090205dc60410464dddc3f356744aaecfa07427f9eb996ff537d65f20fb5be3abccf0354352a6b5f8a1942e0f8ddeea3a170eda78d060be8162ad60e94e4e91fbbdf0a7054785562040000005b6304000000090f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -44,7 +47,7 @@ internal class WalletManagerFactoryTest {
fun createStellarWalletManager() {
val data = "0108bb00000000000379200754414e47454d00020102800a322e3432642053444b0003410487d7bb51b189213e3cedc3fcfa3fc047b3b71b7805b5b215e14639b3a8ebb1952c9dd5ea4354441b6ada4e8b8327674bb102ddae69df55be69643a2c916edf650a04041e76310c618102ffff8a0101820407e30b0d830b54414e47454d2053444b008403584c4d86409a4bc2baf0e5836887da21167cf33458d5249d1a610bced0e31dc053f23729ed24d715912bf89e6804669430dfe396ed83274e0031f6803e2bdb8c041fa993413041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc6020e078212d58b2b9d0edc9c936830d10081cd38b90c31778c56dfb1171027e294e62040000003863040000002c0f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -55,7 +58,7 @@ internal class WalletManagerFactoryTest {
fun createCardanoWalletManager() {
val data = "0108bb00000000000502200754414e47454d00020102800a322e3432642053444b0003410402c1e39257d60583489da2d67d35d1cc2a1c005cc05c1021f44838edcaf25d5615cad7c9d11c2e23f5efa93e50904d33c88808d0e169060508df840992e31f4d0a04041e76310c658102ffff8a0101820407e30b0d830b54414e47454d2053444b00840743415244414e4f8640f24ef5c8c6eba0ff97560d5b013edb4a452594270db9647bd0a3543df8104dec75731d4db3ebe0fc493f2afee00195e560b51e3c41189b7c61ba7895d6434b9d3041045f16bd1d2eafe463e62a335a09e6b2bbcbd04452526885cb679fc4d27af1bd22f553c7deefb54fd3d4f361d14e6dc3f11b7d4ea183250a60720ebdf9e110cd2605086564323535313900080400000064070100090205dc60208a71161cfdf1e0a85d8e7ff372aa4a01136046292aceb5f9ad7ebdb98d3f60a86204000000646304000000000f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -66,7 +69,7 @@ internal class WalletManagerFactoryTest {
fun createXrpWalletManager() {
val data = "0108cb21000000002154200b534d4152542043415348000201028006322e31317200034104bdad63848f97c535da53cf8fd300d24fa33f0516d194aa78ec164a06994d00204bae243a424e316c6ec845e02d9b15eafae8c19018a926b0b7435e6e941cdadb0a0400007e210c5a81020028820407e30502830754414e47454d00840358525086400ed8734b877869722c7d0b37ffb154b9fef21c54bf2c6496feb1fb5c1fc28a2ac28e201dde84f27495fa7f08b3ca2be2fb4954bf0fe78af027d6cdc16c3eee923041048196aa4b410ac44a3b9cce18e7be226aea070acc83a9cf67540fac49af25129f6a538a28ad6341358e3c4f9963064f7e365372a651d374e5c23cdd37fd099bf2050a736563703235366b31000804000f4240070100090205dc604104d2b9fb288540d54e5b32ecaf0381cd571f97f6f1ecd036b66bb11aa52ffe9981110d883080e2e255c6b1640586f7765e6faa325d1340f49b56b83d9de56bc7ed6204000f42406304000000000f01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -77,7 +80,7 @@ internal class WalletManagerFactoryTest {
fun createBinanceWalletManager() {
val data = "0108BB00000000000015200754414E47454D00020102800A322E3432642053444B0003410446D4155890B08BE217F0B1FA7DCCB16138C24B3E825A27315D5E4BBD6CAF76A28C7902007052BC1347355A78D54BD73216C9431D555CED827B54FD9255EB3A830A04041E76310C658102FFFF8A0101820407E40410830B54414E47454D2053444B00840742494E414E4345864029F115878EDC7B0CB2A6F4A4009447DCB43BBE922D7629AEBD0C9A910AD1E3BF15AE409C4F579700951ED2FE4D775171A86CFA8E50009A05938CE210D6D4A2583041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A0070100604104E3F3BE3CE3D8284DB3BA073AD0291040093D83C11A277B905D5555C9EC41073E103F4D9D299EDEA8285C51C3356A8681A545618C174251B984DF841F49D2376F62040001869F6304000000010F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -88,7 +91,7 @@ internal class WalletManagerFactoryTest {
fun createBitcoinCashWalletManager() {
val data = "0108BB00000000000049200754414E47454D00020102800A322E3432642053444B00034104766A1586D164B436E5D420AED01FDAB41B2AE7EDF0C865D7AF1DA995D70AB297E5B94B761CFBB405084C21BC97C02B4A1EA9ED4F515576EAB4D83AD3A0DFAA8A0A04041E76310C618102FFFF8A0101820407E4041B830B54414E47454D2053444B00840342434886408058F0F628C2466B09ECEB13F2A8EFDD4558F5D2DBDA9BD0628EE8C8CC99A778FF0F1AECD35704B9F3518486EA5C1D20F9DFCBAA66184F4CCCD9282E2632882C3041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A0070100604104BE37CD5251C8999EDBBFC759D800EB41E4DCB718289601EB15819404E1B2F2ED90FE50C2A481D06EC790D1EF6184974EB655ABAE4BE56A6D1C9E1A17B1EFDF0262040001869A6304000000060F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -99,7 +102,7 @@ internal class WalletManagerFactoryTest {
fun createLitecoinWalletManager() {
val data = "0108BB00000000000023200754414E47454D00020102800A322E3432642053444B000341043539F86A40ADD04CE165764A761FD3E4D251028615D2A573B1C3AE652E60AFDBFAF02E3239E89EF2C43FA448A327557ADC5AF36376A0574570F6DBD20113514A0A04041E76310C618102FFFF8A0101820407E40414830B54414E47454D2053444B0084034C5443864004BDEAD0117544886346CB47F7CA84ABA8C34239502F23D28595A4B16CAD72F7DE506BA818B86A649C2BB945986D4574993B3B755B47CBEE31C4FB931F6748183041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A00701006041044A76C9A70422160F515F956D0F50C71BBBA4F9862A22913817D63F0B1EF7C2FAF512E1C91B1BE827560EFE24FB1652B47337E296C778DFB1014D080CDD35EF6562040001869D6304000000030F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
@ -107,13 +110,24 @@ internal class WalletManagerFactoryTest {
}
@Test
fun createLTezosWalletManager() {
fun createTezosWalletManager() {
val data = "0108BB00000000000080200754414E47454D00020102800A322E3432642053444B0003410436CFC5D0A11353AE6AFEEDC84A2D02B2635C044DEEE47F99913072B8D166D14E557230AC5FB5272F1A0E523332CCE1A744B51DB53102FF7D3FDE023DC3477C460A04041E76310C638102FFFF8A0101820407E40514830B54414E47454D2053444B00840554455A4F538640C752685B29333CFB0DB0A7347579A0AE763F2B5C4BB09FD68E0B81A06CD01EC51347001732815A3ECFFCD78DDE4E53877581B9E4914B069570629D0C40A771B93041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050865643235353139000804000186A0070100602098E0E504F3A5FDE704400302ABB0A2EFB0DF0F95C166C91D7F207DEDCE10CBA362040001869F6304000000010F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(SessionEnvironment(), responseApdu)
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
.isInstanceOf(TezosWalletManager::class.java)
}
@Test
fun createDucatusWalletManager() {
val data = "0108BB00000000000098200754414E47454D00020102800A322E3432642053444B000341041B5FD7C590938E836B388B996AE451FDED54F625FF2CF05E26E5AADF6F690AEF125E3D0F23CB6B8D1F78040DF6F71B40D098F2D8BE504DDEE2E1F99BEADD90500A04041E76310C618102FFFF8A0101820407E40515830B54414E47454D2053444B00840344554386401B453C10A092A3448FA83CAFD4FC3D7EB5EA1BBBDD6A020CAAC8CED36BB2661F41EF0B0C7418214F670DFE1200DCE18597158119BEF6CC52A4FF3B7E021A53B93041045F16BD1D2EAFE463E62A335A09E6B2BBCBD04452526885CB679FC4D27AF1BD22F553C7DEEFB54FD3D4F361D14E6DC3F11B7D4EA183250A60720EBDF9E110CD26050A736563703235366B31000804000186A007010060410485D520C8B907F0BC5E03FCBBAC212CCD270764BBFF4990A28653A2FB0D656C342DF143C4D52C43582289E20A81D5D014C1384A1FFFEA1D121903AD7ED35A01EA62040001869D6304000000030F01009000"
val responseApdu = ResponseApdu(data.hexToBytes())
val card = ReadCommand().deserialize(sessionEnvironment, responseApdu)
val walletManager = WalletManagerFactory.makeWalletManager(card)
Truth.assertThat(walletManager)
.isInstanceOf(DucatusWalletManager::class.java)
}
}

View file

@ -11,8 +11,9 @@ buildscript {
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'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.0.0-beta04'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.2.0'
classpath 'com.google.firebase:perf-plugin:1.3.1'
classpath 'com.squareup.sqldelight:gradle-plugin:1.4.0'
}
}
@ -21,6 +22,7 @@ allprojects {
google()
jcenter()
maven { url 'https://jitpack.io' }
maven { url "https://api.bitbucket.org/2.0/repositories/tangem/maven_repository/src/releases" }
}
}

View file

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

View file

@ -47,7 +47,7 @@ class LocalStorage
artworks = HashMap()
}
if (artworks.count() < 44) {
if (artworks.count() < 45) {
// forceSave=true only on the last one
putResourceArtworkToCatalog(R.drawable.card_default, false)
putResourceArtworkToCatalog(R.drawable.card_default_nft, false)
@ -90,6 +90,7 @@ class LocalStorage
putResourceArtworkToCatalog(R.drawable.card_tg061, false)
putResourceArtworkToCatalog(R.drawable.card_tg062, false)
putResourceArtworkToCatalog(R.drawable.card_tg063, false)
putResourceArtworkToCatalog(R.drawable.card_tg073, false)
putResourceArtworkToCatalog(R.drawable.card_tgslix, false)
putResourceArtworkToCatalog(R.drawable.card_bc00, false)
putResourceArtworkToCatalog(R.drawable.card_ff32, true)
@ -305,6 +306,7 @@ class LocalStorage
card.batch == "0050" -> R.drawable.card_tg061
card.batch == "0051" -> R.drawable.card_tg062
card.batch == "0052" -> R.drawable.card_tg063
card.batch == "0060" -> R.drawable.card_tg073
card.batch == "FF32" -> R.drawable.card_ff32
else -> null

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View file

@ -1 +1 @@
include ':app', ':tangem-sdk-old', ':server-android', ':tangem-card-old', ':tangem-core', ':tangem-sdk', ':tangem-devkit', ':blockchain', ':blockchain-demo'
include ':app', ':tangem-sdk-old', ':server-android', ':tangem-card-old', ':blockchain', ':blockchain-demo'

View file

@ -1 +0,0 @@
/build

View file

@ -1,59 +0,0 @@
apply plugin: "kotlin"
apply plugin: 'org.jetbrains.dokka'
apply plugin: 'com.github.dcendents.android-maven'
apply from: '../dependencies.gradle'
apply from: '../jitpack.gradle'
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'
// 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"
}
sourceCompatibility = "8"
targetCompatibility = "8"
buildscript {
ext.dokka_version = '0.10.0'
repositories {
mavenCentral()
jcenter()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
}
}
repositories {
mavenCentral()
jcenter()
}
compileKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}
task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
outputFormat = 'markdown'
}

View file

@ -1,14 +0,0 @@
package com.tangem
import com.tangem.common.extensions.CardType
import java.util.*
/**
* Filter that can be used to limit cards that can be interacted with in TangemSdk.
*
* @property allowedCardTypes Type of cards that are allowed to be interacted with in TangemSdk.
*/
data class CardFilter(
var allowedCardTypes: EnumSet<CardType> = EnumSet.allOf(CardType::class.java)
)

View file

@ -1,32 +0,0 @@
package com.tangem
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
/**
* Allows interaction between the phone or any other terminal and Tangem card.
*
* Its default implementation, NfcCardReader, is in our tangem-sdk module.
*/
interface CardReader {
/**
* Sends data to the card and receives the reply.
*
* @param apdu Data to be sent. [CommandApdu] serializes it to a [ByteArray]
* @param callback Returns response from the card,
* [ResponseApdu] Allows to convert raw data to [Tlv]
*/
fun transceiveApdu(apdu: CommandApdu, callback: (response: CompletionResult<ResponseApdu>) -> Unit)
/**
* Signals to [CardReader] to become ready to transceive data.
*/
fun openSession()
/**
* Signals to [CardReader] that no further NFC transition is expected.
*/
fun closeSession()
}

View file

@ -1,260 +0,0 @@
package com.tangem
import com.tangem.commands.Card
import com.tangem.commands.CommandResponse
import com.tangem.commands.OpenSessionCommand
import com.tangem.commands.ReadCommand
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.getType
import com.tangem.crypto.EncryptionHelper
import com.tangem.crypto.FastEncryptionHelper
import com.tangem.crypto.StrongEncryptionHelper
import com.tangem.crypto.pbkdf2Hash
/**
* Basic interface for running tasks and [com.tangem.commands.Command] in a [CardSession]
*/
interface CardSessionRunnable<T : CommandResponse> {
val performPreflightRead: Boolean
/**
* The starting point for custom business logic.
* Implement this interface and use [TangemSdk.startSessionWithRunnable] to run.
* @param session run commands in this [CardSession].
* @param callback trigger the callback to complete the task.
*/
fun run(session: CardSession, callback: (result: CompletionResult<T>) -> Unit)
}
/**
* Allows interaction with Tangem cards. Should be opened before sending commands.
*
* @property environment
* @property reader is an interface that is responsible for NFC connection and
* transfer of data to and from the Tangem Card.
* @property viewDelegate is an interface that allows interaction with users and shows relevant UI.
* @property cardId ID, Unique Tangem card ID number. If not null, the SDK will check that you the card
* with which you tapped a phone has this [cardId] and SDK will return
* the [TangemSdkError.WrongCardNumber] otherwise.
* @property initialMessage A custom description that will be shown at the beginning of the NFC session.
* If null, a default header and text body will be used.
*/
class CardSession(
val environment: SessionEnvironment,
private val reader: CardReader,
val viewDelegate: SessionViewDelegate,
private var cardId: String? = null,
private val initialMessage: Message? = null
) {
private val tag = this.javaClass.simpleName
/**
* True if some operation is still in progress.
*/
private var isBusy = false
private var performPreflightRead = true
/**
* This metod starts a card session, performs preflight [ReadCommand],
* invokes [CardSessionRunnable.run] and closes the session.
* @param runnable [CardSessionRunnable] that will be performed in the session.
* @param callback will be triggered with a [CompletionResult] of a session.
*/
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))
return@start
}
if (runnable is ReadCommand) {
callback(CompletionResult.Success(environment.card as R))
return@start
}
runnable.run(this) { result ->
when (result) {
is CompletionResult.Success -> stop()
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.ExtendedLengthNotSupported) {
if (session.environment.terminalKeys != null) {
session.environment.terminalKeys = null
startWithRunnable(runnable, callback)
return@run
}
}
stopWithError(result.error)
}
}
callback(result)
}
}
}
/**
* Starts a card session and performs preflight [ReadCommand].
* @param callback: callback with the card session. Can contain [TangemSdkError] if something goes wrong.
*/
fun start(callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
try {
startSession()
} catch (error: TangemSdkError) {
callback(this, error)
}
if (!performPreflightRead) {
callback(this, null)
return
}
preflightRead() { result ->
when (result) {
is CompletionResult.Failure -> {
callback(this, result.error)
stopWithError(result.error)
}
is CompletionResult.Success -> {
callback(this, null)
}
}
}
}
private fun startSession() {
if (isBusy) throw TangemSdkError.Busy()
isBusy = true
viewDelegate.onNfcSessionStarted(cardId, initialMessage)
reader.openSession()
}
private fun preflightRead(callback: (result: CompletionResult<Card>) -> Unit) {
val readCommand = ReadCommand()
readCommand.run(this) { result ->
when (result) {
is CompletionResult.Failure -> {
tryHandleError(result.error) { handleErrorResult ->
when (handleErrorResult) {
is CompletionResult.Success -> preflightRead(callback)
is CompletionResult.Failure -> {
stopWithError(result.error)
callback(CompletionResult.Failure(result.error))
}
}
}
}
is CompletionResult.Success -> {
val receivedCardId = result.data.cardId
if (cardId != null && receivedCardId != cardId) {
stopWithError(TangemSdkError.WrongCardNumber())
callback(CompletionResult.Failure(TangemSdkError.WrongCardNumber()))
return@run
}
val allowedCardTypes = environment.cardFilter.allowedCardTypes
if (!allowedCardTypes.contains(result.data.getType())) {
stopWithError(TangemSdkError.WrongCardType())
callback(CompletionResult.Failure(TangemSdkError.WrongCardType()))
return@run
}
environment.card = result.data
cardId = receivedCardId
callback(CompletionResult.Success(result.data))
}
}
}
}
/**
* Stops the current session with the text message.
* @param message If null, the default message will be shown.
*/
private fun stop(message: Message? = null) {
reader.closeSession()
viewDelegate.onNfcSessionCompleted(message)
isBusy = false
}
/**
* Stops the current session on error.
* @param error An error that will be shown.
*/
private fun stopWithError(error: TangemSdkError) {
if (!isBusy) return
reader.closeSession()
isBusy = false
val errorMessage = if (error is TangemSdkError) {
"${error::class.simpleName}: ${error.code}"
} else {
error.localizedMessage
}
if (error !is TangemSdkError.UserCancelled) {
Log.e(tag, "Finishing with error: $errorMessage")
viewDelegate.onError(error)
} else {
Log.i(tag, "User cancelled NFC session")
}
}
fun send(apdu: CommandApdu, callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
reader.transceiveApdu(apdu, callback)
}
private fun tryHandleError(
error: TangemSdkError, callback: (result: CompletionResult<Boolean>) -> Unit) {
when (error) {
is TangemSdkError.NeedEncryption -> {
Log.i(tag, "Establishing encryption")
when (environment.encryptionMode) {
EncryptionMode.NONE -> {
environment.encryptionKey = null
environment.encryptionMode = EncryptionMode.FAST
}
EncryptionMode.FAST -> {
environment.encryptionKey = null
environment.encryptionMode = EncryptionMode.STRONG
}
EncryptionMode.STRONG -> {
Log.e(tag, "Encryption doesn't work")
callback(CompletionResult.Failure(TangemSdkError.NeedEncryption()))
}
}
return establishEncryption(callback)
}
else -> callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
}
}
private fun establishEncryption(callback: (result: CompletionResult<Boolean>) -> Unit) {
val encryptionHelper: EncryptionHelper =
if (environment.encryptionMode == EncryptionMode.STRONG) {
StrongEncryptionHelper()
} else {
FastEncryptionHelper()
}
val openSesssionCommand = OpenSessionCommand(encryptionHelper.keyA)
openSesssionCommand.run(this) { result ->
when (result) {
is CompletionResult.Success -> {
val uid = result.data.uid
val protocolKey = environment.pin1.pbkdf2Hash(uid, 50)
val secret = encryptionHelper.generateSecret(result.data.sessionKeyB)
val sessionKey = (secret + protocolKey).calculateSha256()
environment.encryptionKey = sessionKey
callback(CompletionResult.Success(true))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}

View file

@ -1,41 +0,0 @@
package com.tangem
class Config(
/**
* Enables or disables Linked Terminal feature.
App can optionally generate ECDSA key pair Terminal_PrivateKey / Terminal_PublicKey.
And then submit Terminal_PublicKey to the card in any SIGN command.
Once SIGN is successfully executed by COS (Card Operation System),
including PIN2 verification and/or completion of security delay, the submitted
Terminal_PublicKey key is stored by COS. After that, the App instance is deemed trusted
by COS and COS will allow skipping security delay for subsequent SIGN operations
thus improving convenience without sacrificing security.
In order to skip security delay, App should use Terminal_PrivateKey to compute the signature
of the data being submitted to SIGN command for signing and transmit this signature in
Terminal_Transaction_Signature parameter in the same SIGN command. COS will verify
the correctness of Terminal_Transaction_Signature using previously stored Terminal_PublicKey
and, if correct, will skip security delay for the current SIGN operation.
*/
var linkedTerminal: Boolean = true,
/**
* If not null, it will be used to validate Issuer data and issuer extra data.
* If null, issuerPublicKey from current card will be used.
*/
var issuerPublicKey: ByteArray? = null,
/**
* Level of encryption used in communication with a Tangem Card.
*/
var encryptionMode: EncryptionMode = EncryptionMode.NONE,
/**
* Filter that can be used to limit cards that can be interacted with in TangemSdk.
*/
val cardFilter: CardFilter = CardFilter(),
var handleErrors: Boolean = true
)

View file

@ -1,34 +0,0 @@
package com.tangem
object Log {
private var loggerInstance: LoggerInterface? = null
fun i(logTag: String, message: String) {
loggerInstance?.i(logTag, message)
}
fun e(logTag: String, message: String) {
loggerInstance?.e(logTag, message)
}
fun v(logTag: String, message: String) {
loggerInstance?.v(logTag, message)
}
fun setLogger(logger: LoggerInterface) {
loggerInstance = logger
}
}
/**
* Interface for logging events within the SDK.
*
* It allows to use Android logger or to choose another.
*/
interface LoggerInterface {
fun i(logTag: String, message: String)
fun e(logTag: String, message: String)
fun v(logTag: String, message: String)
}

View file

@ -1,55 +0,0 @@
package com.tangem
import com.tangem.commands.Card
import com.tangem.commands.EllipticCurve
import com.tangem.common.extensions.calculateSha256
import com.tangem.crypto.CryptoUtils.generatePublicKey
/**
* Contains data relating to a Tangem card. It is used in constructing all the commands,
* and commands can return modified [SessionEnvironment].
*
* @property card Current card, read by preflight [com.tangem.commands.ReadCommand].
* @property terminalKeys generated terminal keys used in Linked Terminal feature.
*/
data class SessionEnvironment(
var pin1: ByteArray = DEFAULT_PIN.calculateSha256(),
var pin2: ByteArray = DEFAULT_PIN2.calculateSha256(),
var card: Card? = null,
var terminalKeys: KeyPair? = null,
var encryptionMode: EncryptionMode = EncryptionMode.NONE,
var encryptionKey: ByteArray? = null,
var cvc: ByteArray? = null,
var cardFilter: CardFilter = CardFilter(),
val handleErrors: Boolean = true
) {
fun setPin1(pin1: String) {
this.pin1 = pin1.calculateSha256()
}
fun setPin2(pin2: String) {
this.pin2 = pin2.calculateSha256()
}
companion object {
const val DEFAULT_PIN = "000000"
const val DEFAULT_PIN2 = "000"
}
}
/**
* All possible encryption modes.
*/
enum class EncryptionMode(val code: Byte) {
NONE(0x0),
FAST(0x1),
STRONG(0x2)
}
class KeyPair(val publicKey: ByteArray, val privateKey: ByteArray) {
constructor(privateKey: ByteArray, curve: EllipticCurve = EllipticCurve.Secp256k1) :
this(generatePublicKey(privateKey, curve), privateKey)
}

View file

@ -1,55 +0,0 @@
package com.tangem
import com.tangem.common.CompletionResult
/**
* Allows interaction with users and shows visual elements.
*
* Its default implementation, DefaultCardManagerDelegate, is in our tangem-sdk module.
*/
interface SessionViewDelegate {
/**
* It is called when user is expected to scan a Tangem Card with an Android device.
*/
fun onNfcSessionStarted(cardId: String?, message: Message? = null)
/**
* It is called when security delay is triggered by the card.
* A user is expected to hold the card until the security delay is over.
*/
fun onSecurityDelay(ms: Int, totalDurationSeconds: Int)
/**
* It is called when long tasks are performed.
* A user is expected to hold the card until the task is complete.
*/
fun onDelay(total: Int, current: Int, step: Int)
/**
* It is called when user takes the card away from the Android device during the scanning
* (for example when security delay is in progress) and the TagLostException is received.
*/
fun onTagLost()
/**
* It is called when NFC session was completed and a user can take the card away from the Android device.
*/
fun onNfcSessionCompleted(message: Message? = null)
/**
* It is called when some error occur during NFC session.
*/
fun onError(error: TangemSdkError)
/**
* It is called when a user is expected to enter pin code.
*/
fun onPinRequested(callback: (result: CompletionResult<String>) -> Unit)
}
/**
* Wrapper for a message that can be shown to user after a start of NFC session.
*/
data class Message(val header: String? = null, val body: String? = null)

View file

@ -1,398 +0,0 @@
package com.tangem
import com.tangem.commands.*
import com.tangem.commands.personalization.DepersonalizeCommand
import com.tangem.commands.personalization.DepersonalizeResponse
import com.tangem.commands.personalization.PersonalizeCommand
import com.tangem.commands.personalization.entities.Acquirer
import com.tangem.commands.personalization.entities.CardConfig
import com.tangem.commands.personalization.entities.Issuer
import com.tangem.commands.personalization.entities.Manufacturer
import com.tangem.common.CompletionResult
import com.tangem.common.TerminalKeysService
import com.tangem.crypto.CryptoUtils
import com.tangem.tasks.CreateWalletTask
import com.tangem.tasks.ScanTask
/**
* The main interface of Tangem SDK that allows your app to communicate with Tangem cards.
*
* @property reader is an interface that is responsible for NFC connection and
* transfer of data to and from the Tangem Card.
* Its default implementation, NfcCardReader, is in our tangem-sdk module.
* @property viewDelegate An interface that allows interaction with users and shows relevant UI.
* Its default implementation, DefaultCardSessionViewDelegate, is in our tangem-sdk module.
* @property config allows to change a number of parameters for communication with Tangem cards.
* Do not change the default values unless you know what you are doing.
*/
class TangemSdk(
private val reader: CardReader,
private val viewDelegate: SessionViewDelegate,
var config: Config = Config()
) {
private var terminalKeysService: TerminalKeysService? = null
init {
CryptoUtils.initCrypto()
}
/**
* This method launches a [ScanTask] on a new thread.
*
* To start using any card, you first need to read it using the scanCard() method.
* This method launches an NFC session, and once its connected with the card,
* it obtains the card data. Optionally, if the card contains a wallet (private and public key pair),
* it proves that the wallet owns a private key that corresponds to a public one.
*
* @param callback is triggered on the completion of the [ScanTask] and provides card response
* in the form of [Card] if the task was performed successfully or [TangemSdkError] in case of an error.
*/
fun scanCard(initialMessage: Message? = null, callback: (result: CompletionResult<Card>) -> Unit) {
startSessionWithRunnable(ScanTask(), null, initialMessage, callback)
}
/**
* This method launches a [SignCommand] on a new thread.
*
* It allows you to sign one or multiple hashes.
* Simultaneous signing of array of hashes in a single [SignCommand] is required to support
* Bitcoin-type multi-input blockchains (UTXO).
* The [SignCommand] will return a corresponding array of signatures.
*
* Please note that Tangem cards usually protect the signing with a security delay
* that may last up to 90 seconds, depending on a card.
* It is for [SessionViewDelegate] to notify users of security delay.
*
* @param hashes Array of transaction hashes. It can be from one or up to ten hashes of the same length.
* @param cardId CID, Unique Tangem card ID number
* @param callback is triggered on the completion of the [SignCommand] and provides card response
* 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? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<SignResponse>) -> Unit) {
startSessionWithRunnable(SignCommand(hashes), cardId, initialMessage, callback)
}
/**
* This method launches a [ReadIssuerDataCommand] on a new thread.
* This command returns 512-byte Issuer Data field and its issuers signature.
* Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. For example, this field may contain information about
* wallet balance signed by the issuer or additional issuers attestation data.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [ReadIssuerDataCommand] and provides
* card response in the form of [ReadIssuerDataResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun readIssuerData(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<ReadIssuerDataResponse>) -> Unit) {
startSessionWithRunnable(ReadIssuerDataCommand(config.issuerPublicKey), cardId, initialMessage, callback)
}
/**
* This method launches a [ReadIssuerExtraDataCommand] on a new thread.
*
* This command retrieves Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. . For example, this field may contain photo or
* biometric information for ID card product. Because of the large size of Issuer_Extra_Data,
* a series of these commands have to be executed to read the entire Issuer_Extra_Data.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [ReadIssuerExtraDataCommand] and provides
* card response in the form of [ReadIssuerExtraDataResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun readIssuerExtraData(cardId: String? = null,
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit) {
startSessionWithRunnable(ReadIssuerExtraDataCommand(config.issuerPublicKey), cardId, null, callback)
}
/**
* This method launches a [WriteIssuerDataCommand] on a new thread.
*
* This command writes 512-byte Issuer Data field and its issuers signature.
* Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. For example, this field may contain information about
* wallet balance signed by the issuer or additional issuers attestation data.
*
* @param cardId CID, Unique Tangem card ID number.
* @param issuerData Data provided by issuer.
* @param issuerDataSignature Issuers signature of [issuerData] with Issuer Data Private Key.
* @param issuerDataCounter An optional counter that protect issuer data against replay attack.
* @param callback is triggered on the completion of the [WriteIssuerDataCommand] and provides
* card response in the form of [WriteIssuerDataResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun writeIssuerData(cardId: String? = null,
issuerData: ByteArray,
issuerDataSignature: ByteArray,
issuerDataCounter: Int? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit) {
val command = WriteIssuerDataCommand(
issuerData,
issuerDataSignature,
issuerDataCounter,
config.issuerPublicKey
)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
/**
* This method launches a [WriteIssuerExtraDataCommand] on a new thread.
*
* This command writes Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS.
* The issuer defines purpose of use, format and payload of Issuer Data.
* For example, this field may contain a photo or biometric information for ID card products.
* Because of the large size of IssuerExtraData, a series of these commands have to be executed
* to write entire IssuerExtraData.
*
* @param cardId CID, Unique Tangem card ID number.
* @param issuerData Data provided by issuer.
* @param startingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and
* Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData].
* @param finalizingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay
* andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]).
* @param issuerDataCounter An optional counter that protect issuer data against replay attack.
* @param callback is triggered on the completion of the [WriteIssuerExtraDataCommand] and provides
* card response in the form of [WriteIssuerDataResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun writeIssuerExtraData(cardId: String? = null,
issuerData: ByteArray,
startingSignature: ByteArray,
finalizingSignature: ByteArray,
issuerDataCounter: Int? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit) {
val command = WriteIssuerExtraDataCommand(
issuerData,
startingSignature, finalizingSignature,
issuerDataCounter,
config.issuerPublicKey
)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
/**
* This method launches a [WriteUserDataCommand] on a new thread, writing UserData and UserCounter fields.
*
* User_Data is never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and its payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* The initial value of User_Counter can be set by an App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*
* Writing of UserCounter and UserData is protected only by PIN1.
*/
fun writeUserData(
cardId: String? = null,
userData: ByteArray? = null,
userCounter: Int? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<WriteUserDataResponse>) -> Unit
) {
val command = WriteUserDataCommand(userData = userData,userCounter = userCounter)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
/**
* This method launches a [WriteUserDataCommand] on a new thread,
* writing UserProtectedData and UserProtectedCounter fields.
*
* User_ProtectedData is never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and its payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* The initial value of User_ProtectedCounter can be set by an App and increased on every signing
* of a new transaction (on SIGN command that calculate new signatures). The App defines the purpose of use.
* For example, this fields may contain blockchain nonce value.
*
* UserProtectedCounter and UserProtectedData require PIN2 for confirmation.
*/
fun writeProtectedUserData(
cardId: String? = null,
userProtectedData: ByteArray? = null,
userProtectedCounter: Int? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<WriteUserDataResponse>) -> Unit
) {
val command = WriteUserDataCommand(
userProtectedData = userProtectedData, userProtectedCounter = userProtectedCounter
)
startSessionWithRunnable(command, cardId, initialMessage, callback)
}
/**
* This method launches a [ReadUserDataCommand] on a new thread.
*
* This command returns two up to 512-byte User_Data, User_Protected_Data and two counters User_Counter and
* User_Protected_Counter fields.
* User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and it's payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [ReadUserDataCommand] and provides
* card response in the form of [ReadUserDataResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun readUserData(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<ReadUserDataResponse>) -> Unit) {
startSessionWithRunnable(ReadUserDataCommand(), cardId, initialMessage, callback)
}
/**
* This method launches a [CreateWalletTask] on a new thread.
*
* This this will create a new wallet on the card having Empty state with [CreateWalletCommand]
* and will check the success of the operation by performing [CheckWalletCommand].
* A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card.
* App will need to obtain Wallet_PublicKey from the [CreateWalletResponse] or from the
* response of [ReadCommand] and then transform it into an address of corresponding
* blockchain wallet according to a specific blockchain algorithm.
* WalletPrivateKey is never revealed by the card and will be used by [SignCommand] and [CheckWalletCommand].
* RemainingSignature is set to MaxSignatures.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [CreateWalletTask] and provides
* card response in the form of [CreateWalletResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun createWallet(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
startSessionWithRunnable(CreateWalletTask(), cardId, initialMessage, callback)
}
/**
* This method launches a [PurgeWalletCommand] on a new thread.
*
* This command deletes all wallet data. If IsReusable flag is enabled during personalization,
* or [CreateWalletCommand].
* If IsReusable flag is disabled, the card switches to Purged state.
* Purged state is final, it makes the card useless.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [PurgeWalletCommand] and provides
* card response in the form of [PurgeWalletResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun purgeWallet(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<PurgeWalletResponse>) -> Unit) {
startSessionWithRunnable(PurgeWalletCommand(), cardId, initialMessage, callback)
}
/**
* Command available on SDK cards only
*
* This method launches a [DepersonalizeCommand] on a new thread.
*
* This command resets card to initial state,
* erasing all data written during personalization and usage.
*
* @param cardId CID, Unique Tangem card ID number.
* @param callback is triggered on the completion of the [DepersonalizeCommand] and provides
* card response in the form of [DepersonalizeResponse] if the task was performed successfully
* or [TangemSdkError] in case of an error.
* */
fun depersonalize(cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<DepersonalizeResponse>) -> Unit) {
startSessionWithRunnable(DepersonalizeCommand(), cardId, initialMessage, callback)
}
/**
* Command available on SDK cards only
*
* This method launches a [PersonalizeCommand] on a new thread.
*
* Personalization is an initialization procedure, required before starting using a card.
* During this procedure a card setting is set up.
* During this procedure all data exchange is encrypted.
* @param config is a configuration file with all the card settings that are written on the card
* during personalization.
* @param issuer Issuer is a third-party team or company wishing to use Tangem cards.
* @param manufacturer Tangem Card Manufacturer.
* @param acquirer Acquirer is a trusted third-party company that operates proprietary
* (non-EMV) POS terminal infrastructure and transaction processing back-end.
* @param callback is triggered on the completion of the [PersonalizeCommand] and provides
* card response in the form of [Card] if the command was performed successfully
* or [TangemSdkError] in case of an error.
*/
fun personalize(config: CardConfig,
issuer: Issuer, manufacturer: Manufacturer, acquirer: Acquirer? = null,
initialMessage: Message? = null,
callback: (result: CompletionResult<Card>) -> Unit) {
val command = PersonalizeCommand(config, issuer, manufacturer, acquirer)
startSessionWithRunnable(command, null, initialMessage, callback)
}
/**
* Allows running a custom bunch of commands in one [CardSession] by creating a custom task.
* [TangemSdk] will start a card session, perform preflight [ReadCommand],
* invoke [CardSessionRunnable.run] and close the session.
* You can find the current card in the [CardSession.environment].
* @runnable: A custom task, adopting [CardSessionRunnable] protocol
* @cardId: CID, Unique Tangem card ID number. If not null, the SDK will check that you the card
* with which you tapped a phone has this [cardId] and SDK will return
* the [TangemSdkError.WrongCardNumber] otherwise.
* @initialMessage: A custom description that shows at the beginning of the NFC session.
* If null, default message will be used.
* @callback: Standard [TangemSdk] callback.
*/
fun <T : CommandResponse> startSessionWithRunnable(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
callback: (result: CompletionResult<T>) -> Unit) {
val cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId, initialMessage)
Thread().run { cardSession.startWithRunnable(runnable, callback) }
}
/**
* Allows running a custom bunch of commands in one [CardSession] with lightweight closure syntax.
* Tangem SDK will start a card sesion and perform preflight [ReadCommand].
* @cardId: CID, Unique Tangem card ID number. If not null, the SDK will check that you the card
* with which you tapped a phone has this [cardId] and SDK will return
* the [TangemSdkError.WrongCardNumber] otherwise.
* @initialMessage: A custom description that shows at the beginning of the NFC session.
* If null, default message will be used.
* @callback: At first, you should check that the [TangemSdkError] is not null,
* then you can use the [CardSession] to interact with a card.
*/
fun startSession(cardId: String? = null, initialMessage: Message? = null,
callback: (session: CardSession, error: TangemSdkError?) -> Unit) {
val cardSession = CardSession(buildEnvironment(), reader, viewDelegate, cardId, initialMessage)
Thread().run { cardSession.start(callback) }
}
/**
* Allows to set a particular [TerminalKeysService] to retrieve terminal keys.
* Default implementation is provided in tangem-sdk module: [TerminalKeysStorage].
*/
fun setTerminalKeysService(terminalKeysService: TerminalKeysService) {
this.terminalKeysService = terminalKeysService
}
private fun buildEnvironment(): SessionEnvironment {
val terminalKeys = if (config.linkedTerminal) terminalKeysService?.getKeys() else null
return SessionEnvironment(
terminalKeys = terminalKeys,
cardFilter = config.cardFilter,
handleErrors = config.handleErrors
)
}
companion object
}

View file

@ -1,162 +0,0 @@
package com.tangem
import com.tangem.commands.Card
import com.tangem.commands.ReadCommand
import com.tangem.common.apdu.StatusWord
import com.tangem.tasks.ScanTask
/**
* An error class that represent typical errors that may occur when performing Tangem SDK tasks.
* Errors are propagated back to the caller in callbacks.
*/
sealed class TangemSdkError(val code: Int) : Exception(code.toString()) {
/**
* This error is returned when Android NFC reader loses a tag
* (e.g. a user detaches card from the phone's NFC module) while the NFC session is in progress.
*/
class TagLost : TangemSdkError(10001)
/**
* This error is returned when NFC driver on an Android device does not support sending more than 261 bytes.
*/
class ExtendedLengthNotSupported : TangemSdkError(10002)
class SerializeCommandError : TangemSdkError(20001)
class DeserializeApduFailed : TangemSdkError(20002)
class EncodingFailedTypeMismatch : TangemSdkError(20003)
class EncodingFailed : TangemSdkError(20004)
class DecodingFailedMissingTag : TangemSdkError(20005)
class DecodingFailedTypeMismatch : TangemSdkError(20006)
class DecodingFailed : TangemSdkError(20007)
/**
* This error is returned when unknown [StatusWord] is received from a card.
*/
class UnknownStatus : TangemSdkError(30001)
/**
* This error is returned when a card's reply is [StatusWord.ErrorProcessingCommand].
* The card sends this status in case of internal card error.
*/
class ErrorProcessingCommand : TangemSdkError(30002)
/**
* This error is returned when a card's reply is [StatusWord.InvalidState].
* The card sends this status when command can not be executed in the current state of a card.
*/
class InvalidState : TangemSdkError(30003)
/**
* This error is returned when a card's reply is [StatusWord.InsNotSupported].
* The card sends this status when the card cannot process the [com.tangem.common.apdu.Instruction].
*/
class InsNotSupported : TangemSdkError(30004)
/**
* This error is returned when a card's reply is [StatusWord.InvalidParams].
* The card sends this status when there are wrong or not sufficient parameters in TLV request,
* or wrong PIN1/PIN2.
* The error may be caused, for example, by wrong parameters of the [Task], [CommandSerializer],
* mapping or serialization errors.
*/
class InvalidParams : TangemSdkError(30005)
/**
* This error is returned when a card's reply is [StatusWord.NeedEncryption]
* and the encryption was not established by TangemSdk.
*/
class NeedEncryption : TangemSdkError(30006)
//Personalization Errors
class AlreadyPersonalized : TangemSdkError(40101)
//Depersonalization Errors
class CannotBeDepersonalized : TangemSdkError(40201)
//Read Errors
class Pin1Required : TangemSdkError(40401)
//CreateWallet Errors
class AlreadyCreated : TangemSdkError(40501)
//PurgeWallet Errors
class PurgeWalletProhibited : TangemSdkError(40601)
//SetPin Errors
class Pin1CannotBeChanged : TangemSdkError(40801)
class Pin2CannotBeChanged : TangemSdkError(40802)
class Pin1CannotBeDefault : TangemSdkError(40803)
//Sign Errors
class NoRemainingSignatures : TangemSdkError(40901)
/**
* This error is returned when a [com.tangem.commands.SignCommand]
* receives only empty hashes for signature.
*/
class EmptyHashes : TangemSdkError(40902)
/**
* This error is returned when a [com.tangem.commands.SignCommand]
* receives hashes of different lengths for signature.
*/
class HashSizeMustBeEqual : TangemSdkError(40903)
class CardIsEmpty : TangemSdkError(40904)
class SignHashesNotAvailable : TangemSdkError(40905)
/**
* 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 TooManyHashesInOneTransaction : TangemSdkError(40906)
//Write Extra Issuer Data Errors
class ExendedDataSizeTooLarge : TangemSdkError(41101)
//General Errors
class NotPersonalized() : TangemSdkError(40001)
class NotActivated : TangemSdkError(40002)
class CardIsPurged : TangemSdkError(40003)
class Pin2OrCvcRequired : TangemSdkError(40004)
/**
* This error is returned when a [Task] checks unsuccessfully either
* a card's ability to sign with its private key, or the validity of issuer data.
*/
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.
*/
class MissingCounter : TangemSdkError(40007)
class OverwritingDataIsProhibited : TangemSdkError(40008)
class DataCannotBeWritten : TangemSdkError(40009)
class MissingIssuerPubicKey : TangemSdkError(40010)
//SDK Errors
class UnknownError: TangemSdkError(50001)
/**
* This error is returned when a user manually closes NFC Reading Bottom Sheet Dialog.
*/
class UserCancelled: TangemSdkError(50002)
/**
* This error is returned when [com.tangem.TangemSdk] was called with a new [Task],
* while a previous [Task] is still in progress.
*/
class Busy : TangemSdkError(50003)
/**
* This error is returned when a task (such as [ScanTask]) requires that [ReadCommand]
* is executed before performing other commands.
*/
class MissingPreflightRead : TangemSdkError(50004)
/**
* This error is returned when a [Task] expects a user to use a particular card,
* but the user tries to use a different card.
*/
class WrongCardNumber : TangemSdkError(50005)
/**
* This error is returned when a user scans a card of a [com.tangem.common.extensions.CardType]
* that is not specified in [Config.cardFilter].
*/
class WrongCardType : TangemSdkError(50006)
/**
* This error is returned when a [ScanTask] returns a [Card] without some of the essential fields.
*/
class CardError : TangemSdkError(50007)
}

View file

@ -1,110 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.CryptoUtils
/**
* Deserialized response from the Tangem card after [CheckWalletCommand].
*
* @property cardId Unique Tangem card ID number
* @property salt Random salt generated by the card.
* @property walletSignature Challenge and salt signed with the wallet private key.
*/
class CheckWalletResponse(
val cardId: String,
val salt: ByteArray,
val walletSignature: ByteArray
) : CommandResponse {
fun verify(curve: EllipticCurve, publicKey: ByteArray, challenge: ByteArray): Boolean {
return CryptoUtils.verify(
publicKey,
challenge + salt,
walletSignature,
curve)
}
}
/**
* This command proves that the wallet private key from the card corresponds to the wallet public key.
* Standard challenge/response scheme is used.
*
* @property pin1 Hashed users pin 1 code to access the card. Default unhashed value: 000000.
* @property cardId Unique Tangem card ID number
* @property challenge Random challenge generated by application
*/
class CheckWalletCommand(
private val curve: EllipticCurve, private val publicKey: ByteArray
) : Command<CheckWalletResponse>() {
private val challenge = CryptoUtils.generateRandomBytes(16)
override fun run(session: CardSession, callback: (result: CompletionResult<CheckWalletResponse>) -> Unit) {
super.run(session) { result ->
when (result) {
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
is CompletionResult.Success -> {
val verified = result.data.verify(
curve,
publicKey,
challenge
)
if (verified) {
callback(CompletionResult.Success(result.data))
} else {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
}
}
}
}
}
override fun performPreCheck(
session: CardSession,
callback: (result: CompletionResult<CheckWalletResponse>) -> Unit
): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
return false
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Challenge, challenge)
return CommandApdu(
Instruction.CheckWallet, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): CheckWalletResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return CheckWalletResponse(
cardId = decoder.decode(TlvTag.CardId),
salt = decoder.decode(TlvTag.Salt),
walletSignature = decoder.decode(TlvTag.Signature)
)
}
}

View file

@ -1,138 +0,0 @@
package com.tangem.commands
import com.tangem.*
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.apdu.StatusWord
import com.tangem.common.apdu.toTangemSdkError
import com.tangem.common.extensions.toInt
import com.tangem.common.tlv.TlvTag
/**
* Basic interface for a parsed response from [Command].
*/
interface CommandResponse
/**
* Basic class for Tangem card commands
*/
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.
* @param environment [SessionEnvironment] of the current card
* @return command data converted to [CommandApdu] that allows to convert it to [ByteArray]
* that can be sent to a Tangem card
*/
abstract fun serialize(environment: SessionEnvironment): CommandApdu
/**
* Deserializes data received from a card and stored in [ResponseApdu]
* into an array of [com.tangem.common.tlv.Tlv]. Then maps it into a [CommandResponse].
* @param environment [SessionEnvironment] of the current card.
* @param apdu received data.
* @return Card response converted to a [CommandResponse] of a type [T]
*/
abstract fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): T
override fun run(session: CardSession, callback: (result: CompletionResult<T>) -> Unit) {
Log.i("Command", "Initializing ${this::class.java.simpleName}")
if (session.environment.handleErrors) {
if (performPreCheck(session, callback)) return
}
transceive(session) { result ->
if (session.environment.handleErrors) {
if (performAfterCheck(session, result, callback)) return@transceive
}
callback(result)
}
}
open fun performPreCheck(session: CardSession,
callback: (result: CompletionResult<T>) -> Unit): Boolean {
return false
}
open fun performAfterCheck(session: CardSession,
result: CompletionResult<T>,
callback: (result: CompletionResult<T>) -> Unit): Boolean {
return false
}
fun transceive(session: CardSession, callback: (result: CompletionResult<T>) -> Unit) {
try {
val apdu = serialize(session.environment)
transceiveApdu(apdu, session) { result ->
when (result) {
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
is CompletionResult.Success -> {
val response = deserialize(session.environment, result.data)
callback(CompletionResult.Success(response))
}
}
}
} catch (error: TangemSdkError) {
callback(CompletionResult.Failure(error))
}
}
private fun transceiveApdu(apdu: CommandApdu, session: CardSession, callback: (result: CompletionResult<ResponseApdu>) -> Unit) {
session.send(apdu) { result ->
when (result) {
is CompletionResult.Success -> {
val responseApdu = result.data
when (responseApdu.statusWord) {
StatusWord.ProcessCompleted, StatusWord.Pin1Changed, StatusWord.Pin2Changed, StatusWord.PinsChanged
-> callback(CompletionResult.Success(responseApdu))
StatusWord.NeedPause -> {
// NeedPause is returned from the card whenever security delay is triggered.
val remainingTime = deserializeSecurityDelay(responseApdu, session.environment)
if (remainingTime != null) {
session.viewDelegate.onSecurityDelay(
remainingTime,
session.environment.card?.pauseBeforePin2 ?: 0)
}
Log.i(this::class.simpleName!!, "Nfc command ${this::class.simpleName!!} " +
"triggered security delay of $remainingTime milliseconds")
transceiveApdu(apdu, session, callback)
}
else -> {
val error = responseApdu.statusWord.toTangemSdkError()
if (error != null && !tryHandleError(error)) {
callback(CompletionResult.Failure(error))
} else {
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
}
}
}
}
is CompletionResult.Failure ->
if (result.error is TangemSdkError.TagLost) {
session.viewDelegate.onTagLost()
} else {
callback(CompletionResult.Failure(result.error))
}
}
}
}
/**
* Helper method to parse security delay information received from a card.
*
* @return Remaining security delay in milliseconds.
*/
private fun deserializeSecurityDelay(responseApdu: ResponseApdu, environment: SessionEnvironment): Int? {
val tlv = responseApdu.getTlvData()
return tlv?.find { it.tag == TlvTag.Pause }?.value?.toInt()
}
private fun tryHandleError(error: TangemSdkError): Boolean {
return false
}
}

View file

@ -1,100 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class CreateWalletResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Current status of the card [1 - Empty, 2 - Loaded, 3- Purged]
*/
val status: CardStatus,
/**
*/
val walletPublicKey: ByteArray
) : CommandResponse
/**
* This command will create a new wallet on the card having Empty state.
* A key pair WalletPublicKey / WalletPrivateKey is generated and securely stored in the card.
* App will need to obtain Wallet_PublicKey from the response of [CreateWalletCommand] or [ReadCommand]
* and then transform it into an address of corresponding blockchain wallet
* according to a specific blockchain algorithm.
* WalletPrivateKey is never revealed by the card and will be used by [SignCommand] and [CheckWalletCommand].
* RemainingSignature is set to MaxSignatures.
*
* @property cardId CID, Unique Tangem card ID number.
*/
class CreateWalletCommand : Command<CreateWalletResponse>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
if (session.environment.card?.status == CardStatus.Purged) {
callback(CompletionResult.Failure(TangemSdkError.CardIsPurged()))
return true
}
if (session.environment.card?.status == CardStatus.Loaded) {
callback(CompletionResult.Failure(TangemSdkError.AlreadyCreated()))
return true
}
return false
}
override fun performAfterCheck(session: CardSession,
result: CompletionResult<CreateWalletResponse>,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams) {
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
return true
}
return false
}
else -> return false
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
return CommandApdu(
Instruction.CreateWallet, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): CreateWalletResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return CreateWalletResponse(
cardId = decoder.decode(TlvTag.CardId),
status = decoder.decode(TlvTag.Status),
walletPublicKey = decoder.decode(TlvTag.WalletPublicKey)
)
}
}

View file

@ -1,43 +0,0 @@
package com.tangem.commands
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class OpenSessionResponse(
val sessionKeyB: ByteArray,
val uid: ByteArray
) : CommandResponse
/**
* In case of encrypted communication, App should setup a session before calling any further command.
* [OpenSessionCommand] generates secret session_key that is used by both host and card
* to encrypt and decrypt commands payload.
*/
class OpenSessionCommand(private val sessionKeyA: ByteArray) : Command<OpenSessionResponse>() {
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.SessionKeyA, sessionKeyA)
return CommandApdu(
Instruction.OpenSession, tlvBuilder.serialize(),
encryptionMode = environment.encryptionMode
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): OpenSessionResponse {
val tlvData = apdu.getTlvData()
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return OpenSessionResponse(
sessionKeyB = decoder.decode(TlvTag.SessionKeyB),
uid = decoder.decode(TlvTag.Uid)
)
}
}

View file

@ -1,85 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class PurgeWalletResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Current status of the card [1 - Empty, 2 - Loaded, 3- Purged]
*/
val status: CardStatus
) : CommandResponse
/**
* This command deletes all wallet data. If Is_Reusable flag is enabled during personalization,
* If Is_Reusable flag is disabled, the card switches to Purged state.
* Purged state is final, it makes the card useless.
* @property cardId CID, Unique Tangem card ID number.
*/
class PurgeWalletCommand : Command<PurgeWalletResponse>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<PurgeWalletResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
if (session.environment.card?.settingsMask?.contains(Settings.ProhibitPurgeWallet) == true) {
callback(CompletionResult.Failure(TangemSdkError.PurgeWalletProhibited()))
return true
}
return false
}
override fun performAfterCheck(session: CardSession,
result: CompletionResult<PurgeWalletResponse>,
callback: (result: CompletionResult<PurgeWalletResponse>) -> Unit): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams) {
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
return true
}
return false
}
else -> return false
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
return CommandApdu(
Instruction.PurgeWallet, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): PurgeWalletResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return PurgeWalletResponse(
cardId = decoder.decode(TlvTag.CardId),
status = decoder.decode(TlvTag.Status))
}
}

View file

@ -1,454 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import java.util.*
/**
* Determines which type of data is required for signing.
*/
data class SigningMethodMask(val rawValue: Int) {
fun contains(signingMethod: SigningMethod): Boolean {
return if (rawValue and 0x80 == 0) {
signingMethod.code == rawValue
} else {
rawValue and (0x01 shl signingMethod.code) != 0
}
}
}
enum class SigningMethod(val code: Int) {
SignHash(0),
SignRaw(1),
SignHashValidateByIssuer(2),
SignRawValidateByIssuer(3),
SignHashValidateByIssuerWriteIssuerData(4),
SignRawValidateByIssuerWriteIssuerData(5),
SignPos(6)
}
class SigningMethodMaskBuilder() {
private val signingMethods = mutableSetOf<SigningMethod>()
fun add(signingMethod: SigningMethod) {
signingMethods.add(signingMethod)
}
fun build(): SigningMethodMask {
val rawValue: Int = when {
signingMethods.count() == 0 -> {
0
}
signingMethods.count() == 1 -> {
signingMethods.iterator().next().code
}
else -> {
signingMethods.fold(
0x80, { acc, singingMethod -> acc + (0x01 shl singingMethod.code) }
)
}
}
return SigningMethodMask(rawValue)
}
}
/**
* Elliptic curve used for wallet key operations.
*/
enum class EllipticCurve(val curve: String) {
Secp256k1("secp256k1"),
Ed25519("ed25519");
companion object {
private val values = values()
fun byName(curve: String): EllipticCurve? = values.find { it.curve == curve }
}
}
/**
* Status of the card and its wallet.
*/
enum class CardStatus(val code: Int) {
NotPersonalized(0),
Empty(1),
Loaded(2),
Purged(3);
companion object {
private val values = values()
fun byCode(code: Int): CardStatus? = values.find { it.code == code }
}
}
/**
* Mask of products enabled on card
* @property rawValue Products mask values,
* while flags definitions and values are in [ProductMask.Companion] as constants.
*/
data class ProductMask(val rawValue: Int) {
fun contains(product: Product): Boolean = (rawValue and product.code) != 0
}
enum class Product(val code: Int) {
Note(0x01),
Tag(0x02),
IdCard(0x04),
IdIssuer(0x08)
}
class ProductMaskBuilder() {
private var productMaskValue = 0
fun add(product: Product) {
productMaskValue = productMaskValue or product.code
}
fun build() = ProductMask(productMaskValue)
}
/**
* Stores and maps Tangem card settings.
*
* @property rawValue Card settings in a form of flags,
* while flags definitions and possible values are in [Settings].
*/
data class SettingsMask(val rawValue: Int) {
fun contains(settings: Settings): Boolean = (rawValue and settings.code) != 0
}
enum class Settings(val code: Int) {
IsReusable(0x0001),
UseActivation(0x0002),
ProhibitPurgeWallet(0x0004),
UseBlock(0x0008),
AllowSwapPIN(0x0010),
AllowSwapPIN2(0x0020),
UseCVC(0x0040),
ForbidDefaultPIN(0x0080),
UseOneCommandAtTime(0x0100),
UseNdef(0x0200),
UseDynamicNdef(0x0400),
SmartSecurityDelay(0x0800),
ProtocolAllowUnencrypted(0x1000),
ProtocolAllowStaticEncryption(0x2000),
ProtectIssuerDataAgainstReplay(0x4000),
RestrictOverwriteIssuerDataEx(0x00100000),
AllowSelectBlockchain(0x8000),
DisablePrecomputedNdef(0x00010000),
SkipSecurityDelayIfValidatedByLinkedTerminal(0x00080000),
SkipCheckPin2andCvcIfValidatedByIssuer(0x00040000),
SkipSecurityDelayIfValidatedByIssuer(0x00020000),
RequireTermTxSignature(0x01000000),
RequireTermCertSignature(0x02000000),
CheckPIN3onCard(0x04000000)
}
class SettingsMaskBuilder() {
private var settingsMaskValue = 0
fun add(settings: Settings) {
settingsMaskValue = settingsMaskValue or settings.code
}
fun build() = SettingsMask(settingsMaskValue)
}
/**
* Detailed information about card contents.
*/
class CardData(
/**
* Tangem internal manufacturing batch ID.
*/
val batchId: String?,
/**
* Timestamp of manufacturing.
*/
val manufactureDateTime: Date?,
/**
* Name of the issuer.
*/
val issuerName: String?,
/**
* Name of the blockchain.
*/
val blockchainName: String?,
/**
* Signature of CardId with manufacturers private key.
*/
val manufacturerSignature: ByteArray?,
/**
* Mask of products enabled on card.
*/
val productMask: ProductMask?,
/**
* Name of the token.
*/
val tokenSymbol: String?,
/**
* Smart contract address.
*/
val tokenContractAddress: String?,
/**
* Number of decimals in token value.
*/
val tokenDecimal: Int?
)
/**
* Response for [ReadCommand]. Contains detailed card information.
*/
class Card(
/**
* Unique Tangem card ID number.
*/
val cardId: String,
/**
* Name of Tangem card manufacturer.
*/
val manufacturerName: String,
/**
* Current status of the card.
*/
val status: CardStatus?,
/**
* Version of Tangem COS.
*/
val firmwareVersion: String?,
/**
* Public key that is used to authenticate the card against manufacturers database.
* It is generated one time during card manufacturing.
*/
val cardPublicKey: ByteArray?,
/**
* Card settings defined by personalization (bit mask: 0 Enabled, 1 Disabled).
*/
val settingsMask: SettingsMask?,
/**
* Public key that is used by the card issuer to sign IssuerData field.
*/
val issuerPublicKey: ByteArray?,
/**
* Explicit text name of the elliptic curve used for all wallet key operations.
* Supported curves: secp256k1 and ed25519.
*/
val curve: EllipticCurve?,
/**
* Total number of signatures allowed for the wallet when the card was personalized.
*/
val maxSignatures: Int?,
/**
* Defines what data should be submitted to SIGN command.
*/
val signingMethods: SigningMethodMask?,
/**
* Delay in seconds before COS executes commands protected by PIN2.
*/
val pauseBeforePin2: Int?,
/**
* Public key of the blockchain wallet.
*/
val walletPublicKey: ByteArray?,
/**
* Remaining number of [SignCommand] operations before the wallet will stop signing transactions.
*/
val walletRemainingSignatures: Int?,
/**
* Total number of signed single hashes returned by the card in
* [SignCommand] responses since card personalization.
* Sums up array elements within all [SignCommand].
*/
val walletSignedHashes: Int?,
/**
* Any non-zero value indicates that the card experiences some hardware problems.
* User should withdraw the value to other blockchain wallet as soon as possible.
* Non-zero Health tag will also appear in responses of all other commands.
*/
val health: Int?,
/**
* Whether the card requires issuers confirmation of activation.
*/
val isActivated: Boolean,
/**
* A random challenge generated by personalisation that should be signed and returned
* to COS by the issuer to confirm the card has been activated.
* This field will not be returned if the card is activated.
*/
val activationSeed: ByteArray?,
/**
* Returned only if [SigningMethod.SignPos] enabling POS transactions is supported by card.
*/
val paymentFlowVersion: ByteArray?,
/**
* This value can be initialized by terminal and will be increased by COS on execution of every [SignCommand].
* For example, this field can store blockchain nonce for quick one-touch transaction on POS terminals.
* Returned only if [SigningMethod.SignPos] enabling POS transactions is supported by card.
*/
val userCounter: Int?,
/**
* This value can be initialized by App (with PIN2 confirmation) and will be increased by COS
* with the execution of each [SignCommand]. For example, this field can store blockchain nonce
* for a quick one-touch transaction on POS terminals. Returned only if [SigningMethod.SignPos].
*/
val userProtectedCounter: Int?,
/**
* When this value is true, it means that the application is linked to the card,
* and COS will not enforce security delay if [SignCommand] will be called
* with [TlvTag.TerminalTransactionSignature] parameter containing a correct signature of raw data
* to be signed made with [TlvTag.TerminalPublicKey].
*/
val terminalIsLinked: Boolean,
/**
* Detailed information about card contents. Format is defined by the card issuer.
* Cards complaint with Tangem Wallet application should have TLV format.
*/
val cardData: CardData?
) : CommandResponse
/**
* This command receives from the Tangem Card all the data about the card and the wallet,
* including unique card number (CID or cardId) that has to be submitted while calling all other commands.
*/
class ReadCommand : Command<Card>() {
override fun performAfterCheck(session: CardSession, result: CompletionResult<Card>, callback: (result: CompletionResult<Card>) -> Unit): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams) {
callback(CompletionResult.Failure(TangemSdkError.Pin1Required()))
return true
}
return false
}
else -> return false
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
/**
* [SessionEnvironment] stores the pin1 value. If no pin1 value was set, it will contain
* default value of 000000.
* In order to obtain cards data, [ReadCommand] should use the correct pin 1 value.
* The card will not respond if wrong pin 1 has been submitted.
*/
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.TerminalPublicKey, environment.terminalKeys?.publicKey)
return CommandApdu(
Instruction.Read, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): Card {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return Card(
cardId = decoder.decodeOptional(TlvTag.CardId) ?: "",
manufacturerName = decoder.decodeOptional(TlvTag.ManufactureId) ?: "",
status = decoder.decodeOptional(TlvTag.Status),
firmwareVersion = decoder.decodeOptional(TlvTag.Firmware),
cardPublicKey = decoder.decodeOptional(TlvTag.CardPublicKey),
settingsMask = decoder.decodeOptional(TlvTag.SettingsMask),
issuerPublicKey = decoder.decodeOptional(TlvTag.IssuerDataPublicKey),
curve = decoder.decodeOptional(TlvTag.CurveId),
maxSignatures = decoder.decodeOptional(TlvTag.MaxSignatures),
signingMethods = decoder.decodeOptional(TlvTag.SigningMethod),
pauseBeforePin2 = decoder.decodeOptional(TlvTag.PauseBeforePin2),
walletPublicKey = decoder.decodeOptional(TlvTag.WalletPublicKey),
walletRemainingSignatures = decoder.decodeOptional(TlvTag.RemainingSignatures),
walletSignedHashes = decoder.decodeOptional(TlvTag.SignedHashes),
health = decoder.decodeOptional(TlvTag.Health),
isActivated = decoder.decode(TlvTag.IsActivated),
activationSeed = decoder.decodeOptional(TlvTag.ActivationSeed),
paymentFlowVersion = decoder.decodeOptional(TlvTag.PaymentFlowVersion),
userCounter = decoder.decodeOptional(TlvTag.UserCounter),
userProtectedCounter = decoder.decodeOptional(TlvTag.UserProtectedCounter),
terminalIsLinked = decoder.decode(TlvTag.TerminalIsLinked),
cardData = deserializeCardData(tlvData)
)
}
private fun deserializeCardData(tlvData: List<Tlv>): CardData? {
val cardDataTlvs = tlvData.find { it.tag == TlvTag.CardData }?.let {
Tlv.deserialize(it.value)
}
if (cardDataTlvs.isNullOrEmpty()) return null
val decoder = TlvDecoder(cardDataTlvs)
return CardData(
batchId = decoder.decodeOptional(TlvTag.Batch),
manufactureDateTime = decoder.decodeOptional(TlvTag.ManufactureDateTime),
issuerName = decoder.decodeOptional(TlvTag.IssuerId),
blockchainName = decoder.decodeOptional(TlvTag.BlockchainId),
manufacturerSignature = decoder.decodeOptional(TlvTag.ManufacturerSignature),
productMask = decoder.decodeOptional(TlvTag.ProductMask),
tokenSymbol = decoder.decodeOptional(TlvTag.TokenSymbol),
tokenContractAddress = decoder.decodeOptional(TlvTag.TokenContractAddress),
tokenDecimal = decoder.decodeOptional(TlvTag.TokenDecimal)
)
}
}

View file

@ -1,124 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class ReadIssuerDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Data defined by issuer.
*/
val issuerData: ByteArray,
/**
* Issuers signature of [issuerData] with Issuer Data Private Key (which is kept on card).
* Issuers signature of SHA256-hashed [cardId] concatenated with [issuerData]:
* SHA256([cardId] | [issuerData]).
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
*/
val issuerDataSignature: ByteArray,
/**
* An optional counter that protect issuer data against replay attack.
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
*/
val issuerDataCounter: Int?
) : CommandResponse
/**
* This command returns 512-byte Issuer Data field and its issuers signature.
* Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. For example, this field may contain information about
* wallet balance signed by the issuer or additional issuers attestation data.
* @property cardId CID, Unique Tangem card ID number.
*/
class ReadIssuerDataCommand(
val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<ReadIssuerDataResponse>(), IssuerDataVerifier by verifier {
override fun run(session: CardSession, callback: (result: CompletionResult<ReadIssuerDataResponse>) -> Unit) {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
val publicKey = issuerPublicKey ?: card.issuerPublicKey
if (publicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey()))
return
}
super.run(session) { result ->
when (result) {
is CompletionResult.Failure -> callback(result)
is CompletionResult.Success -> {
if (result.data.issuerData.isEmpty()) {
callback(result)
return@run
}
val issuerDataToVerify = IssuerDataToVerify(
card.cardId, result.data.issuerData, result.data.issuerDataCounter
)
if (verify(publicKey, result.data.issuerDataSignature, issuerDataToVerify)) {
callback(result)
} else {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
}
}
}
}
}
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<ReadIssuerDataResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
return false
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadData)
return CommandApdu(
Instruction.ReadIssuerData, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): ReadIssuerDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return ReadIssuerDataResponse(
cardId = decoder.decode(TlvTag.CardId),
issuerData = decoder.decode(TlvTag.IssuerData),
issuerDataSignature = decoder.decode(TlvTag.IssuerDataSignature),
issuerDataCounter = decoder.decodeOptional(TlvTag.IssuerDataCounter)
)
}
}

View file

@ -1,183 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import java.io.ByteArrayOutputStream
class ReadIssuerExtraDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Size of all Issuer_Extra_Data field.
*/
val size: Int?,
/**
* Data defined by issuer.
*/
val issuerData: ByteArray,
/**
* Issuers signature of [issuerData] with Issuer Data Private Key (which is kept on card).
* Issuers signature of SHA256-hashed [cardId] concatenated with [issuerData]:
* SHA256([cardId] | [issuerData]).
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask] then signature of
* SHA256-hashed CID Issuer_Data concatenated with and [issuerDataCounter]:
* SHA256([cardId] | [issuerData] | [issuerDataCounter]).
*/
val issuerDataSignature: ByteArray?,
/**
* An optional counter that protects issuer data against replay attack.
* When flag [Settings.ProtectIssuerDataAgainstReplay] set in [SettingsMask]
* then this value is mandatory and must increase on each execution of [WriteIssuerDataCommand].
*/
val issuerDataCounter: Int?
) : CommandResponse
/**
* This command retrieves Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. . For example, this field may contain photo or
* biometric information for ID card product. Because of the large size of Issuer_Extra_Data,
* a series of these commands have to be executed to read the entire Issuer_Extra_Data.
*/
class ReadIssuerExtraDataCommand(
private val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<ReadIssuerExtraDataResponse>(), IssuerDataVerifier by verifier {
private val issuerData = ByteArrayOutputStream()
private var offset: Int = 0
private var issuerDataSize: Int = 0
override fun run(session: CardSession, callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit) {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
val publicKey = issuerPublicKey ?: card.issuerPublicKey
if (publicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey()))
return
}
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return
}
readIssuerData(session, card.cardId, publicKey, callback)
}
private fun readIssuerData(
session: CardSession,
cardId: String, publicKey: ByteArray,
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit) {
if (issuerDataSize != 0) {
session.viewDelegate.onDelay(
issuerDataSize, offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE
)
}
transceive(session) { result ->
when (result) {
is CompletionResult.Success -> {
if (result.data.size != null) {
if (result.data.size == 0) {
callback(CompletionResult.Success(result.data))
return@transceive
}
issuerDataSize = result.data.size
}
issuerData.write(result.data.issuerData)
if (result.data.issuerDataSignature == null) {
offset = issuerData.size()
readIssuerData(session, cardId, publicKey, callback)
} else {
completeTask(result.data, cardId, publicKey, callback)
}
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
private fun completeTask(data: ReadIssuerExtraDataResponse,
cardId: String, publicKey: ByteArray,
callback: (result: CompletionResult<ReadIssuerExtraDataResponse>) -> Unit) {
val dataToVerify = IssuerDataToVerify(
cardId,
issuerData.toByteArray(),
data.issuerDataCounter
)
if (verify(publicKey, data.issuerDataSignature!!, dataToVerify)) {
val finalResult = ReadIssuerExtraDataResponse(
data.cardId,
issuerDataSize,
issuerData.toByteArray(),
data.issuerDataSignature,
data.issuerDataCounter
)
callback(CompletionResult.Success(finalResult))
} else {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.ReadExtraData)
tlvBuilder.append(TlvTag.Offset, offset)
return CommandApdu(
Instruction.ReadIssuerData, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): ReadIssuerExtraDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return ReadIssuerExtraDataResponse(
cardId = decoder.decode(TlvTag.CardId),
size = decoder.decodeOptional(TlvTag.Size),
issuerData = decoder.decodeOptional(TlvTag.IssuerData) ?: byteArrayOf(),
issuerDataSignature = decoder.decodeOptional(TlvTag.IssuerDataSignature),
issuerDataCounter = decoder.decodeOptional(TlvTag.IssuerDataCounter)
)
}
companion object {
/**
* This mode value specifies that this command retrieves Issuer EXTRA data from the card
* (with value 0 the command will get instead simple Issuer Data from the card).
*/
const val EXTRA_DATA_MODE = 1
}
}

View file

@ -1,90 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class ReadUserDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String,
/**
* Data defined by user's App.
*/
val userData: ByteArray,
/**
* Data defined by user's App (confirmed by PIN2).
*/
val userProtectedData: ByteArray,
/**
* Counter initialized by user's App and increased on every signing of new transaction
*/
val userCounter: Int,
/**
* Counter initialized by user's App (confirmed by PIN2) and increased on every signing of new transaction
*/
val userProtectedCounter: Int
) : CommandResponse
/**
* This command returns two up to 512-byte User_Data, User_Protected_Data and two counters User_Counter and
* User_Protected_Counter fields.
* User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and it's payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*/
class ReadUserDataCommand : Command<ReadUserDataResponse>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<ReadUserDataResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
return false
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val builder = TlvBuilder()
builder.append(TlvTag.CardId, environment.card?.cardId)
builder.append(TlvTag.Pin, environment.pin1)
return CommandApdu(
Instruction.ReadUserData, builder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): ReadUserDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return ReadUserDataResponse(
cardId = decoder.decode(TlvTag.CardId),
userData = decoder.decode(TlvTag.UserData),
userProtectedData = decoder.decode(TlvTag.UserProtectedData),
userCounter = decoder.decode(TlvTag.UserCounter),
userProtectedCounter = decoder.decode(TlvTag.UserProtectedCounter)
)
}
}

View file

@ -1,147 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.sign
/**
* @param cardId CID, Unique Tangem card ID number
* @param signature Signed hashes (array of resulting signatures)
* @param walletRemainingSignatures Remaining number of sign operations before the wallet will stop signing transactions.
* @param walletSignedHashes Total number of signed single hashes returned by the card in sign command responses.
* Sums up array elements within all SIGN commands
*/
class SignResponse(
val cardId: String,
val signature: ByteArray,
val walletRemainingSignatures: Int,
val walletSignedHashes: Int
) : CommandResponse
/**
* Signs transaction hashes using a wallet private key, stored on the card.
*
* @property hashes Array of transaction hashes.
* @property cardId CID, Unique Tangem card ID number
*/
class SignCommand(private val hashes: Array<ByteArray>)
: Command<SignResponse>() {
private val hashSizes = if (hashes.isNotEmpty()) hashes.first().size else 0
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<SignResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
if (session.environment.card?.status == CardStatus.Purged) {
callback(CompletionResult.Failure(TangemSdkError.CardIsPurged()))
return true
}
if (session.environment.card?.status == CardStatus.Empty) {
callback(CompletionResult.Failure(TangemSdkError.CardIsEmpty()))
return true
}
if (session.environment.card?.walletRemainingSignatures == 0) {
callback(CompletionResult.Failure(TangemSdkError.NoRemainingSignatures()))
return true
}
if (session.environment.card?.signingMethods?.contains(SigningMethod.SignHash) != true) {
callback(CompletionResult.Failure(TangemSdkError.SignHashesNotAvailable()))
return true
}
if (hashSizes == 0) {
callback(CompletionResult.Failure(TangemSdkError.EmptyHashes()))
return true
}
if (hashes.any { it.size != hashSizes }) {
callback(CompletionResult.Failure(TangemSdkError.HashSizeMustBeEqual()))
return true
}
return false
}
override fun performAfterCheck(session: CardSession,
result: CompletionResult<SignResponse>,
callback: (result: CompletionResult<SignResponse>) -> Unit): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams) {
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
return true
}
return false
}
else -> return false
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val dataToSign = flattenHashes()
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.Pin2, environment.pin2)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.TransactionOutHashSize, byteArrayOf(hashSizes.toByte()))
tlvBuilder.append(TlvTag.TransactionOutHash, dataToSign)
tlvBuilder.append(TlvTag.Cvc, environment.cvc)
addTerminalSignature(environment, dataToSign, tlvBuilder)
return CommandApdu(
Instruction.Sign, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
private fun flattenHashes(): ByteArray {
checkForErrors()
return hashes.reduce { arr1, arr2 -> arr1 + arr2 }
}
private fun checkForErrors() {
if (hashes.isEmpty()) throw TangemSdkError.EmptyHashes()
if (hashes.size > 10) throw TangemSdkError.TooManyHashesInOneTransaction()
if (hashes.any { it.size != hashSizes }) throw TangemSdkError.HashSizeMustBeEqual()
}
/**
* Application can optionally submit a public key Terminal_PublicKey in [SignCommand].
* Submitted key is stored by the Tangem card if it differs from a previous submitted Terminal_PublicKey.
* The Tangem card will not enforce security delay if [SignCommand] will be called with
* TerminalTransactionSignature parameter containing a correct signature of raw data to be signed made with TerminalPrivateKey
* (this key should be generated and securily stored by the application).
*/
private fun addTerminalSignature(
environment: SessionEnvironment, dataToSign: ByteArray, tlvBuilder: TlvBuilder) {
environment.terminalKeys?.let { terminalKeyPair ->
val signedData = dataToSign.sign(terminalKeyPair.privateKey)
tlvBuilder.append(TlvTag.TerminalTransactionSignature, signedData)
tlvBuilder.append(TlvTag.TerminalPublicKey, terminalKeyPair.publicKey)
}
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): SignResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return SignResponse(
cardId = decoder.decode(TlvTag.CardId),
signature = decoder.decode(TlvTag.Signature),
walletRemainingSignatures = decoder.decode(TlvTag.RemainingSignatures),
walletSignedHashes = decoder.decode(TlvTag.SignedHashes)
)
}
}

View file

@ -1,136 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class WriteIssuerDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String
) : CommandResponse
/**
* This command writes 512-byte Issuer Data field and its issuers signature.
* Issuer Data is never changed or parsed from within the Tangem COS. The issuer defines purpose of use,
* format and payload of Issuer Data. For example, this field may contain information about
* wallet balance signed by the issuer or additional issuers attestation data.
* @property cardId CID, Unique Tangem card ID number.
* @property issuerData Data provided by issuer.
* @property issuerDataSignature Issuers signature of [issuerData] with Issuer Data Private Key (which is kept on card).
* @property issuerDataCounter An optional counter that protect issuer data against replay attack.
*/
class WriteIssuerDataCommand(
private val issuerData: ByteArray,
private val issuerDataSignature: ByteArray,
private val issuerDataCounter: Int? = null,
private val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<WriteIssuerDataResponse>(), IssuerDataVerifier by verifier {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit): Boolean {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return true
}
val publicKey = issuerPublicKey ?: card.issuerPublicKey
if (publicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey()))
return true
}
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
if (issuerData.size > MAX_SIZE) {
callback(CompletionResult.Failure(TangemSdkError.DataSizeTooLarge()))
return true
}
if (!isCounterValid(issuerDataCounter, card)) {
callback(CompletionResult.Failure(TangemSdkError.MissingCounter()))
return true
}
if (!verifySignature(publicKey, card.cardId)) {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
return true
}
return false
}
override fun performAfterCheck(session: CardSession,
result: CompletionResult<WriteIssuerDataResponse>,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit
): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams &&
isCounterRequired(session.environment.card)) {
callback(CompletionResult.Failure(TangemSdkError.DataCannotBeWritten()))
return true
}
return false
}
else -> return false
}
}
private fun isCounterValid(issuerDataCounter: Int?, card: Card): Boolean =
if (isCounterRequired(card)) issuerDataCounter != null else true
private fun isCounterRequired(card: Card?): Boolean =
card?.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false
private fun verifySignature(publicKey: ByteArray, cardId: String): Boolean {
return verify(
publicKey,
issuerDataSignature,
IssuerDataToVerify(cardId, issuerData, issuerDataCounter)
)
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, IssuerDataMode.WriteData)
tlvBuilder.append(TlvTag.IssuerData, issuerData)
tlvBuilder.append(TlvTag.IssuerDataSignature, issuerDataSignature)
tlvBuilder.append(TlvTag.IssuerDataCounter, issuerDataCounter)
return CommandApdu(
Instruction.WriteIssuerData, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): WriteIssuerDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return WriteIssuerDataResponse(
cardId = decoder.decode(TlvTag.CardId)
)
}
companion object {
const val MAX_SIZE = 512
}
}

View file

@ -1,213 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.common.DefaultIssuerDataVerifier
import com.tangem.commands.common.IssuerDataMode
import com.tangem.commands.common.IssuerDataToVerify
import com.tangem.commands.common.IssuerDataVerifier
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
/**
* This command writes Issuer Extra Data field and its issuers signature.
* Issuer Extra Data is never changed or parsed from within the Tangem COS.
* The issuer defines purpose of use, format and payload of Issuer Data.
* For example, this field may contain a photo or biometric information for ID card products.
* Because of the large size of Issuer_Extra_Data, a series of these commands have to be executed
* to write entire Issuer_Extra_Data.
* @param issuerData Data provided by issuer.
* @param startingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerDataCounter] (if flags Protect_Issuer_Data_Against_Replay and
* Restrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]) and size of [issuerData].
* @param finalizingSignature Issuers signature with Issuer Data Private Key of [cardId],
* [issuerData] and [issuerDataCounter] (the latter one only if flags Protect_Issuer_Data_Against_Replay
* andRestrict_Overwrite_Issuer_Extra_Data are set in [SettingsMask]).
* @param issuerDataCounter An optional counter that protect issuer data against replay attack.
*/
class WriteIssuerExtraDataCommand(
private val issuerData: ByteArray,
private val startingSignature: ByteArray,
private val finalizingSignature: ByteArray,
private val issuerDataCounter: Int? = null,
private val issuerPublicKey: ByteArray? = null,
verifier: IssuerDataVerifier = DefaultIssuerDataVerifier()
) : Command<WriteIssuerDataResponse>(), IssuerDataVerifier by verifier {
var mode: IssuerDataMode = IssuerDataMode.InitializeWritingExtraData
var offset: Int = 0
override fun run(session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit) {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
val publicKey = issuerPublicKey ?: card.issuerPublicKey
if (publicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey()))
return
}
writeIssuerData(session, card.cardId, publicKey) { response ->
when (response) {
is CompletionResult.Success -> callback(response)
is CompletionResult.Failure -> {
if (response.error is TangemSdkError.InvalidParams && isCounterRequired(card)) {
callback(CompletionResult.Failure(TangemSdkError.DataCannotBeWritten()))
return@writeIssuerData
}
if (response.error is TangemSdkError.InvalidState &&
card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false) {
callback(CompletionResult.Failure(TangemSdkError.OverwritingDataIsProhibited()))
return@writeIssuerData
}
}
}
}
}
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit): Boolean {
val card = session.environment.card
if (card == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return true
}
val publicKey = issuerPublicKey ?: card.issuerPublicKey
if (publicKey == null) {
callback(CompletionResult.Failure(TangemSdkError.MissingIssuerPubicKey()))
return true
}
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
if (issuerData.size > MAX_SIZE) {
callback(CompletionResult.Failure(TangemSdkError.ExendedDataSizeTooLarge()))
return true
}
if (!isCounterValid(issuerDataCounter, card)) {
callback(CompletionResult.Failure(TangemSdkError.MissingCounter()))
return true
}
if (!verifySignatures(card.cardId, publicKey)) {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
return true
}
return false
}
private fun isCounterValid(issuerDataCounter: Int?, card: Card): Boolean =
if (isCounterRequired(card)) issuerDataCounter != null else true
private fun isCounterRequired(card: Card): Boolean =
card.settingsMask?.contains(Settings.ProtectIssuerDataAgainstReplay) != false
private fun verifySignatures(cardId: String, publicKey: ByteArray): Boolean {
val firstData = IssuerDataToVerify(cardId, null, issuerDataCounter, issuerData.size)
val secondData = IssuerDataToVerify(cardId, issuerData, issuerDataCounter)
return verify(publicKey, startingSignature, firstData) &&
verify(publicKey, finalizingSignature, secondData)
}
private fun writeIssuerData(
session: CardSession,
cardId: String, publicKey: ByteArray,
callback: (result: CompletionResult<WriteIssuerDataResponse>) -> Unit
) {
if (mode == IssuerDataMode.WriteExtraData) {
session.viewDelegate.onDelay(issuerData.size, offset, WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE)
}
transceive(session) { result ->
when (result) {
is CompletionResult.Success -> {
when (mode) {
IssuerDataMode.InitializeWritingExtraData -> {
mode = IssuerDataMode.WriteExtraData
writeIssuerData(session, cardId, publicKey, callback)
return@transceive
}
IssuerDataMode.WriteExtraData -> {
offset += WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE
if (offset >= issuerData.size) {
mode = IssuerDataMode.FinalizeExtraData
}
writeIssuerData(session, cardId, publicKey, callback)
return@transceive
}
IssuerDataMode.FinalizeExtraData -> {
callback(CompletionResult.Success(result.data))
}
}
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
}
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Pin, environment.pin1)
tlvBuilder.append(TlvTag.CardId, environment.card?.cardId)
tlvBuilder.append(TlvTag.Mode, mode)
when (mode) {
IssuerDataMode.InitializeWritingExtraData -> {
tlvBuilder.append(TlvTag.Size, issuerData.size)
tlvBuilder.append(TlvTag.IssuerDataSignature, startingSignature)
tlvBuilder.append(TlvTag.IssuerDataCounter, issuerDataCounter)
}
IssuerDataMode.WriteExtraData -> {
tlvBuilder.append(TlvTag.IssuerData, getDataToWrite())
tlvBuilder.append(TlvTag.Offset, offset)
}
IssuerDataMode.FinalizeExtraData -> {
tlvBuilder.append(TlvTag.IssuerDataSignature, finalizingSignature)
}
}
return CommandApdu(
Instruction.WriteIssuerData, tlvBuilder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
private fun getDataToWrite(): ByteArray =
issuerData.copyOfRange(offset, offset + calculatePartSize())
private fun calculatePartSize(): Int {
val bytesLeft = issuerData.size - offset
return if (bytesLeft < SINGLE_WRITE_SIZE) bytesLeft else SINGLE_WRITE_SIZE
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): WriteIssuerDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
return WriteIssuerDataResponse(cardId = TlvDecoder(tlvData).decode(TlvTag.CardId)
)
}
companion object {
const val SINGLE_WRITE_SIZE = 1524
const val MAX_SIZE = 32 * 1024
}
}

View file

@ -1,95 +0,0 @@
package com.tangem.commands
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
class WriteUserDataResponse(
/**
* CID, Unique Tangem card ID number.
*/
val cardId: String
) : CommandResponse
/**
* This command writes to the card any of User_Data, User_ProtectedData, User_Counter and User_ProtectedCounter fields.
* User_Data and User_ProtectedData are never changed or parsed by the executable code the Tangem COS.
* The App defines purpose of use, format and it's payload. For example, this field may contain cashed information
* from blockchain to accelerate preparing new transaction.
* User_Counter and User_ProtectedCounter are counters, that initial values can be set by App and increased on every signing
* of new transaction (on SIGN command that calculate new signatures). The App defines purpose of use.
* For example, this fields may contain blockchain nonce value.
*
* Writing of User_Counter and User_Data protected only by PIN1.
* User_ProtectedCounter and User_ProtectedData additionaly need PIN2 to confirmation.
*/
class WriteUserDataCommand(private val userData: ByteArray? = null, private val userProtectedData: ByteArray? = null,
private val userCounter: Int? = null,
private val userProtectedCounter: Int? = null) : Command<WriteUserDataResponse>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<WriteUserDataResponse>) -> Unit): Boolean {
if (session.environment.card?.status == CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.NotPersonalized()))
return true
}
if (session.environment.card?.isActivated == true) {
callback(CompletionResult.Failure(TangemSdkError.NotActivated()))
return true
}
if (userData?.size ?: 0 > MAX_SIZE || userProtectedData?.size ?: 0 > MAX_SIZE) {
callback(CompletionResult.Failure(TangemSdkError.DataSizeTooLarge()))
return true
}
return false
}
override fun performAfterCheck(session: CardSession,
result: CompletionResult<WriteUserDataResponse>,
callback: (result: CompletionResult<WriteUserDataResponse>) -> Unit
): Boolean {
when (result) {
is CompletionResult.Failure -> {
if (result.error is TangemSdkError.InvalidParams) {
callback(CompletionResult.Failure(TangemSdkError.Pin2OrCvcRequired()))
return true
}
return false
}
else -> return false
}
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
val builder = TlvBuilder()
builder.append(TlvTag.CardId, environment.card?.cardId)
builder.append(TlvTag.Pin, environment.pin1)
builder.append(TlvTag.UserData, userData)
builder.append(TlvTag.UserCounter, userCounter)
builder.append(TlvTag.UserProtectedData, userProtectedData)
builder.append(TlvTag.UserProtectedCounter, userProtectedCounter)
if (userProtectedCounter != null || userProtectedData != null)
builder.append(TlvTag.Pin2, environment.pin2)
return CommandApdu(
Instruction.WriteUserData, builder.serialize(),
environment.encryptionMode, environment.encryptionKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): WriteUserDataResponse {
val tlvData = apdu.getTlvData(environment.encryptionKey)
?: throw TangemSdkError.DeserializeApduFailed()
return WriteUserDataResponse(TlvDecoder(tlvData).decode(TlvTag.CardId))
}
companion object{
const val MAX_SIZE = 512
}
}

View file

@ -1,44 +0,0 @@
package com.tangem.commands.common
import com.tangem.commands.WriteIssuerExtraDataCommand
/**
* This enum specifies modes for [WriteIssuerExtraDataCommand].
*/
enum class IssuerDataMode(val code: Byte) {
/**
* This mode is required to read issuer data from the card.
*/
ReadData(0),
/**
* This mode is required to write issuer data to the card.
*/
WriteData(0),
/**
* This mode is required to read issuer extra data from the card.
*/
ReadExtraData(1),
/**
* This mode is required to initiate writing issuer extra data to the card.
*/
InitializeWritingExtraData(1),
/**
* With this mode, the command writes part of issuer extra data
* (block of a size [WriteIssuerExtraDataCommand.SINGLE_WRITE_SIZE]) to the card.
*/
WriteExtraData(2),
/**
* This mode is used after the issuer extra data was fully written to the card.
* Under this mode the command provides the issuer signature
* to confirm the validity of data that was written to card.
*/
FinalizeExtraData(3);
companion object {
private val values = values()
fun byCode(code: Byte): IssuerDataMode? = values.find { it.code == code }
}
}

View file

@ -1,40 +0,0 @@
package com.tangem.commands.common
import com.tangem.common.tlv.TlvEncoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.CryptoUtils
import java.io.ByteArrayOutputStream
interface IssuerDataVerifier {
fun verify(
issuerPublicKey: ByteArray, signature: ByteArray, issuerDataToVerify: IssuerDataToVerify
): Boolean
}
class IssuerDataToVerify(
val cardId: String,
val issuerData: ByteArray?,
val issuerDataCounter: Int? = null,
val issuerExtraDataSize: Int? = null
)
class DefaultIssuerDataVerifier : IssuerDataVerifier {
override fun verify(
issuerPublicKey: ByteArray,
signature: ByteArray,
issuerDataToVerify: IssuerDataToVerify
): Boolean {
val tlvEncoder = TlvEncoder()
val dataToVerify = ByteArrayOutputStream()
dataToVerify.write(tlvEncoder.encodeValue(TlvTag.CardId, issuerDataToVerify.cardId))
issuerDataToVerify.issuerData?.let { dataToVerify.write(it) }
issuerDataToVerify.issuerDataCounter?.let { counter ->
dataToVerify.write(tlvEncoder.encodeValue(TlvTag.IssuerDataCounter, counter))
}
issuerDataToVerify.issuerExtraDataSize?.let {
dataToVerify.write(tlvEncoder.encodeValue(TlvTag.Size, it))
}
return CryptoUtils.verify(issuerPublicKey, dataToVerify.toByteArray(), signature)
}
}

View file

@ -1,119 +0,0 @@
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 java.lang.reflect.Type
import java.text.DateFormat
import java.util.*
/**
[REDACTED_AUTHOR]
*/
class ResponseConverter {
val gson: Gson by lazy { init() }
private val fieldConverter = ResponseFieldConverter()
private fun init(): Gson {
val builder = GsonBuilder().apply {
registerTypeAdapter(ByteArray::class.java, ByteTypeAdapter(fieldConverter))
registerTypeAdapter(SigningMethodMask::class.java, SigningMethodTypeAdapter(fieldConverter))
registerTypeAdapter(SettingsMask::class.java, SettingsMaskTypeAdapter(fieldConverter))
registerTypeAdapter(ProductMask::class.java, ProductMaskTypeAdapter(fieldConverter))
registerTypeAdapter(Date::class.java, DateTypeAdapter())
}
builder.setPrettyPrinting()
return builder.create()
}
fun convertResponse(response: CommandResponse?): String = gson.toJson(response)
}
class ByteTypeAdapter(
private val fieldConverter: ResponseFieldConverter
) : JsonSerializer<ByteArray> {
override fun serialize(src: ByteArray, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
return JsonPrimitive(fieldConverter.byteArrayToHex(src))
}
}
class SettingsMaskTypeAdapter(
private val fieldConverter: ResponseFieldConverter
) : JsonSerializer<SettingsMask> {
override fun serialize(src: SettingsMask, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
return JsonArray().apply {
fieldConverter.settingsMaskList(src).forEach { add(it) }
}
}
}
class ProductMaskTypeAdapter(
private val fieldConverter: ResponseFieldConverter
) : JsonSerializer<ProductMask> {
override fun serialize(src: ProductMask, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
return JsonArray().apply {
fieldConverter.productMaskList(src).forEach { add(it) }
}
}
}
class SigningMethodTypeAdapter(
private val fieldConverter: ResponseFieldConverter
) : JsonSerializer<SigningMethodMask> {
override fun serialize(src: SigningMethodMask, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
return JsonArray().apply {
fieldConverter.signingMethodList(src).forEach { add(it) }
}
}
}
class DateTypeAdapter : JsonSerializer<Date> {
override fun serialize(src: Date, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
val formatter = DateFormat.getDateInstance(DateFormat.DEFAULT, Locale("en_US"))
return JsonPrimitive(formatter.format(src).toString())
}
}
class ResponseFieldConverter {
fun productMask(productMask: ProductMask?): String {
return productMaskList(productMask).print(wrap = false)
}
fun productMaskList(productMask: ProductMask?): List<String> {
val mask = productMask ?: return emptyList()
return Product.values().filter { mask.contains(it) }.map { it.name }
}
fun signingMethod(signingMask: SigningMethodMask?): String {
return signingMethodList(signingMask).print(wrap = false)
}
fun signingMethodList(signingMask: SigningMethodMask?): List<String> {
val mask = signingMask ?: return emptyList()
return SigningMethod.values().filter { mask.contains(it) }.map { it.name }
}
fun settingsMask(settingsMask: SettingsMask?): String {
return settingsMaskList(settingsMask).print(wrap = false)
}
fun settingsMaskList(settingsMask: SettingsMask?): List<String> {
val masks = settingsMask ?: return emptyList()
return Settings.values().filter { masks.contains(it) }.map { it.name }
}
fun byteArrayToHex(byteArray: ByteArray?): String? {
return byteArray?.toHexString()
}
fun byteArrayToString(byteArray: ByteArray?): String? {
return if (byteArray == null) null else String(byteArray)
}
}

View file

@ -1,43 +0,0 @@
package com.tangem.commands.personalization
import com.tangem.SessionEnvironment
import com.tangem.commands.Command
import com.tangem.commands.CommandResponse
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
data class DepersonalizeResponse(val success: Boolean) : CommandResponse
/**
* Command available on SDK cards only
*
* This command resets card to initial state,
* erasing all data written during personalization and usage.
*/
class DepersonalizeCommand : Command<DepersonalizeResponse>() {
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(
Instruction.Depersonalize, byteArrayOf()
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): DepersonalizeResponse {
return DepersonalizeResponse(true)
}
}

View file

@ -1,88 +0,0 @@
package com.tangem.commands.personalization
import com.tangem.commands.personalization.entities.NdefRecord
import java.io.ByteArrayOutputStream
import java.nio.charset.StandardCharsets
/**
* Encodes information that is to be written on the card as an Ndef Tag.
*/
class NdefEncoder(private val ndefRecords: List<NdefRecord>, private val useDinamicNdef: Boolean) {
fun encode(): ByteArray {
val bs = ByteArrayOutputStream()
// space for size
bs.write(0)
bs.write(0)
for (i in ndefRecords.indices) {
val headerValue = (if (i == 0) 0x80 else 0x00) or (if (!useDinamicNdef && i == ndefRecords.size - 1) 0x40 else 0x00)
var value: ByteArray = ndefRecords[i].value.toByteArray(StandardCharsets.UTF_8)
encodeValue(ndefRecords[i], headerValue, bs)
}
val result = bs.toByteArray()
result[0] = (result.size - 2 shr 8).toByte()
result[1] = (result.size - 2 and 0xFF).toByte()
return result
}
private fun encodeValue(ndefRecord: NdefRecord, headerValue: Int, bs: ByteArrayOutputStream) {
when (ndefRecord.type) {
NdefRecord.Type.AAR -> {
bs.write((headerValue or 0x14)) // NDEF Header
bs.write(0x0F) // Length of the record type
bs.write(ndefRecord.valueInBytes.size) // Length of the payload data
bs.write(byteArrayOf(0x61.toByte(), 0x6E.toByte(), 0x64.toByte(), 0x72.toByte(), 0x6F.toByte(), 0x69.toByte(), 0x64.toByte(), 0x2E.toByte(), 0x63.toByte(), 0x6F.toByte(), 0x6D.toByte(), 0x3A.toByte(),
0x70.toByte(), 0x6B.toByte(), 0x67.toByte())) // type name
bs.write(ndefRecord.valueInBytes)
}
NdefRecord.Type.URI -> {
bs.write((headerValue or 0x11)) // NDEF Header
bs.write(0x01) // Length of the record type
val uriIdentifierCode: Byte
val prefix: String
when {
ndefRecord.value.startsWith("http://www.") -> {
uriIdentifierCode = 0x01.toByte()
prefix = "http://www."
}
ndefRecord.value.startsWith("https://www.") -> {
uriIdentifierCode = 0x02.toByte()
prefix = "https://www."
}
ndefRecord.value.startsWith("http://") -> {
uriIdentifierCode = 0x03.toByte()
prefix = "http://"
}
ndefRecord.value.startsWith("https://") -> {
uriIdentifierCode = 0x04.toByte()
prefix = "https://"
}
else -> {
throw Exception()
}
}
val value = ndefRecord.value.substring(prefix.length).toByteArray()
bs.write(value.size + 1) // Length of the payload data
bs.write(0x55) // URI
bs.write(uriIdentifierCode.toInt()) // ?
bs.write(value)
}
NdefRecord.Type.TEXT -> {
bs.write((headerValue or 0x11)) // NDEF Header
bs.write(0x01) // Length of the record type
bs.write(ndefRecord.valueInBytes.size.toByte() + 1 + "en".length) // Length of the payload data
bs.write(0x54) // Text
bs.write(0x02) // UTF8(MSB=0)|"en".length
bs.write("en".toByteArray(StandardCharsets.US_ASCII))
bs.write(ndefRecord.valueInBytes)
}
else -> throw Exception("Invalid NDEF record in config!")
}
}
}

View file

@ -1,168 +0,0 @@
package com.tangem.commands.personalization
import com.tangem.CardSession
import com.tangem.SessionEnvironment
import com.tangem.TangemSdkError
import com.tangem.commands.Card
import com.tangem.commands.CardData
import com.tangem.commands.CardStatus
import com.tangem.commands.Command
import com.tangem.commands.personalization.entities.*
import com.tangem.common.CompletionResult
import com.tangem.common.apdu.CommandApdu
import com.tangem.common.apdu.Instruction
import com.tangem.common.apdu.ResponseApdu
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvBuilder
import com.tangem.common.tlv.TlvDecoder
import com.tangem.common.tlv.TlvTag
import com.tangem.crypto.sign
/**
* Command available on SDK cards only
*
* Personalization is an initialization procedure, required before starting using a card.
* During this procedure a card setting is set up.
* During this procedure all data exchange is encrypted.
* @property config is a configuration file with all the card settings that are written on the card
* during personalization.
* @property issuer Issuer is a third-party team or company wishing to use Tangem cards.
* @property manufacturer Tangem Card Manufacturer.
* @property acquirer Acquirer is a trusted third-party company that operates proprietary
* (non-EMV) POS terminal infrastructure and transaction processing back-end.
*/
class PersonalizeCommand(
private val config: CardConfig,
private val issuer: Issuer, private val manufacturer: Manufacturer,
private val acquirer: Acquirer? = null
) : Command<Card>() {
override fun performPreCheck(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit): Boolean {
if (session.environment.card?.status != CardStatus.NotPersonalized) {
callback(CompletionResult.Failure(TangemSdkError.AlreadyPersonalized()))
return true
}
return false
}
override fun serialize(environment: SessionEnvironment): CommandApdu {
return CommandApdu(
Instruction.Personalize,
serializePersonalizationData(config),
encryptionKey = devPersonalizationKey
)
}
override fun deserialize(environment: SessionEnvironment, apdu: ResponseApdu): Card {
val tlvData = apdu.getTlvData(devPersonalizationKey)
?: throw TangemSdkError.DeserializeApduFailed()
val decoder = TlvDecoder(tlvData)
return Card(
cardId = decoder.decodeOptional(TlvTag.CardId) ?: "",
manufacturerName = decoder.decodeOptional(TlvTag.ManufactureId) ?: "",
status = decoder.decodeOptional(TlvTag.Status),
firmwareVersion = decoder.decodeOptional(TlvTag.Firmware),
cardPublicKey = decoder.decodeOptional(TlvTag.CardPublicKey),
settingsMask = decoder.decodeOptional(TlvTag.SettingsMask),
issuerPublicKey = decoder.decodeOptional(TlvTag.IssuerDataPublicKey),
curve = decoder.decodeOptional(TlvTag.CurveId),
maxSignatures = decoder.decodeOptional(TlvTag.MaxSignatures),
signingMethods = decoder.decodeOptional(TlvTag.SigningMethod),
pauseBeforePin2 = decoder.decodeOptional(TlvTag.PauseBeforePin2),
walletPublicKey = decoder.decodeOptional(TlvTag.WalletPublicKey),
walletRemainingSignatures = decoder.decodeOptional(TlvTag.RemainingSignatures),
walletSignedHashes = decoder.decodeOptional(TlvTag.SignedHashes),
health = decoder.decodeOptional(TlvTag.Health),
isActivated = decoder.decode(TlvTag.IsActivated),
activationSeed = decoder.decodeOptional(TlvTag.ActivationSeed),
paymentFlowVersion = decoder.decodeOptional(TlvTag.PaymentFlowVersion),
userCounter = decoder.decodeOptional(TlvTag.UserCounter),
userProtectedCounter = decoder.decodeOptional(TlvTag.UserProtectedCounter),
terminalIsLinked = decoder.decode(TlvTag.TerminalIsLinked),
cardData = deserializeCardData(tlvData)
)
}
private fun deserializeCardData(tlvData: List<Tlv>): CardData? {
val cardDataTlvs = tlvData.find { it.tag == TlvTag.CardData }?.let {
Tlv.deserialize(it.value)
}
if (cardDataTlvs.isNullOrEmpty()) return null
val decoder = TlvDecoder(cardDataTlvs)
return CardData(
batchId = decoder.decodeOptional(TlvTag.Batch),
manufactureDateTime = decoder.decodeOptional(TlvTag.ManufactureDateTime),
issuerName = decoder.decodeOptional(TlvTag.IssuerId),
blockchainName = decoder.decodeOptional(TlvTag.BlockchainId),
manufacturerSignature = decoder.decodeOptional(TlvTag.ManufacturerSignature),
productMask = decoder.decodeOptional(TlvTag.ProductMask),
tokenSymbol = decoder.decodeOptional(TlvTag.TokenSymbol),
tokenContractAddress = decoder.decodeOptional(TlvTag.TokenContractAddress),
tokenDecimal = decoder.decodeOptional(TlvTag.TokenDecimal)
)
}
private fun serializePersonalizationData(config: CardConfig): ByteArray {
val cardId = config.createCardId() ?: throw TangemSdkError.SerializeCommandError()
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.CardId, cardId)
tlvBuilder.append(TlvTag.CurveId, config.curveID)
tlvBuilder.append(TlvTag.MaxSignatures, config.maxSignatures)
tlvBuilder.append(TlvTag.SigningMethod, config.signingMethods)
tlvBuilder.append(TlvTag.SettingsMask, config.createSettingsMask())
tlvBuilder.append(TlvTag.PauseBeforePin2, config.pauseBeforePin2 / 10)
tlvBuilder.append(TlvTag.Cvc, config.cvc.toByteArray())
if (!config.ndefRecords.isNullOrEmpty())
tlvBuilder.append(TlvTag.NdefData, serializeNdef(config))
tlvBuilder.append(TlvTag.CreateWalletAtPersonalize, config.createWallet)
tlvBuilder.append(TlvTag.NewPin, config.pin)
tlvBuilder.append(TlvTag.NewPin2, config.pin2)
tlvBuilder.append(TlvTag.NewPin3, config.pin3)
tlvBuilder.append(TlvTag.CrExKey, config.hexCrExKey)
tlvBuilder.append(TlvTag.IssuerDataPublicKey, issuer.dataKeyPair.publicKey)
tlvBuilder.append(TlvTag.IssuerTransactionPublicKey, issuer.transactionKeyPair.publicKey)
tlvBuilder.append(TlvTag.AcquirerPublicKey, acquirer?.keyPair?.publicKey)
tlvBuilder.append(TlvTag.CardData, serializeCardData(cardId, config.cardData))
return tlvBuilder.serialize()
}
private fun serializeNdef(config: CardConfig): ByteArray {
return NdefEncoder(config.ndefRecords, config.useDynamicNdef).encode()
}
private fun serializeCardData(cardId: String, cardData: CardData): ByteArray {
val tlvBuilder = TlvBuilder()
tlvBuilder.append(TlvTag.Batch, cardData.batchId)
tlvBuilder.append(TlvTag.ProductMask, cardData.productMask)
tlvBuilder.append(TlvTag.ManufactureDateTime, cardData.manufactureDateTime)
tlvBuilder.append(TlvTag.IssuerId, issuer.id)
tlvBuilder.append(TlvTag.BlockchainId, cardData.blockchainName)
if (cardData.tokenSymbol != null) {
tlvBuilder.append(TlvTag.TokenSymbol, cardData.tokenSymbol)
tlvBuilder.append(TlvTag.TokenContractAddress, cardData.tokenContractAddress)
tlvBuilder.append(TlvTag.TokenDecimal, cardData.tokenDecimal)
}
tlvBuilder.append(
TlvTag.CardIdManufacturerSignature,
cardId.hexToBytes().sign(manufacturer.keyPair.privateKey)
)
return tlvBuilder.serialize()
}
companion object {
val devPersonalizationKey = "1234".calculateSha256().copyOf(32)
}
}

View file

@ -1,9 +0,0 @@
package com.tangem.commands.personalization.entities
import com.tangem.KeyPair
data class Acquirer(
val keyPair: KeyPair,
val name: String? = null,
val id: String? = null
)

View file

@ -1,71 +0,0 @@
package com.tangem.commands.personalization.entities
import com.tangem.commands.CardData
import com.tangem.commands.EllipticCurve
import com.tangem.commands.SigningMethodMask
data class NdefRecord(
val type: Type,
val value: String
) {
enum class Type {
URI, AAR, TEXT
}
@delegate:Transient
val valueInBytes: ByteArray by lazy { value.toByteArray() }
}
/**
* It is a configuration file with all the card settings that are written on the card
* during [PersonalizeCommand].
*/
data class CardConfig(
val issuerName: String? = null,
val acquirerName: String? = null,
val series: String? = null,
val startNumber: Long = 0,
val count: Int = 0,
val pin: String,
val pin2: String,
val pin3: String,
val hexCrExKey: String?,
val cvc: String,
val pauseBeforePin2: Int,
val smartSecurityDelay: Boolean,
val curveID: EllipticCurve,
val signingMethods: SigningMethodMask,
val maxSignatures: Int,
val isReusable: Boolean,
val allowSwapPin: Boolean,
val allowSwapPin2: Boolean,
val useActivation: Boolean,
val useCvc: Boolean,
val useNdef: Boolean,
val useDynamicNdef: Boolean,
val useOneCommandAtTime: Boolean,
val useBlock: Boolean,
val allowSelectBlockchain: Boolean,
val forbidPurgeWallet: Boolean,
val protocolAllowUnencrypted: Boolean,
val protocolAllowStaticEncryption: Boolean,
val protectIssuerDataAgainstReplay: Boolean,
val forbidDefaultPin: Boolean,
val disablePrecomputedNdef: Boolean,
val skipSecurityDelayIfValidatedByIssuer: Boolean,
val skipCheckPIN2andCVCIfValidatedByIssuer: Boolean,
val skipSecurityDelayIfValidatedByLinkedTerminal: Boolean,
val restrictOverwriteIssuerDataEx: Boolean,
val requireTerminalTxSignature: Boolean,
val requireTerminalCertSignature: Boolean,
val checkPin3onCard: Boolean,
val createWallet: Boolean,
val cardData: CardData,
val ndefRecords: List<NdefRecord>
) {
companion object
}

View file

@ -1,81 +0,0 @@
package com.tangem.commands.personalization.entities
import com.tangem.commands.Settings
import com.tangem.commands.SettingsMask
import com.tangem.commands.SettingsMaskBuilder
internal fun CardConfig.createSettingsMask(): SettingsMask {
val builder = SettingsMaskBuilder()
if (allowSwapPin) builder.add(Settings.AllowSwapPIN)
if (allowSwapPin2) builder.add(Settings.AllowSwapPIN2)
if (useCvc) builder.add(Settings.UseCVC)
if (isReusable) builder.add(Settings.IsReusable)
if (useOneCommandAtTime) builder.add(Settings.UseOneCommandAtTime)
if (useNdef) builder.add(Settings.UseNdef)
if (useDynamicNdef) builder.add(Settings.UseDynamicNdef)
if (disablePrecomputedNdef) builder.add(Settings.DisablePrecomputedNdef)
if (protocolAllowUnencrypted) builder.add(Settings.ProtocolAllowUnencrypted)
if (protocolAllowStaticEncryption) builder.add(Settings.ProtocolAllowStaticEncryption)
if (forbidDefaultPin) builder.add(Settings.ForbidDefaultPIN)
if (useActivation) builder.add(Settings.UseActivation)
if (useBlock) builder.add(Settings.UseBlock)
if (smartSecurityDelay) builder.add(Settings.SmartSecurityDelay)
if (protectIssuerDataAgainstReplay) builder.add(Settings.ProtectIssuerDataAgainstReplay)
if (forbidPurgeWallet) builder.add(Settings.ProhibitPurgeWallet)
if (allowSelectBlockchain) builder.add(Settings.AllowSelectBlockchain)
if (skipCheckPIN2andCVCIfValidatedByIssuer) builder.add(Settings.SkipCheckPin2andCvcIfValidatedByIssuer)
if (skipSecurityDelayIfValidatedByIssuer) builder.add(Settings.SkipSecurityDelayIfValidatedByIssuer)
if (skipSecurityDelayIfValidatedByLinkedTerminal) builder.add(Settings.SkipSecurityDelayIfValidatedByLinkedTerminal)
if (restrictOverwriteIssuerDataEx) builder.add(Settings.RestrictOverwriteIssuerDataEx)
if (requireTerminalTxSignature) builder.add(Settings.RequireTermTxSignature)
if (requireTerminalCertSignature) builder.add(Settings.RequireTermCertSignature)
if (checkPin3onCard) builder.add(Settings.CheckPIN3onCard)
return builder.build()
}
internal fun CardConfig.createCardId(): String? {
if (series == null) return null
if (startNumber <= 0 || (series.length != 2 && series.length != 4)) return null
val Alf = "ABCDEF0123456789"
fun checkSeries(series: String): Boolean {
val containsList = series.filter { Alf.contains(it) }
return containsList.length == series.length
}
if (!checkSeries(series)) return null
val tail = if (series.length == 2) String.format("%013d", startNumber) else String.format("%011d", startNumber)
var cardId = (series + tail).replace(" ", "")
if (cardId.length != 15 || Alf.indexOf(cardId[0]) == -1 || Alf.indexOf(cardId[1]) == -1)
return null
cardId += "0"
val length = cardId.length
var sum = 0
for (i in 0 until length) {
// get digits in reverse order
var digit: Int
val cDigit = cardId[length - i - 1]
digit = if (cDigit in '0'..'9') cDigit - '0' else cDigit - 'A'
// every 2nd number multiply with 2
if (i % 2 == 1) digit *= 2
sum += if (digit > 9) digit - 9 else digit
}
val lunh = (10 - sum % 10) % 10
return cardId.substring(0, 15) + String.format("%d", lunh)
}

View file

@ -1,10 +0,0 @@
package com.tangem.commands.personalization.entities
import com.tangem.KeyPair
data class Issuer(
val name: String,
val id: String,
val dataKeyPair: KeyPair,
val transactionKeyPair: KeyPair
)

View file

@ -1,8 +0,0 @@
package com.tangem.commands.personalization.entities
import com.tangem.KeyPair
data class Manufacturer(
val keyPair: KeyPair,
val name: String? = null
)

View file

@ -1,13 +0,0 @@
package com.tangem.common
import com.tangem.TangemSdkError
import com.tangem.common.CompletionResult.Success
/**
* Response class encapsulating successful and failed results.
* @param T Type of data that is returned in [Success].
*/
sealed class CompletionResult<T> {
class Success<T>(val data: T) : CompletionResult<T>()
class Failure<T>(val error: TangemSdkError) : CompletionResult<T>()
}

View file

@ -1,14 +0,0 @@
package com.tangem.common
import com.tangem.KeyPair
/**
* Interface for a service for managing Terminal keypair, used for Linked Terminal feature.
* Its implementation Needs to be provided to [com.tangem.TangemSdk]
* by calling [com.tangem.TangemSdk.setTerminalKeysService].
* Default implementation is provided in tangem-sdk module: [TerminalKeysStorage].
* Linked Terminal feature can be disabled manually by editing [com.tangem.Config].
*/
interface TerminalKeysService {
fun getKeys(): KeyPair
}

View file

@ -1,98 +0,0 @@
package com.tangem.common.apdu
import com.tangem.EncryptionMode
import com.tangem.common.extensions.calculateCrc16
import com.tangem.common.extensions.toByteArray
import com.tangem.crypto.encrypt
import java.io.ByteArrayOutputStream
/**
* Class that provides conversion of serialized request and Instruction code
* to a raw data that can be sent to the card.
*
* @property ins Instruction code that determines the type of request for the card.
* @property tlvs Tlvs encoded to a [ByteArray] that are to be sent to the card.
*/
class CommandApdu(
private val ins: Int,
private val tlvs: ByteArray,
private val le: Int = 0x00,
private val encryptionMode: EncryptionMode = EncryptionMode.NONE,
private val encryptionKey: ByteArray? = null,
private val cla: Int = ISO_CLA) {
constructor(
instruction: Instruction,
tlvs: ByteArray,
encryptionMode: EncryptionMode = EncryptionMode.NONE,
encryptionKey: ByteArray? = null
) : this(
instruction.code,
tlvs,
encryptionMode = encryptionMode,
encryptionKey = encryptionKey
)
private val p1: Int
private val p2: Int
init {
if (ins == Instruction.OpenSession.code) {
p1 = 0x00
p2 = encryptionMode.code.toInt()
} else {
p1 = encryptionMode.code.toInt()
p2 = 0x00
}
}
/**
* Request converted to a raw data
*/
val apduData: ByteArray
init {
apduData = toBytes()
}
private fun toBytes(): ByteArray {
val data = if (encryptionKey != null) tlvs.encrypt() else tlvs
val byteStream = ByteArrayOutputStream()
byteStream.write(cla)
byteStream.write(ins)
byteStream.write(p1)
byteStream.write(p2)
if (data.isNotEmpty()) {
byteStream.writeLength(data.size)
byteStream.write(data)
}
return byteStream.toByteArray()
}
private fun ByteArrayOutputStream.writeLength(lc: Int) {
this.write(0)
this.write(lc shr 8)
this.write(lc and 0xFF)
}
private fun ByteArray.encrypt(): ByteArray {
val crc: ByteArray = tlvs.calculateCrc16()
val stream = ByteArrayOutputStream()
stream.write(this.size.toByteArray(2))
stream.write(crc)
stream.write(this)
return stream.toByteArray().encrypt(encryptionKey!!)
}
companion object {
const val ISO_CLA = 0x00
}
}

View file

@ -1,32 +0,0 @@
package com.tangem.common.apdu
/**
* Instruction code that determines the type of the command that is sent to the Tangem card.
* It is used in the construction of [com.tangem.common.apdu.CommandApdu].
*/
enum class Instruction(var code: Int) {
Unknown(0x00),
Personalize(0xF1),
Read(0xF2),
VerifyCard(0xF3),
ValidateCard(0xF4),
VerifyCode(0xF5),
WriteIssuerData(0xF6),
ReadIssuerData(0xF7),
CreateWallet(0xF8),
CheckWallet(0xF9),
SwapPIN(0xFA),
Sign(0xFB),
PurgeWallet(0xFC),
Activate(0xFE),
OpenSession(0xFF),
WriteUserData(0xE0),
ReadUserData(0xE1),
Depersonalize(0xE3);
companion object {
private val values = values()
fun byCode(code: Int): Instruction = values.find { it.code == code } ?: Unknown
}
}

View file

@ -1,66 +0,0 @@
package com.tangem.common.apdu
import com.tangem.common.extensions.calculateCrc16
import com.tangem.common.tlv.Tlv
import com.tangem.crypto.decrypt
import java.io.ByteArrayInputStream
/**
* Stores response data from the card and parses it to [Tlv] and [StatusWord].
*
* @property data Raw response from the card.
* @property sw Status word code, reflecting the status of the response.
* @property statusWord Parsed status word.
*/
class ResponseApdu(private val data: ByteArray) {
private val sw1: Int = 0x00FF and data[data.size - 2].toInt()
private val sw2: Int = 0x00FF and data[data.size - 1].toInt()
val sw: Int = sw1 shl 8 or sw2
val statusWord: StatusWord = StatusWord.byCode(sw)
/**
* Converts raw response data to the list of TLVs.
*
* @param encryptionKey key to decrypt response.
* (Encryption / decryption functionality is not implemented yet.)
*/
fun getTlvData(encryptionKey: ByteArray? = null): List<Tlv>? {
return if (data.size <= 2) {
null
} else {
val responseData = data.copyOf(data.size - 2)
return if (encryptionKey != null) {
if (data.size >= 18) {
val decryptedData = decrypt(responseData, encryptionKey)
Tlv.deserialize(decryptedData)
} else {
null
}
} else {
Tlv.deserialize(responseData)
}
}
}
private fun decrypt(responseData: ByteArray, encryptionKey: ByteArray): ByteArray {
val decryptedData: ByteArray = responseData.decrypt(encryptionKey)
val inputStream = ByteArrayInputStream(decryptedData)
val baLength = ByteArray(2)
inputStream.read(baLength)
val length = (baLength[0].toInt() and 0xFF) * 256 + (baLength[1].toInt() and 0xFF)
if (length > decryptedData.size - 4) throw Exception("Can't decrypt - data size invalid")
val baCRC = ByteArray(2)
inputStream.read(baCRC)
val answerData = ByteArray(length)
inputStream.read(answerData)
val crc: ByteArray = answerData.calculateCrc16()
if (!baCRC.contentEquals(crc)) throw Exception("Can't decrypt - crc invalid")
return answerData
}
}

View file

@ -1,40 +0,0 @@
package com.tangem.common.apdu
import com.tangem.TangemSdkError
/**
* Part of a response from the card, shows the status of the operation
*/
enum class StatusWord(val code: Int, val description: String) {
ProcessCompleted(0x9000, "SW_PROCESS_COMPLETED"),
InvalidParams(0x6A86, "SW_INVALID_PARAMS"),
ErrorProcessingCommand(0x6286, "SW_ERROR_PROCESSING_COMMAND"),
InvalidState(0x6985, "SW_INVALID_STATE"),
Pin1Changed(ProcessCompleted.code + 0x0001, "SW_PIN1_CHANGED"),
Pin2Changed(ProcessCompleted.code + 0x0002, "SW_PIN2_CHANGED"),
PinsChanged(ProcessCompleted.code + 0x0003, "SW_PINS_CHANGED"),
InsNotSupported(0x6D00, "SW_INS_NOT_SUPPORTED"),
NeedEncryption(0x6982, "SW_NEED_ENCRYPTION"),
NeedPause(0x9789, "SW_NEED_PAUSE"),
Unknown(0x0000, "SW_UNKNOWN");
companion object {
private val values = values()
fun byCode(code: Int): StatusWord = values.find { it.code == code } ?: Unknown
}
}
fun StatusWord.toTangemSdkError(): TangemSdkError? {
return when (this) {
StatusWord.ProcessCompleted, StatusWord.Pin1Changed,
StatusWord.Pin2Changed, StatusWord.PinsChanged -> null
StatusWord.NeedPause -> null
StatusWord.InvalidParams -> TangemSdkError.InvalidParams()
StatusWord.ErrorProcessingCommand -> TangemSdkError.ErrorProcessingCommand()
StatusWord.InvalidState -> TangemSdkError.InvalidState()
StatusWord.InsNotSupported -> TangemSdkError.InsNotSupported()
StatusWord.NeedEncryption -> TangemSdkError.NeedEncryption()
StatusWord.Unknown -> TangemSdkError.UnknownStatus()
}
}

View file

@ -1,7 +0,0 @@
package com.tangem.common.extensions
import java.math.BigDecimal
fun BigDecimal.isZero() : Boolean {
return this.compareTo(BigDecimal.ZERO) == 0
}

View file

@ -1,74 +0,0 @@
package com.tangem.common.extensions
import org.spongycastle.crypto.digests.RIPEMD160Digest
import org.spongycastle.jce.ECNamedCurveTable
import java.nio.ByteBuffer
import java.security.MessageDigest
import java.util.*
import kotlin.experimental.and
import kotlin.experimental.xor
/**
* Extension functions for [ByteArray].
*/
fun ByteArray.toHexString(): String = joinToString("") { "%02x".format(it) }
fun ByteArray.toUtf8(): String = String(this).removeSuffix("\u0000")
fun ByteArray.toInt(): Int {
return when (this.size) {
1 -> (this[0] and 0xFF.toByte()).toInt()
2 -> ByteBuffer.wrap(this).short.toInt()
4 -> ByteBuffer.wrap(this).int
else -> throw IllegalArgumentException("Length must be 1,2 or 4. Length = " + this.size)
}
}
fun ByteArray.toDate(): Date {
val year = copyOfRange(0, 2).toInt()
val month = if (this.size > 2) this[2] - 1 else 0
val day = if (this.size > 3) this[3].toInt() else 0
val cd = Calendar.getInstance()
cd.set(year, month, day, 0, 0, 0)
return cd.time
}
fun ByteArray.calculateSha512(): ByteArray = MessageDigest.getInstance("SHA-512").digest(this)
fun ByteArray.calculateSha256(): ByteArray = MessageDigest.getInstance("SHA-256").digest(this)
fun ByteArray.calculateRipemd160(): ByteArray {
val digest = RIPEMD160Digest()
digest.update(this, 0, this.size)
val out = ByteArray(20)
digest.doFinal(out, 0)
return out
}
fun ByteArray.toCompressedPublicKey(): ByteArray {
return if (this.size == 65) {
val spec = ECNamedCurveTable.getParameterSpec("secp256k1")
val publicKeyPoint = spec.curve.decodePoint(this)
publicKeyPoint.getEncoded(true)
} else {
this
}
}
fun ByteArray.calculateCrc16(): ByteArray {
var chBlock: Byte
// STEP 1 Initialize the CRC-16 value
var wCRC = 0x6363 // ITU-V.41
var i = 0
// STEP 2 Update data and Calucuate their CRC
do {
chBlock = this.get(i++)
chBlock = chBlock xor (wCRC and 0x00FF).toByte()
val chBlockInt = (chBlock.toInt() xor (chBlock.toInt() shl 4))
wCRC = wCRC shr 8 xor (chBlockInt and 0xFF shl 8) and 0xFFFF xor (chBlockInt and 0xFF shl 3 and 0xFFFF) xor (chBlockInt and 0xFF shr 4 and 0xFFFF)
// (wCRC>>8)^((int)chBlock<<8)^((int) chBlock<<3)^((int)chBlock>>4);
} while (i < this.size)
return byteArrayOf((wCRC and 0xFF).toByte(), (wCRC and 0xFFFF shr 8).toByte())
}

View file

@ -1,22 +0,0 @@
package com.tangem.common.extensions
import com.tangem.commands.Card
fun Card.getType(): CardType {
val firmware = this.firmwareVersion ?: return CardType.Unknown
return when {
firmware.endsWith("d SDK") -> {
CardType.Sdk
}
firmware.endsWith("r") -> {
CardType.Release
}
else -> {
CardType.Unknown
}
}
}
enum class CardType {
Sdk, Release, Unknown
}

View file

@ -1,16 +0,0 @@
package com.tangem.common.extensions
import java.nio.ByteBuffer
fun Int.toByteArray(size: Int = Int.SIZE_BYTES): ByteArray {
if (size == Int.SIZE_BYTES) {
val buffer = ByteBuffer.allocate(size)
buffer.putInt(this)
return buffer.array()
} else if (size == Short.SIZE_BYTES){
return byteArrayOf(
(this ushr 8).toByte(),
this.toByte())
}
return byteArrayOf()
}

View file

@ -1,13 +0,0 @@
package com.tangem.common.extensions
fun <T> List<T>.print(delimiter: String = ", ", wrap: Boolean = true): String {
val builder = StringBuilder()
forEach { builder.append(it).append(delimiter) }
val length = builder.length
if (length > delimiter.length) {
builder.delete(length - delimiter.length, length)
}
val result = builder.toString()
return if (wrap) "[$result]" else result
}

View file

@ -1,26 +0,0 @@
package com.tangem.common.extensions
import java.nio.charset.Charset
import java.security.MessageDigest
/**
* Extension functions for [String].
*/
fun String.calculateSha256(): ByteArray {
val sha256 = MessageDigest.getInstance("SHA-256")
val data = this.toByteArray(Charset.forName("UTF-8"))
return sha256.digest(data)
}
fun String.calculateSha512(): ByteArray {
val sha = MessageDigest.getInstance("SHA-512")
val data = this.toByteArray(Charset.forName("UTF-8"))
return sha.digest(data)
}
fun String.hexToBytes(): ByteArray {
return ByteArray(this.length / 2)
{ i ->
Integer.parseInt(this.substring(2 * i, 2 * i + 2), 16).toByte()
}
}

View file

@ -1,105 +0,0 @@
package com.tangem.common.tlv
import com.tangem.Log
import com.tangem.common.extensions.toHexString
import java.io.ByteArrayInputStream
import java.io.IOException
/**
* The data converted to the Tag Length Value protocol.
*/
class Tlv {
val tag: TlvTag
val value: ByteArray
val tagRaw: Int
constructor(tagCode: Int, value: ByteArray = byteArrayOf()) {
this.tag = TlvTag.byCode(tagCode)
this.tagRaw = tagCode
this.value = value
}
constructor(tag: TlvTag, value: ByteArray = byteArrayOf()) {
this.tag = tag
this.tagRaw = tag.code
this.value = value
}
companion object {
private fun tlvFromBytes(stream: ByteArrayInputStream): Tlv? {
val code = stream.read()
if (code == -1) return null
var len = stream.read()
if (len == -1)
throw IOException("Can't read TLV")
if (len == 0xFF) {
val lenH = stream.read()
if (lenH == -1)
throw IOException("Can't read TLV")
len = stream.read()
if (len == -1)
throw IOException("Can't read TLV")
len = len or (lenH shl 8)
}
val value = ByteArray(len)
if (len > 0) {
if (len != stream.read(value)) {
throw IOException("Can't read TLV")
}
}
val tag = TlvTag.byCode(code)
return if (tag == TlvTag.Unknown) Tlv(code, value) else Tlv(tag, value)
}
fun deserialize(data: ByteArray, nfcV: Boolean = false): List<Tlv>? {
val tlvList = mutableListOf<Tlv>()
val stream = ByteArrayInputStream(data)
var tlv: Tlv?
do {
try {
tlv = tlvFromBytes(stream)
if (tlv != null) tlvList.add(tlv)
} catch (e: IOException) {
Log.e(this::class.java.simpleName,"TLVError: " + e.message)
if (nfcV) break else return null
}
} while (tlv != null)
return tlvList
}
}
override fun toString(): String {
return "${this.tag} ($tagRaw): ${value.toHexString()}"
}
}
fun List<Tlv>.serialize(): ByteArray =
this.map { it.serialize() }.reduce { arr1, arr2 -> arr1 + arr2 }
fun Tlv.serialize(): ByteArray {
val tag = byteArrayOf(this.tag.code.toByte())
val length = getLengthInBytes(this.value.size)
val value = if (this.value.isNotEmpty()) this.value else byteArrayOf(0x00)
return tag + length + value
}
private fun getLengthInBytes(tlvLength: Int): ByteArray {
return if (tlvLength > 0) {
if (tlvLength > 0xFE) {
byteArrayOf(
0xFF.toByte(),
(tlvLength shr 8 and 0xFF).toByte(),
(tlvLength and 0xFF).toByte()
)
} else {
byteArrayOf((tlvLength and 0xFF).toByte())
}
} else {
byteArrayOf()
}
}

View file

@ -1,21 +0,0 @@
package com.tangem.common.tlv
import com.tangem.Log
class TlvBuilder {
private val tlvs = mutableListOf<Tlv>()
private val encoder = TlvEncoder()
internal inline fun <reified T> append(tag: TlvTag, value: T?) {
if (value == null) return
tlvs.add(encoder.encode(tag, value))
}
fun serialize(): ByteArray {
Log.v("TLV",
"Data encoded to TLVs:\n${tlvs.joinToString("\n")}")
return tlvs.serialize()
}
}

View file

@ -1,162 +0,0 @@
package com.tangem.common.tlv
import com.tangem.Log
import com.tangem.TangemSdkError
import com.tangem.commands.*
import com.tangem.commands.common.IssuerDataMode
import com.tangem.common.extensions.toDate
import com.tangem.common.extensions.toHexString
import com.tangem.common.extensions.toInt
import com.tangem.common.extensions.toUtf8
import java.util.*
/**
* Maps value fields in [Tlv] from raw [ByteArray] to concrete classes
* according to their [TlvTag] and corresponding [TlvValueType].
*
* @property tlvList List of TLVs, which values are to be converted to particular classes.
*/
class TlvDecoder(val tlvList: List<Tlv>) {
init {
Log.v("TLV",
"Decoding data from TLV:\n${tlvList.joinToString("\n")}")
}
/**
* Finds [Tlv] by its [TlvTag].
* Returns null if [Tlv] is not found, otherwise converts its value to [T].
*
* @param tag [TlvTag] of a [Tlv] which value is to be returned.
*
* @return Value converted to a nullable type [T].
*/
inline fun <reified T> decodeOptional(tag: TlvTag): T? =
try {
decode<T>(tag, false)
} catch (exception: TangemSdkError.DecodingFailedMissingTag) {
null
}
/**
* Finds [Tlv] by its [TlvTag].
* Throws [TaskError.MissingTag] if [Tlv] is not found,
* otherwise converts [Tlv] value to [T].
*
* @param tag [TlvTag] of a [Tlv] which value is to be returned.
*
* @return [Tlv] value converted to a nullable type [T].
*
* @throws [TangemSdkError.DecodingFailedMissingTag] exception if no [Tlv] is found by the Tag.
*/
inline fun <reified T> decode(tag: TlvTag, logError: Boolean = true): T {
val tlvValue: ByteArray = tlvList.find { it.tag == tag }?.value
?: if (tag.valueType() == TlvValueType.BoolValue && T::class == Boolean::class) {
return false as T
} else {
if (logError) {
Log.e(this::class.simpleName!!, "TLV $tag not found")
} else {
Log.v(this::class.simpleName!!, "TLV $tag not found, but it is not required")
}
throw TangemSdkError.DecodingFailedMissingTag()
}
return when (tag.valueType()) {
TlvValueType.HexString, TlvValueType.HexStringToHash -> {
typeCheck<T, String>(tag)
tlvValue.toHexString() as T
}
TlvValueType.Utf8String -> {
typeCheck<T, String>(tag)
tlvValue.toUtf8() as T
}
TlvValueType.Uint16, TlvValueType.Uint32 -> {
typeCheck<T, Int>(tag)
try {
tlvValue.toInt() as T
} catch (exception: IllegalArgumentException) {
Log.e(this::class.simpleName!!, exception.message ?: "")
throw TangemSdkError.DecodingFailed()
}
}
TlvValueType.BoolValue -> {
typeCheck<T, Boolean>(tag)
true as T
}
TlvValueType.ByteArray -> {
typeCheck<T, ByteArray>(tag)
tlvValue as T
}
TlvValueType.EllipticCurve -> {
typeCheck<T, EllipticCurve>(tag)
try {
EllipticCurve.byName(tlvValue.toUtf8()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toUtf8(), exception)
throw TangemSdkError.DecodingFailed()
}
}
TlvValueType.DateTime -> {
typeCheck<T, Date>(tag)
try {
tlvValue.toDate() as T
} catch (exception: Exception) {
logException(tag, tlvValue.toHexString(), exception)
throw TangemSdkError.DecodingFailed()
}
}
TlvValueType.ProductMask -> {
typeCheck<T, ProductMask>(tag)
ProductMask(tlvValue.toInt()) as T
}
TlvValueType.SettingsMask -> {
typeCheck<T, SettingsMask>(tag)
SettingsMask(tlvValue.toInt()) as T
}
TlvValueType.CardStatus -> {
typeCheck<T, CardStatus>(tag)
try {
CardStatus.byCode(tlvValue.toInt()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toInt().toString(), exception)
throw TangemSdkError.DecodingFailed()
}
}
TlvValueType.SigningMethod -> {
typeCheck<T, SigningMethodMask>(tag)
try {
SigningMethodMask(tlvValue.toInt()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toInt().toString(), exception)
throw TangemSdkError.DecodingFailed()
}
}
TlvValueType.IssuerDataMode -> {
typeCheck<T, IssuerDataMode>(tag)
try {
IssuerDataMode.byCode(tlvValue.toInt().toByte()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toInt().toString(), exception)
throw TangemSdkError.DecodingFailed()
}
}
}
}
fun logException(tag: TlvTag, value: String, exception: Exception) {
Log.e(this::class.simpleName!!,
"Unknown ${tag.name} with value of: value, \n${exception.message}")
}
inline fun <reified T, reified ExpectedT> typeCheck(tag: TlvTag) {
if (T::class != ExpectedT::class) {
Log.e(this::class.simpleName!!,
"Mapping error. Type for tag: $tag must be ${tag.valueType()}. It is ${T::class}")
throw TangemSdkError.DecodingFailedTypeMismatch()
}
}
}

Some files were not shown because too many files have changed in this diff Show more