Updated on 2026-08-14

This commit is contained in:
Tangem 2020-01-27 14:55:01 +03:00
commit 90d7a293ab
58 changed files with 2369 additions and 284 deletions

View file

@ -23,10 +23,13 @@ public enum Blockchain {
BinanceTestNet("BINANCE/test", "BNB", 100000000.0, R.drawable.tangem2, "Binance Testnet"),
Matic("MATIC", "MTX", 1.0, R.drawable.tangem2, "Matic"),
MaticTestNet("MATIC/test", "MTX", 1.0, R.drawable.tangem2, "Matic Testnet"),
Stellar("XLM", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar"),
StellarTestNet("XLM/test", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar Testnet"),
Stellar("XLM", "XLM", 10000000.0, R.drawable.ic_logo_stellar, "Stellar"),
StellarTestNet("XLM/test", "XLM", 10000000.0, R.drawable.ic_logo_stellar, "Stellar Testnet"),
StellarAsset("Asset", "XLM", 10000000.0, R.drawable.ic_logo_stellar, "Stellar"),
Eos("EOS", "EOS", 10000.0, R.drawable.tangem2, "EOS");
StellarTag("XLM-Tag", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar"),
Eos("EOS", "EOS", 10000.0, R.drawable.tangem2, "EOS"),
Ducatus("DUC", "DUC", 100000000.0, R.drawable.tangem2, "Ducatus"),
Tezos("TEZOS", "XTZ", 10000000.0, R.drawable.tangem2, "Tezos");
Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) {
mID = ID;

View file

@ -2,6 +2,7 @@ package com.tangem.data.network;
import com.tangem.data.network.model.InsightBody;
import com.tangem.data.network.model.InsightResponse;
import com.tangem.data.network.model.InsightUtxo;
import java.util.List;
@ -18,7 +19,7 @@ public interface InsightApi {
Call<InsightResponse> insightAddress(@Path("address") String address);
@GET(ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS)
Call<List<InsightResponse>> insightUnspent(@Path("address") String address);
Call<List<InsightUtxo>> insightUnspent(@Path("address") String address);
@GET(ServerApiInsight.INSIGHT_TRANSACTION)
Call<InsightResponse> insightTransaction(@Path("txId") String txId);

View file

@ -6,6 +6,7 @@ import androidx.annotation.NonNull;
import com.tangem.data.network.model.InsightBody;
import com.tangem.data.network.model.InsightResponse;
import com.tangem.data.network.model.InsightUtxo;
import java.util.List;
@ -18,11 +19,11 @@ import retrofit2.converter.gson.GsonConverterFactory;
public class ServerApiInsight {
private static String TAG = ServerApiInsight.class.getSimpleName();
public static final String INSIGHT_ADDRESS = "/addr/{address}";
public static final String INSIGHT_UNSPENT_OUTPUTS = "/addr/{address}/utxo";
public static final String INSIGHT_TRANSACTION = "/rawtx/{txId}";
public static final String INSIGHT_FEE = "/utils/estimatefee?nbBlocks=2,3,6";
public static final String INSIGHT_SEND = "/tx/send";
public static final String INSIGHT_ADDRESS = "addr/{address}";
public static final String INSIGHT_UNSPENT_OUTPUTS = "addr/{address}/utxo";
public static final String INSIGHT_TRANSACTION = "rawtx/{txId}";
public static final String INSIGHT_FEE = "utils/estimatefee?nbBlocks=2,3,6";
public static final String INSIGHT_SEND = "tx/send";
private int requestsCount = 0;
@ -38,7 +39,7 @@ public class ServerApiInsight {
public interface ResponseListener {
void onSuccess(String method, InsightResponse insightResponse);
void onSuccess(String method, List<InsightResponse> utxoList);
void onSuccess(String method, List<InsightUtxo> utxoList);
void onFail(String method, String message);
}
@ -49,7 +50,7 @@ public class ServerApiInsight {
public void requestData(String method, String wallet, String tx) {
requestsCount++;
String insightURL = "http://130.185.109.17:3001/insigth-api"; //TODO: make random selection
String insightURL = "https://insight.ducatus.io/insight-lite-api/"; //TODO: make random selection
this.lastNode = insightURL; //TODO: show node instead of URL
Retrofit retrofitInsight = new Retrofit.Builder()
@ -61,10 +62,10 @@ public class ServerApiInsight {
InsightApi insightApi = retrofitInsight.create(InsightApi.class);
if (method.equals(INSIGHT_UNSPENT_OUTPUTS)) {
Call<List<InsightResponse>> call = insightApi.insightUnspent(wallet);
call.enqueue(new Callback<List<InsightResponse>>() {
Call<List<InsightUtxo>> call = insightApi.insightUnspent(wallet);
call.enqueue(new Callback<List<InsightUtxo>>() {
@Override
public void onResponse(@NonNull Call<List<InsightResponse>> call, @NonNull Response<List<InsightResponse>> response) {
public void onResponse(@NonNull Call<List<InsightUtxo>> call, @NonNull Response<List<InsightUtxo>> response) {
requestsCount--;
if (response.code() == 200) {
@ -77,7 +78,7 @@ public class ServerApiInsight {
}
@Override
public void onFailure(@NonNull Call<List<InsightResponse>> call, @NonNull Throwable t) {
public void onFailure(@NonNull Call<List<InsightUtxo>> call, @NonNull Throwable t) {
requestsCount--;
responseListener.onFail(method, String.valueOf(t.getMessage()));
Log.e(TAG, "requestData " + method + " onFailure " + t.getMessage());
@ -92,13 +93,9 @@ public class ServerApiInsight {
call = insightApi.insightAddress(wallet);
break;
case INSIGHT_TRANSACTION:
call = insightApi.insightTransaction(tx);
break;
case INSIGHT_FEE:
call = insightApi.insightFee();
break;
// case INSIGHT_FEE:
// call = insightApi.insightFee();
// break;
case INSIGHT_SEND:
call = insightApi.insightSend(new InsightBody(tx));

View file

@ -33,7 +33,7 @@ import io.reactivex.schedulers.Schedulers;
public class ServerApiStellar {
public ServerApiStellar(Blockchain blockchain) {
if (blockchain == Blockchain.Stellar || blockchain == Blockchain.StellarAsset) {
if (blockchain == Blockchain.Stellar || blockchain == Blockchain.StellarAsset || blockchain == Blockchain.StellarTag) {
currentURL = ServerURL.API_STELLAR;
} else {
currentURL = ServerURL.API_STELLAR_TESTNET;
@ -173,10 +173,11 @@ public class ServerApiStellar {
stellarRequest.setError(null);
try {
Server server;
if (ctx.getBlockchain() == Blockchain.Stellar || ctx.getBlockchain() == Blockchain.StellarAsset) {
Blockchain blockchain = ctx.getBlockchain();
if (blockchain == Blockchain.Stellar || blockchain == Blockchain.StellarAsset || blockchain == Blockchain.StellarTag) {
Network.usePublicNetwork();
server = new Server(currentURL);
} else if (ctx.getBlockchain() == Blockchain.StellarTestNet) {
} else if (blockchain == Blockchain.StellarTestNet) {
Network.useTestNetwork();
server = new Server(currentURL);
} else {

View file

@ -0,0 +1,139 @@
package com.tangem.data.network;
import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import com.tangem.data.network.model.TezosAccountResponse;
import com.tangem.data.network.model.TezosForgeBody;
import com.tangem.data.network.model.TezosHeaderResponse;
import com.tangem.data.network.model.TezosPreapplyBody;
import com.tangem.tangem_card.util.Log;
import java.util.ArrayList;
import java.util.List;
import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.converter.scalars.ScalarsConverterFactory;
public class ServerApiTezos {
private static String TAG = ServerApiTezos.class.getSimpleName();
private final String letzbakeURI = "https://teznode.letzbake.com";
private final String tezrpcURI = "https://mainnet.tezrpc.me";
static final String TEZOS_ADDRESS = "chains/main/blocks/head/context/contracts/{address}";
static final String TEZOS_HEADER = "chains/main/blocks/head/header";
static final String TEZOS_MANAGER_KEY = "chains/main/blocks/head/context/contracts/{address}/manager_key";
static final String TEZOS_FORGE_OPERATIONS = "chains/main/blocks/head/helpers/forge/operations";
static final String TEZOS_PREAPPLY_OPERATIONS = "chains/main/blocks/head/helpers/preapply/operations";
static final String TEZOS_RUN_OPERATION = "chains/main/blocks/head/helpers/scripts/run_operation";
static final String TEZOS_INJECT_OPERATIONS = "injection/operation";
private Retrofit retrofitTezos = new Retrofit.Builder()
.baseUrl(letzbakeURI)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(ScalarsConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
//logging for testing
.client(new OkHttpClient.Builder().addInterceptor(
new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
).build())
.build();
private TezosApi tezosApi = retrofitTezos.create(TezosApi.class);
private int requestsCount = 0;
public boolean isRequestsSequenceCompleted() {
Log.i(TAG, String.format("isRequestsSequenceCompleted: %s (%d requests left)", String.valueOf(requestsCount <= 0), requestsCount));
return requestsCount <= 0;
}
public void getAddress(String wallet, SingleObserver<TezosAccountResponse> accountObserver) {
requestsCount++;
Log.i(TAG, "new getAddress request");
Single<TezosAccountResponse> accountSingle = tezosApi.getAccount(wallet)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((object, throwable) -> requestsCount--);
accountSingle.subscribe(accountObserver);
}
public void getMangerKey(String wallet, SingleObserver<String> accountObserver) {
requestsCount++;
Log.i(TAG, "new getManagerKey request");
Single<String> managerKeySingle = tezosApi.getManagerKey(wallet)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((object, throwable) -> requestsCount--);
managerKeySingle.subscribe(accountObserver);
}
public TezosHeaderResponse getHeader() throws Exception { // TODO? not async
requestsCount++;
Log.i(TAG, "new getHeader request");
Response<TezosHeaderResponse> headerResponse = tezosApi.getHeader().execute();
requestsCount--;
if (headerResponse.code() == 200) {
return headerResponse.body();
} else {
throw new Exception("Wrong header response, code: " + headerResponse.code());
}
}
public String forgeOperations(TezosForgeBody tezosForgeBody) throws Exception { // TODO? not async
requestsCount++;
Log.i(TAG, "new forgeOperations request");
Response<String> forgeResponse = tezosApi.forgeOperations(tezosForgeBody).execute();
requestsCount--;
if (forgeResponse.code() == 200) {
return forgeResponse.body();
} else {
throw new Exception("Wrong forge response, code: " + forgeResponse.code());
}
}
public void peapplyOperations(TezosPreapplyBody tezosPreapplyBody) throws Exception {
Log.i(TAG, "new peapplyOperations request");
List<TezosPreapplyBody> tezosPreapplyBodyList = new ArrayList<>();
tezosPreapplyBodyList.add(tezosPreapplyBody);
Response<Void> preapplyResponse = tezosApi.preapplyOperations(tezosPreapplyBodyList).execute();
if (preapplyResponse.code() != 200) {
String error = "Preapply error: unknown error";
if (preapplyResponse.errorBody() != null) {
error = "Preapply error: " + preapplyResponse.errorBody().string();
}
Log.e(TAG, error);
throw new Exception(error);
}
}
public void injectOperations(String txForSend, SingleObserver<Object> injectObserver) {
requestsCount++;
Log.i(TAG, "new injectOperations request");
Single<Object> injectSingle = tezosApi.injectOperations(txForSend)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnEvent((object, throwable) -> requestsCount--);
injectSingle.subscribe(injectObserver);
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.data.network;
import com.tangem.data.network.model.TezosAccountResponse;
import com.tangem.data.network.model.TezosForgeBody;
import com.tangem.data.network.model.TezosHeaderResponse;
import com.tangem.data.network.model.TezosPreapplyBody;
import java.util.List;
import io.reactivex.Single;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.POST;
import retrofit2.http.Path;
public interface TezosApi {
@GET(ServerApiTezos.TEZOS_ADDRESS)
Single<TezosAccountResponse> getAccount(@Path("address") String address);
@GET(ServerApiTezos.TEZOS_HEADER)
Call<TezosHeaderResponse> getHeader();
@GET(ServerApiTezos.TEZOS_MANAGER_KEY)
Single<String> getManagerKey(@Path("address") String address);
@POST(ServerApiTezos.TEZOS_FORGE_OPERATIONS)
Call<String> forgeOperations(@Body TezosForgeBody tezosForgeBody);
@POST(ServerApiTezos.TEZOS_PREAPPLY_OPERATIONS)
Call<Void> preapplyOperations(@Body List<TezosPreapplyBody> tezosPreapplyBodyList);
@POST(ServerApiTezos.TEZOS_INJECT_OPERATIONS)
Single<Object> injectOperations(@Body String txForSend);
}

View file

@ -12,27 +12,29 @@ data class InsightResponse(
@SerializedName("addrStr")
var addrStr: String = "",
// @SerializedName("2")
// var fee2: String = "",
//
// @SerializedName("3")
// var fee3: String = "",
//
// @SerializedName("6")
// var fee6: String = "",
@SerializedName("error")
var error: String = ""
)
data class InsightUtxo(
@SerializedName("txid")
var txid: String = "",
@SerializedName("satoshis")
var satoshis: Long? = null,
@SerializedName("height")
var height: Int? = null,
@SerializedName("vout")
var vout: Int? = null,
@SerializedName("2")
var fee2: String = "",
@SerializedName("3")
var fee3: String = "",
@SerializedName("6")
var fee6: String = "",
@SerializedName("rawtx")
var rawtx: String = "",
@SerializedName("error")
var error: String = ""
@SerializedName("scriptPubKey")
var scriptPubKey: String? = null
)

View file

@ -0,0 +1,25 @@
package com.tangem.data.network.model
data class TezosForgeBody(
val branch: String,
val contents: List<TezosOperationContent>
)
data class TezosOperationContent(
val kind: String,
val source: String,
val fee: String,
val counter: String,
val gas_limit: String,
val storage_limit: String,
val public_key: String? = null,
val destination: String? = null,
val amount: String? = null
)
data class TezosPreapplyBody(
val protocol: String,
val branch: String,
val contents: List<TezosOperationContent>,
val signature: String
)

View file

@ -0,0 +1,19 @@
package com.tangem.data.network.model
import com.google.gson.annotations.SerializedName
data class TezosAccountResponse(
@SerializedName("balance")
var balance: Long? = null,
@SerializedName("counter")
var counter: Long? = null
)
data class TezosHeaderResponse(
@SerializedName("protocol")
var protocol: String? = null,
@SerializedName("hash")
var hash: String? = null
)

View file

@ -7,10 +7,12 @@ import android.net.Uri
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.nfc.tech.NfcV
import android.os.Bundle
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
import android.util.Log
import android.view.*
import android.widget.PopupMenu
import android.widget.TextView
@ -20,14 +22,21 @@ import androidx.core.os.bundleOf
import androidx.lifecycle.ViewModelProviders
import com.tangem.App
import com.tangem.Constant
import com.tangem.data.Blockchain
import com.tangem.data.Logger
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_card.reader.CardProtocol
import com.tangem.tangem_card.reader.TLV
import com.tangem.tangem_card.reader.TLVException
import com.tangem.tangem_card.reader.TLVList
import com.tangem.tangem_card.tasks.CustomReadCardTask
import com.tangem.tangem_card.tasks.ReadCardInfoTask
import com.tangem.tangem_sdk.android.data.PINStorage
import com.tangem.tangem_sdk.android.nfc.NfcDeviceAntennaLocation
import com.tangem.tangem_sdk.android.reader.NfcReader
import com.tangem.tangem_sdk.android.reader.NfcVReader
import com.tangem.tangem_sdk.android.reader.ReadResult
import com.tangem.tangem_sdk.android.reader.ReadSlixTagTask
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD
import com.tangem.tangem_sdk.data.EXTRA_TANGEM_CARD_UID
import com.tangem.tangem_sdk.data.loadFromBundle
@ -45,10 +54,16 @@ import com.tangem.wallet.BuildConfig
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import com.tangem.wallet.xlmTag.XlmTagEngine
import kotlinx.android.synthetic.main.fragment_main.*
import kotlinx.android.synthetic.main.layout_touch_card.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.File
import java.util.*
import kotlin.coroutines.CoroutineContext
class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.ReaderCallback,
CardProtocol.Notifications, androidx.appcompat.widget.PopupMenu.OnMenuItemClickListener,
@ -68,6 +83,11 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
private var zipFile: File? = null
private var unknownBlockchain = false
private val parentJob = Job()
private val coroutineContext: CoroutineContext
get() = parentJob + Dispatchers.IO
private val scope = CoroutineScope(coroutineContext)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
setHasOptionsMenu(true)
return super.onCreateView(inflater, container, savedInstanceState)
@ -109,7 +129,7 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
tvBuyCards?.setText(spannable, TextView.BufferType.SPANNABLE)
llShoppingView?.setOnClickListener {
val uri = Uri.parse ("https://www.tangemcards.com")
val uri = Uri.parse("https://www.tangemcards.com")
val intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(intent)
}
@ -152,6 +172,8 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
return
}
NfcV.get(tag)?.let { onNfcVDiscovered(it, tag.id) }
try {
// get IsoDep handle and run cardReader thread
val isoDep = IsoDep.get(tag)
@ -175,6 +197,44 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
}
}
private fun onNfcVDiscovered(nfcV: NfcV, uid: ByteArray) {
scope.launch {
when (val readResult = ReadSlixTagTask(NfcVReader(nfcV)).read()) {
is ReadResult.Failure -> (activity as MainActivity).nfcManager.notifyReadResult(false)
is ReadResult.Success -> {
try {
val tlvs = readResult.tlvs
val cardDataTlv = TLVList.fromBytes((tlvs.getTLV(TLV.Tag.TAG_CardData)).Value)
Log.v(TAG, "\n" + tlvs.getParsedTLVs(""))
val card = TangemCard(uid.toString())
card.batch = cardDataTlv.getTLV(TLV.Tag.TAG_Batch).asHexString
card.setIssuer(cardDataTlv.getTLV(TLV.Tag.TAG_Issuer_ID).Value.toString(), null)
card.blockchainID = Blockchain.StellarTag.id
card.walletPublicKey = tlvs.getTLV(TLV.Tag.TAG_Wallet_PublicKey).Value
card.status = TangemCard.Status.Loaded
card.tagSignature = tlvs.getTLV(TLV.Tag.TAG_Signature).Value
val ctx = TangemContext(card)
val engineCoin = XlmTagEngine(ctx)
engineCoin.defineWallet()
launch(Dispatchers.Main) {
val bundle = Bundle()
bundle.putParcelable(Constant.EXTRA_LAST_DISCOVERED_TAG, lastTag)
ctx.saveToBundle(bundle)
navigateForResult(Constant.REQUEST_CODE_SHOW_CARD_ACTIVITY,
R.id.action_main_to_tagFragment, bundle)
}
} catch (e: TLVException) {
e.printStackTrace()
Log.v(TAG, e.message)
}
}
}
}
}
override fun onReadStart(cardProtocol: CardProtocol) {
rlProgressBar?.post { rlProgressBar?.visibility = View.VISIBLE }
}
@ -225,8 +285,13 @@ class MainFragment : BaseFragment(), NavigationResultListener, NfcAdapter.Reader
}
}
card.status == TangemCard.Status.Empty -> {
val bundle = Bundle().apply { ctx.saveToBundle(this) }
navigateToDestination(R.id.action_main_to_emptyWalletFragment, bundle)
val engineCoin = CoinEngineFactory.create(ctx)
if (engineCoin != null) {
val bundle = Bundle().apply { ctx.saveToBundle(this) }
navigateToDestination(R.id.action_main_to_emptyWalletFragment, bundle)
} else {
showUnkownBlockchainWarning()
}
}
card.status == TangemCard.Status.Purged -> Toast.makeText(context, R.string.main_screen_erased_wallet, Toast.LENGTH_SHORT).show()
card.status == TangemCard.Status.NotPersonalized -> Toast.makeText(context, R.string.main_screen_not_personalized, Toast.LENGTH_SHORT).show()

View file

@ -0,0 +1,212 @@
package com.tangem.ui.fragment.additional
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.text.Html
import android.view.View
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.tangem.data.network.ServerApiCommon
import com.tangem.server_android.ServerApiTangem
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.fragment.wallet.LoadedWalletFragment
import com.tangem.ui.fragment.wallet.LoadedWalletViewModel
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.LOG
import com.tangem.util.UtilHelper
import com.tangem.wallet.*
import com.tangem.wallet.xlmTag.XlmTagEngine
import kotlinx.android.synthetic.main.fr_loaded_wallet.*
import kotlinx.android.synthetic.main.layout_btn_details.*
import kotlinx.android.synthetic.main.layout_tangem_card.*
class TagFragment : BaseFragment(), NavigationResultListener {
override val layoutId = R.layout.fragment_tag
private lateinit var viewModel: LoadedWalletViewModel
private lateinit var ctx: TangemContext
private var serverApiCommon: ServerApiCommon = ServerApiCommon()
private var serverApiTangem: ServerApiTangem = ServerApiTangem()
private var requestCounter: Int = 0
set(value) {
field = value
LOG.i(LoadedWalletFragment.TAG, "requestCounter, set $field")
if (field <= 0) {
LOG.e(LoadedWalletFragment.TAG, "+++++++++++ FINISH REFRESH")
if (srl != null && srl.isRefreshing)
srl.isRefreshing = false
} else if (srl != null && !srl.isRefreshing)
srl.isRefreshing = true
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ctx = TangemContext.loadFromBundle(context, arguments)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val engine = XlmTagEngine(ctx)
btnLoad.visibility = View.GONE
btnDetails.visibility = View.GONE
btnExtract.text = getString(R.string.tag_claim)
btnExtract.isEnabled = false //TODO: enable when we implement extraction
btnExtract.backgroundTintList =
ContextCompat.getColorStateList(requireContext(), R.color.btn_dark)
ivTangemCard.setImageResource(R.drawable.card_tgslix)
tvBalance.setSingleLine(!engine.needMultipleLinesForBalance())
tvWallet.text = ctx.coinData.wallet
tvWallet.setOnClickListener { shareWallet() }
btnExplore.setOnClickListener { startActivity(Intent(Intent.ACTION_VIEW, engine.walletExplorerUri)) }
btnCopy.setOnClickListener { shareWallet() }
btnNewScan.setOnClickListener { navigateUp() }
srl?.setOnRefreshListener { refresh(true) }
requestBalanceAndUnspentTransactions()
}
private fun shareWallet() {
val txtShare = ctx.coinData.wallet
val clipboard = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.primaryClip = ClipData.newPlainText(txtShare, txtShare)
Toast.makeText(activity, R.string.loaded_wallet_toast_copied, Toast.LENGTH_LONG).show()
}
private fun update() {
ctx.coinData.setIsBalanceEqual(true)
if (srl.isRefreshing) {
tvBalanceLine1.setTextColor(resources.getColor(R.color.primary))
tvBalanceLine1.text = getString(R.string.loaded_wallet_verifying_in_blockchain)
tvBalanceLine2.text = ""
tvBalance.text = ""
tvBalanceEquivalent.text = ""
} else {
val validator = BalanceValidator()
validator.check(ctx, false)
context?.let { ContextCompat.getColor(it, validator.color) }?.let { tvBalanceLine1?.setTextColor(it) }
tvBalanceLine1?.text = getString(validator.firstLine)
tvBalanceLine2?.text = getString(validator.getSecondLine(false))
}
val engine = CoinEngineFactory.create(ctx)
when {
engine!!.hasBalanceInfo() -> {
@Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
Html.fromHtml(engine.balanceHTML, Html.FROM_HTML_MODE_LEGACY)
else
Html.fromHtml(engine.balanceHTML)
tvBalance.text = html
tvBalanceEquivalent.text = engine.balanceEquivalent
}
ctx.card?.offlineBalance != null -> {
@Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
Html.fromHtml(engine.offlineBalanceHTML, Html.FROM_HTML_MODE_LEGACY)
else
Html.fromHtml(engine.offlineBalanceHTML)
tvBalance.text = html
}
else -> tvBalance.text = ""
}
@Suppress("DEPRECATION") val html = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
Html.fromHtml(ctx.blockchainName, Html.FROM_HTML_MODE_LEGACY)
else
Html.fromHtml(ctx.blockchainName)
tvBlockchain.text = html
}
private fun requestBalanceAndUnspentTransactions() {
if (UtilHelper.isOnline(context as Activity)) {
val coinEngine = CoinEngineFactory.create(ctx)
requestCounter++
coinEngine!!.requestBalanceAndUnspentTransactions(
object : CoinEngine.BlockchainRequestsCallbacks {
override fun onComplete(success: Boolean) {
LOG.i(TAG, "requestBalanceAndUnspentTransactions onComplete: $success, request counter $requestCounter")
if (activity == null) return
requestCounter--
if (!success) {
LOG.e(TAG, "requestBalanceAndUnspentTransactions ctx.error: " + ctx.error)
}
update()
}
override fun onProgress() {
if (activity == null) return
LOG.i(TAG, "requestBalanceAndUnspentTransactions onProgress")
// update()
}
override fun allowAdvance(): Boolean {
return try {
context?.let { UtilHelper.isOnline(it) }!!
} catch (e: KotlinNullPointerException) {
e.printStackTrace()
false
}
}
}
)
} else {
ctx.error = getString(R.string.general_error_no_connection)
update()
}
}
private fun refresh(clearData: Boolean = true) {
if (ctx.card == null) return
// clear all card data and request again
ctx.coinData.clearInfo()
if (clearData) {
ctx.error = null
ctx.message = null
}
LOG.w(TAG, "============= START REFRESH")
requestCounter = 0
srl?.isRefreshing = true
update()
ctx.coinData.setIsBalanceEqual(true)
requestBalanceAndUnspentTransactions()
if (requestCounter == 0) {
// if no connection and no requests posted
srl?.isRefreshing = false
update()
}
}
companion object {
val TAG: String = TagFragment::class.java.simpleName
}
}

View file

@ -1,23 +1,25 @@
package com.tangem.wallet
import android.util.Log
import com.tangem.wallet.btc.BtcEngine
import com.tangem.wallet.eth.EthEngine
import com.tangem.wallet.token.TokenEngine
import com.tangem.wallet.bch.BtcCashEngine
import com.tangem.data.Blockchain
import com.tangem.wallet.eos.EosEngine
import com.tangem.wallet.bch.BtcCashEngine
import com.tangem.wallet.binance.BinanceEngine
import com.tangem.wallet.btc.BtcEngine
import com.tangem.wallet.cardano.CardanoData
import com.tangem.wallet.cardano.CardanoEngine
import com.tangem.wallet.ducatus.DucatusEngine
import com.tangem.wallet.eos.EosEngine
import com.tangem.wallet.eth.EthEngine
import com.tangem.wallet.ltc.LtcEngine
import com.tangem.wallet.matic.MaticTokenEngine
import com.tangem.wallet.nftToken.NftTokenEngine
import com.tangem.wallet.rsk.RskEngine
import com.tangem.wallet.rsk.RskTokenEngine
import com.tangem.wallet.token.TokenEngine
import com.tangem.wallet.tezos.TezosEngine
import com.tangem.wallet.xlm.XlmAssetEngine
import com.tangem.wallet.xlm.XlmEngine
import com.tangem.wallet.xlmTag.XlmTagEngine
import com.tangem.wallet.xrp.XrpEngine
/**
@ -48,7 +50,10 @@ object CoinEngineFactory {
Blockchain.Matic, Blockchain.MaticTestNet -> MaticTokenEngine()
Blockchain.StellarTestNet, Blockchain.Stellar -> XlmEngine()
Blockchain.StellarAsset -> XlmAssetEngine()
Blockchain.StellarTag -> XlmTagEngine()
Blockchain.Eos -> EosEngine()
Blockchain.Ducatus -> DucatusEngine()
Blockchain.Tezos -> TezosEngine()
else -> null
}
}
@ -84,8 +89,14 @@ object CoinEngineFactory {
XlmEngine(context)
else if (Blockchain.StellarAsset == context.blockchain)
XlmAssetEngine(context)
else if (Blockchain.StellarTag == context.blockchain)
XlmTagEngine(context)
else if (Blockchain.Eos == context.blockchain)
EosEngine(context)
else if (Blockchain.Ducatus == context.blockchain)
DucatusEngine(context)
else if (Blockchain.Tezos == context.blockchain)
TezosEngine(context)
else
return null
} catch (e: Exception) {

View file

@ -55,14 +55,16 @@ public class TangemContext {
public String getBlockchainName() {
Blockchain blockchain = getBlockchain();
if (blockchain == Blockchain.Token || blockchain == Blockchain.RootstockToken) {
String token = card.getTokenSymbol();
return token + " <br><small><small> " + getBlockchain().getOfficialName() + " smart contract token</small></small>";
return card.getTokenSymbol()+ "<br><small><small> " + getBlockchain().getOfficialName() + " smart contract token</small></small>";
}
if (blockchain == Blockchain.NftToken) {
return card.getTokenSymbol().substring(4) + " <br><small><small> " + getBlockchain().getOfficialName() + " NFT token</small></small>";
return card.getTokenSymbol().substring(4) + "<br><small><small> " + getBlockchain().getOfficialName() + " non-fungible token</small></small>";
}
if (blockchain == Blockchain.StellarAsset) {
return card.getTokenSymbol() + " <br><small><small> " + getBlockchain().getOfficialName() + " asset</small></small>";
return card.getTokenSymbol() + "<br><small><small> " + getBlockchain().getOfficialName() + " asset</small></small>";
}
if (blockchain == Blockchain.StellarTag) {
return "TANGEM TAG<br><small><small> "+ getBlockchain().getOfficialName() + " non-fungible token </small></small>";
}
return blockchain.getOfficialName();
}

View file

@ -566,7 +566,7 @@ public final class Transaction {
public static Script buildOutput(String address) throws BitcoinException {
//noinspection TryWithIdenticalCatches
byte[] addressWithCheckSumAndNetworkCode = Base58.decodeBase58(address);
if (addressWithCheckSumAndNetworkCode[0] == 0 || addressWithCheckSumAndNetworkCode[0] == 111 || addressWithCheckSumAndNetworkCode[0] == 48) { //0 for BTC/BCH 1 address | 48 for LTC L address
if (addressWithCheckSumAndNetworkCode[0] == 0 || addressWithCheckSumAndNetworkCode[0] == 111 || addressWithCheckSumAndNetworkCode[0] == 48 || addressWithCheckSumAndNetworkCode[0] == 49) { //0 for BTC/BCH 1 address | 48 for LTC L address | 49 for Ducatus
return buildOutputP2H(address);
}
@ -601,7 +601,7 @@ public final class Transaction {
//noinspection TryWithIdenticalCatches
try {
byte[] addressWithCheckSumAndNetworkCode = Base58.decodeBase58(address);
if (addressWithCheckSumAndNetworkCode[0] != 0 && addressWithCheckSumAndNetworkCode[0] != 111 && addressWithCheckSumAndNetworkCode[0] != 48) {
if (addressWithCheckSumAndNetworkCode[0] != 0 && addressWithCheckSumAndNetworkCode[0] != 111 && addressWithCheckSumAndNetworkCode[0] != 48 && addressWithCheckSumAndNetworkCode[0] != 49) {
throw new BitcoinException(BitcoinException.ERR_UNSUPPORTED, "Unknown address type", address);
}

View file

@ -24,6 +24,7 @@ import com.tangem.wallet.TangemContext;
import com.tangem.wallet.Transaction;
import com.tangem.wallet.UnspentOutputInfo;
import com.tangem.wallet.btc.BtcData;
import com.tangem.wallet.btc.Unspents;
import org.json.JSONArray;
import org.json.JSONException;
@ -274,11 +275,11 @@ public class BtcCashEngine extends CoinEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();
@ -424,7 +425,15 @@ public class BtcCashEngine extends CoinEngine {
@Override
public String getUnspentInputsDescription() {
return coinData.getUnspentInputsDescription();
Unspents unspents = coinData.getUnspentInputsDescription();
if (unspents == null) {
return "";
} else {
return String.format(
ctx.getContext().getString(R.string.details_unspents_number),
unspents.getUnspetns(),
unspents.getGatheredUnspents());
}
}
@Override

View file

@ -53,7 +53,7 @@ public class BinanceData extends CoinData {
}
public CoinEngine.Amount getBalance() {
return new CoinEngine.Amount(balance, "BNB");
return balance == null ? null : new CoinEngine.Amount(balance, "BNB");
}
public void setBalance(String balance) {

View file

@ -247,11 +247,11 @@ public class BinanceEngine extends CoinEngine {
}
}
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalance().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
return true;
} catch (Exception e) {

View file

@ -22,18 +22,19 @@ public class BtcData extends CoinData {
//for blockchain.info
private boolean hasUnconfirmed = false;
public String getUnspentInputsDescription() {
public Unspents getUnspentInputsDescription() {
try {
int gatheredUnspents = 0;
if (unspentTransactions == null) return "";
if (unspentTransactions == null) return null;
for (int i = 0; i < unspentTransactions.size(); i++) {
if (unspentTransactions.get(i).script != null && unspentTransactions.get(i).script.length() > 1)
gatheredUnspents++;
}
return unspentTransactions.size() + " unspents (" + gatheredUnspents + " received)";
return new Unspents(unspentTransactions.size(), gatheredUnspents);
} catch (Exception e) {
e.printStackTrace();
return "";
return null;
}
}

View file

@ -310,11 +310,11 @@ public class BtcEngine extends CoinEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();
@ -437,7 +437,15 @@ public class BtcEngine extends CoinEngine {
@Override
public String getUnspentInputsDescription() {
return coinData.getUnspentInputsDescription();
Unspents unspents = coinData.getUnspentInputsDescription();
if (unspents == null) {
return "";
} else {
return String.format(
ctx.getContext().getString(R.string.details_unspents_number),
unspents.getUnspetns(),
unspents.getGatheredUnspents());
}
}
@Override

View file

@ -0,0 +1,3 @@
package com.tangem.wallet.btc
data class Unspents(val unspetns: Int, val gatheredUnspents: Int)

View file

@ -105,7 +105,7 @@ public class CardanoData extends CoinData {
}
public CoinEngine.InternalAmount getBalanceInInternalUnits() {
return new CoinEngine.InternalAmount(BigDecimal.valueOf(balance),"Lovelace");
return balance == null ? null : new CoinEngine.InternalAmount(BigDecimal.valueOf(balance),"Lovelace");
}
public void setBalance(Long balance) {

View file

@ -251,11 +251,11 @@ public class CardanoEngine extends CoinEngine {
}
}
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
return true;
} catch (Exception e) {
@ -503,7 +503,7 @@ public class CardanoEngine extends CoinEngine {
@Override
public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) {
return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw;
return signingMethod == TangemCard.SigningMethod.Sign_Hash;
}
@Override

View file

@ -7,6 +7,7 @@ import android.util.Log;
import com.tangem.App;
import com.tangem.data.network.ServerApiInsight;
import com.tangem.data.network.model.InsightResponse;
import com.tangem.data.network.model.InsightUtxo;
import com.tangem.tangem_card.data.TangemCard;
import com.tangem.tangem_card.reader.CardProtocol;
import com.tangem.tangem_card.tasks.SignTask;
@ -24,11 +25,11 @@ import com.tangem.wallet.Transaction;
import com.tangem.wallet.UnspentOutputInfo;
import com.tangem.wallet.btc.BtcData;
import com.tangem.wallet.btc.BtcEngine;
import com.tangem.wallet.btc.Unspents;
import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
@ -48,7 +49,7 @@ public class DucatusEngine extends BtcEngine {
} else if (context.getCoinData() instanceof BtcData) {
coinData = (BtcData) context.getCoinData();
} else {
throw new Exception("Invalid type of Blockchain data for LtcEngine");
throw new Exception("Invalid type of Blockchain data for DucatusEngine");
}
}
@ -83,7 +84,7 @@ public class DucatusEngine extends BtcEngine {
@Override
public String getBalanceCurrency() {
return "LTC";
return "DUC";
}
@Override
@ -118,7 +119,7 @@ public class DucatusEngine extends BtcEngine {
@Override
public String getFeeCurrency() {
return "LTC";
return "DUC";
}
@Override
@ -168,16 +169,12 @@ public class DucatusEngine extends BtcEngine {
@Override
public Uri getWalletExplorerUri() {
return Uri.parse("https://live.blockcypher.com/ltc/address/" + ctx.getCoinData().getWallet());
return Uri.parse("https://insight.ducatus.io/insight/address/" + ctx.getCoinData().getWallet());
}
@Override
public Uri getShareWalletUri() {
if (ctx.getCard().getDenomination() != null) {
return Uri.parse("litecoin:" + ctx.getCoinData().getWallet() + "?amount=" + convertToAmount(convertToInternalAmount(ctx.getCard().getDenomination())).toValueString(8));
} else {
return Uri.parse("litecoin:" + ctx.getCoinData().getWallet());
}
return Uri.parse(ctx.getCoinData().getWallet());
}
@Override
@ -266,11 +263,11 @@ public class DucatusEngine extends BtcEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();
@ -317,7 +314,7 @@ public class DucatusEngine extends BtcEngine {
@Override
public String calculateAddress(byte[] pkUncompressed) throws NoSuchProviderException, NoSuchAlgorithmException {
byte netSelectionByte = (byte) 0x30;
byte netSelectionByte = (byte) 0x31;
byte hash1[] = Util.calculateSHA256(pkUncompressed);
byte hash2[] = Util.calculateRIPEMD160(hash1);
@ -381,23 +378,35 @@ public class DucatusEngine extends BtcEngine {
@Override
public String getUnspentInputsDescription() {
return coinData.getUnspentInputsDescription();
Unspents unspents = coinData.getUnspentInputsDescription();
if (unspents == null) {
return "";
} else {
return String.format(
ctx.getContext().getString(R.string.details_unspents_number),
unspents.getUnspetns(),
unspents.getGatheredUnspents());
}
}
@Override
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception {
final ArrayList<UnspentOutputInfo> unspentOutputs;
ArrayList<UnspentOutputInfo> unspentOutputs = new ArrayList<>();
checkBlockchainDataExists();
String myAddress = ctx.getCoinData().getWallet();
byte[] pbKey = ctx.getCard().getWalletPublicKey();
// Build script for our address
List<BtcData.UnspentTransaction> rawTxList = coinData.getUnspentTransactions();
byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(myAddress).bytes;
// // Build script for our address
// List<BtcData.UnspentTransaction> rawTxList = coinData.getUnspentTransactions();
// byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(myAddress).bytes;
//
// // Collect unspent
// unspentOutputs = BTCUtils.getOutputs(rawTxList, outputScriptWeAreAbleToSpend);
// Collect unspent
unspentOutputs = BTCUtils.getOutputs(rawTxList, outputScriptWeAreAbleToSpend);
for (BtcData.UnspentTransaction utxo : coinData.getUnspentTransactions()) {
unspentOutputs.add(new UnspentOutputInfo(BTCUtils.fromHex(utxo.txID), new Transaction.Script(BTCUtils.fromHex(utxo.script)), utxo.amount, utxo.outputN, -1, utxo.txID, null));
}
long fullAmount = 0;
for (int i = 0; i < unspentOutputs.size(); ++i) {
@ -413,8 +422,8 @@ public class DucatusEngine extends BtcEngine {
change = change - fees;
}
final long amountFinal=amount;
final long changeFinal=change;
final long amountFinal = amount;
final long changeFinal = change;
if (amount + fees > fullAmount) {
throw new CardProtocol.TangemException_WrongAmount(String.format("Balance (%d) < change (%d) + amount (%d)", fullAmount, change, amount));
@ -422,7 +431,7 @@ public class DucatusEngine extends BtcEngine {
final byte[][] txForSign = new byte[unspentOutputs.size()][];
final byte[][] bodyDoubleHash = new byte[unspentOutputs.size()][];
final byte[][] bodyHash= new byte[unspentOutputs.size()][];
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);
@ -434,13 +443,14 @@ public class DucatusEngine extends BtcEngine {
@Override
public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) {
return signingMethod==TangemCard.SigningMethod.Sign_Hash || signingMethod==TangemCard.SigningMethod.Sign_Raw;
return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw;
}
@Override
public byte[][] getHashesToSign() throws Exception {
byte[][] dataForSign=new byte[unspentOutputs.size()][];
if (txForSign.length > 10) throw new Exception("To much hashes in one transaction!");
byte[][] dataForSign = new byte[unspentOutputs.size()][];
if (txForSign.length > 10)
throw new Exception("To much hashes in one transaction!");
for (int i = 0; i < unspentOutputs.size(); ++i) {
dataForSign[i] = bodyDoubleHash[i];
}
@ -479,7 +489,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(targetAddress, myAddress, unspentOutputs, amountFinal, changeFinal);
notifyOnNeedSendTransaction(txForSend);
return txForSend;
}
@ -493,39 +503,20 @@ public class DucatusEngine extends BtcEngine {
ServerApiInsight.ResponseListener responseListener = new ServerApiInsight.ResponseListener() {
@Override
public void onSuccess(String method, InsightResponse insightResponse) {
switch (method) {
case ServerApiInsight.INSIGHT_ADDRESS: {
try {
String walletAddress = insightResponse.getAddrStr();
if (!walletAddress.equals(coinData.getWallet())) {
// todo - check
throw new Exception("Invalid wallet address in answer!");
}
coinData.setBalanceReceived(true);
coinData.setBalanceConfirmed(insightResponse.getBalanceSat());
coinData.setBalanceUnconfirmed(insightResponse.getUnconfirmedBalanceSat());
coinData.setValidationNodeDescription(ServerApiInsight.lastNode);
}
catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "FAIL INSIGHT_ADDRESS Exception");
}
}
break;
case ServerApiInsight.INSIGHT_TRANSACTION: {
try {
String raw = insightResponse.getRawtx();
String txHash = new String(BTCUtils.reverse(CryptoUtil.doubleSha256(BTCUtils.fromHex(raw)))); //TODO: check
for (BtcData.UnspentTransaction tx : coinData.getUnspentTransactions()) {
if (tx.txID.equals(txHash))
tx.script = raw;
}
} catch (Exception e) {
e.printStackTrace();
}
try {
String walletAddress = insightResponse.getAddrStr();
if (!walletAddress.equals(coinData.getWallet())) {
// todo - check
throw new Exception("Invalid wallet address in answer!");
}
break;
coinData.setBalanceReceived(true);
coinData.setBalanceConfirmed(insightResponse.getBalanceSat());
coinData.setBalanceUnconfirmed(insightResponse.getUnconfirmedBalanceSat());
coinData.setValidationNodeDescription(ServerApiInsight.lastNode);
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "FAIL INSIGHT_ADDRESS Exception");
}
if (serverApiInsight.isRequestsSequenceCompleted()) {
@ -535,30 +526,27 @@ public class DucatusEngine extends BtcEngine {
}
}
public void onSuccess(String method, List<InsightResponse> utxoList) {
// case ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS: TODO: check method
try {
coinData.getUnspentTransactions().clear();
for (InsightResponse utxo : utxoList) {
BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction();
trUnspent.txID = utxo.getTxid();
trUnspent.amount = utxo.getSatoshis();
trUnspent.outputN = utxo.getHeight();
coinData.getUnspentTransactions().add(trUnspent);
}
for (InsightResponse utxo : utxoList) {
//if (height != -1) { TODO: check
if (blockchainRequestsCallbacks.allowAdvance()) {
serverApiInsight.requestData(ServerApiInsight.INSIGHT_TRANSACTION, "", utxo.getTxid());
} else {
ctx.setError("Terminated by user");
}
}
} catch (Exception e) {
e.printStackTrace();
public void onSuccess(String method, List<InsightUtxo> utxoList) {
// case ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS: TODO: check method
try {
coinData.getUnspentTransactions().clear();
for (InsightUtxo utxo : utxoList) {
BtcData.UnspentTransaction trUnspent = new BtcData.UnspentTransaction();
trUnspent.txID = utxo.getTxid();
trUnspent.amount = utxo.getSatoshis();
trUnspent.outputN = utxo.getVout();
trUnspent.script = utxo.getScriptPubKey();
coinData.getUnspentTransactions().add(trUnspent);
}
} catch (Exception e) {
e.printStackTrace();
}
if (serverApiInsight.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
@Override
@ -582,65 +570,71 @@ public class DucatusEngine extends BtcEngine {
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) throws Exception {
final int calcSize = calculateEstimatedTransactionSize(targetAddress, amount.toValueString());
Log.e(TAG, String.format("Estimated tx size %d", calcSize));
coinData.minFee=null;
coinData.maxFee=null;
coinData.normalFee=null;
final ServerApiInsight serverApiInsight = new ServerApiInsight();
final ServerApiInsight.ResponseListener responseListener = new ServerApiInsight.ResponseListener() {
@Override
public void onSuccess(String method, InsightResponse insightResponse) {
if ( method.equals(ServerApiInsight.INSIGHT_FEE)) {
try {
BigDecimal minFee = new BigDecimal(insightResponse.getFee2()); //fee per KB
BigDecimal normalFee = new BigDecimal(insightResponse.getFee3());
BigDecimal maxFee = new BigDecimal(insightResponse.getFee6());
if (minFee.equals(BigDecimal.ZERO) || normalFee.equals(BigDecimal.ZERO) || maxFee.equals(BigDecimal.ZERO)) {
serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "","");
}
minFee = minFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); // (per KB -> per byte)*size
normalFee = normalFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024));
maxFee = maxFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024));
// //compare fee to usual relay fee TODO: check if needed after we get access to Ducatus network
// if (fee.compareTo(relayFee) < 0) {
// fee = relayFee;
// coinData.minFee=null;
// coinData.maxFee=null;
// coinData.normalFee=null;
//
// final ServerApiInsight serverApiInsight = new ServerApiInsight();
//
// final ServerApiInsight.ResponseListener responseListener = new ServerApiInsight.ResponseListener() {
// @Override
// public void onSuccess(String method, InsightResponse insightResponse) {
// if ( method.equals(ServerApiInsight.INSIGHT_FEE)) {
// try {
// BigDecimal minFee = new BigDecimal(insightResponse.getFee2()); //fee per KB
// BigDecimal normalFee = new BigDecimal(insightResponse.getFee3());
// BigDecimal maxFee = new BigDecimal(insightResponse.getFee6());
//
// if (minFee.equals(BigDecimal.ZERO) || normalFee.equals(BigDecimal.ZERO) || maxFee.equals(BigDecimal.ZERO)) {
// serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "","");
// }
minFee = minFee.setScale(8, RoundingMode.DOWN);
normalFee = normalFee.setScale(8, RoundingMode.DOWN);
maxFee = maxFee.setScale(8, RoundingMode.DOWN);
//
// minFee = minFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); // (per KB -> per byte)*size
// normalFee = normalFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024));
// maxFee = maxFee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024));
//
//// //compare fee to usual relay fee TODO: check if needed after we get access to Ducatus network
//// if (fee.compareTo(relayFee) < 0) {
//// fee = relayFee;
//// }
// minFee = minFee.setScale(8, RoundingMode.DOWN);
// normalFee = normalFee.setScale(8, RoundingMode.DOWN);
// maxFee = maxFee.setScale(8, RoundingMode.DOWN);
//
// coinData.minFee = new Amount(minFee, ctx.getBlockchain().getCurrency());
// coinData.normalFee = new Amount(normalFee, ctx.getBlockchain().getCurrency());
// coinData.maxFee = new Amount(maxFee, ctx.getBlockchain().getCurrency());
//
// blockchainRequestsCallbacks.onComplete(true);
//
// } catch (Exception e) {
// e.printStackTrace();
// }
// }
// }
//
// @Override
// public void onSuccess (String method, List<InsightResponse> utxoList) {
// Log.e(TAG, "Wrong response body, InsightResponse expected");
// }
//
// @Override
// public void onFail(String method, String message) {
// if (!serverApiInsight.isRequestsSequenceCompleted()) {
// ctx.setError(message);
// blockchainRequestsCallbacks.onComplete(false);
// }
// }
// };
// serverApiInsight.setResponseListener(responseListener);
//
// serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "", ""); TODO: fee api returns -1 now
coinData.minFee = new Amount(minFee, ctx.getBlockchain().getCurrency());
coinData.normalFee = new Amount(normalFee, ctx.getBlockchain().getCurrency());
coinData.maxFee = new Amount(maxFee, ctx.getBlockchain().getCurrency());
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);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
public void onSuccess (String method, List<InsightResponse> utxoList) {
Log.e(TAG, "Wrong response body, InsightResponse expected");
}
@Override
public void onFail(String method, String message) {
if (!serverApiInsight.isRequestsSequenceCompleted()) {
ctx.setError(message);
blockchainRequestsCallbacks.onComplete(false);
}
}
};
serverApiInsight.setResponseListener(responseListener);
serverApiInsight.requestData(ServerApiInsight.INSIGHT_FEE, "", "");
blockchainRequestsCallbacks.onComplete(true);
}
@Override
@ -657,7 +651,7 @@ public class DucatusEngine extends BtcEngine {
if (resultString.isEmpty()) {
ctx.setError("No response from node");
blockchainRequestsCallbacks.onComplete(false);
}else { // TODO: Make check for a valid send response
} else { // TODO: Make check for a valid send response
ctx.setError(null);
blockchainRequestsCallbacks.onComplete(true);
}
@ -676,7 +670,7 @@ public class DucatusEngine extends BtcEngine {
}
@Override
public void onSuccess (String method, List<InsightResponse> utxoList) {
public void onSuccess(String method, List<InsightUtxo> utxoList) {
Log.e(TAG, "Wrong response body, InsightResponse expected");
}

View file

@ -293,11 +293,11 @@ public class EosEngine extends CoinEngine {
}
}
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && getBalance().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_verify_online);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_verify_online);
// }
return true;

View file

@ -334,11 +334,11 @@ public class EthEngine extends CoinEngine {
}
}
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && getBalance().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_verify_online);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_verify_online);
// }
return true;

View file

@ -31,6 +31,7 @@ import com.tangem.wallet.Transaction;
import com.tangem.wallet.UnspentOutputInfo;
import com.tangem.wallet.btc.BtcData;
import com.tangem.wallet.btc.BtcEngine;
import com.tangem.wallet.btc.Unspents;
import org.json.JSONException;
@ -274,11 +275,11 @@ public class LtcEngine extends BtcEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_verify_online);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_verify_online);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();
@ -389,7 +390,15 @@ public class LtcEngine extends BtcEngine {
@Override
public String getUnspentInputsDescription() {
return coinData.getUnspentInputsDescription();
Unspents unspents = coinData.getUnspentInputsDescription();
if (unspents == null) {
return "";
} else {
return String.format(
ctx.getContext().getString(R.string.details_unspents_number),
unspents.getUnspetns(),
unspents.getGatheredUnspents());
}
}
@Override

View file

@ -0,0 +1,49 @@
package com.tangem.wallet.tezos
import android.os.Bundle
import android.util.Log
import com.tangem.wallet.CoinData
import java.lang.Exception
class TezosData : CoinData() {
var balance: Long? = null
var counter: Long? = null
var publicKeyReavealed: Boolean? = null
var tezosPublicKey: String? = null
override fun clearInfo() {
super.clearInfo()
balance = null
counter = null
publicKeyReavealed = null
tezosPublicKey = null
}
override fun loadFromBundle(B: Bundle) {
super.loadFromBundle(B)
if (B.containsKey("Balance")) balance = B.getLong("Balance")
if (B.containsKey("Counter")) counter = B.getLong("Counter")
if (B.containsKey("PublicKeyReavealed")) publicKeyReavealed = B.getBoolean("PublicKeyReavealed")
if (B.containsKey("TezosPublicKey")) tezosPublicKey = B.getString("TezosPublicKey")
}
override fun saveToBundle(B: Bundle) {
super.saveToBundle(B)
try {
if (balance != null) B.putLong("Balance", balance!!)
if (counter != null) B.putLong("Counter", counter!!)
if (publicKeyReavealed != null) B.putBoolean("PublicKeyReavealed", publicKeyReavealed!!)
if (tezosPublicKey != null) B.putString("TezosPublicKey", tezosPublicKey!!)
} catch (e: Exception) {
Log.e("Can't save to bundle ", e.message)
}
}
}

View file

@ -0,0 +1,506 @@
package com.tangem.wallet.tezos
import android.net.Uri
import android.os.StrictMode
import android.text.InputFilter
import android.util.Log
import com.tangem.App
import com.tangem.data.network.ServerApiTezos
import com.tangem.data.network.model.TezosAccountResponse
import com.tangem.data.network.model.TezosForgeBody
import com.tangem.data.network.model.TezosOperationContent
import com.tangem.data.network.model.TezosPreapplyBody
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_card.tasks.SignTask
import com.tangem.tangem_card.util.Util
import com.tangem.util.CryptoUtil
import com.tangem.util.DecimalDigitsInputFilter
import com.tangem.wallet.*
import io.reactivex.observers.DisposableSingleObserver
import org.spongycastle.jcajce.provider.digest.Blake2b
import java.math.BigDecimal
class TezosEngine : CoinEngine {
constructor()
constructor(context: TangemContext) : super(context) {
if (context.coinData == null) {
coinData = TezosData()
context.coinData = coinData
} else if (context.coinData is TezosData) {
coinData = context.coinData as TezosData
} else {
throw Exception("Invalid type of Blockchain data for XlmEngine")
}
}
private val TAG = TezosEngine::class.java.simpleName
var coinData: TezosData? = null
private fun getDecimals() = 6
@Throws(Exception::class)
private fun checkBlockchainDataExists() {
if (coinData == null) throw Exception("No blockchain data")
}
override fun awaitingConfirmation(): Boolean {
return App.pendingTransactionsStorage.hasTransactions(ctx.card)
}
override fun getBalanceHTML(): String? {
val balance = balance
return if (balance != null) {
balance.toDescriptionString(getDecimals())
} else {
""
}
}
override fun getBalanceCurrency(): String? {
return "XTZ"
}
override fun isBalanceNotZero(): Boolean {
if (coinData == null) return false
return if (balance == null) false else balance!!.notZero()
}
override fun hasBalanceInfo(): Boolean {
return if (coinData == null) false else coinData!!.balance != null
}
override fun isExtractPossible(): Boolean {
if (!hasBalanceInfo()) {
ctx.setMessage(R.string.loaded_wallet_error_obtaining_blockchain_data)
} else if (!isBalanceNotZero) {
ctx.setMessage(R.string.general_wallet_empty)
} else if (awaitingConfirmation()) {
ctx.setMessage(R.string.loaded_wallet_message_wait)
} else {
return true
}
return false
}
override fun getFeeCurrency(): String? {
return "XTZ"
}
override fun validateAddress(address: String?): Boolean {
val prefixedHashWithChecksum = Base58.decodeBase58(address)
if (prefixedHashWithChecksum == null || prefixedHashWithChecksum.size != 27) return false
val prefixedHash = prefixedHashWithChecksum.copyOf(23)
val checksum = prefixedHashWithChecksum.copyOfRange(23, 27)
val calcChecksum = CryptoUtil.doubleSha256(prefixedHash).copyOfRange(0, 4)
return calcChecksum.contentEquals(checksum)
}
override fun isNeedCheckNode(): Boolean {
return false
}
override fun getWalletExplorerUri(): Uri? {
return Uri.parse("https://tezblock.io/account/" + ctx.coinData.wallet)
}
override fun getShareWalletUri(): Uri? {
return Uri.parse(ctx.coinData.wallet)
}
override fun getAmountInputFilters(): Array<InputFilter>? {
return arrayOf(DecimalDigitsInputFilter(getDecimals()))
}
override fun checkNewTransactionAmount(amount: Amount): Boolean {
if (coinData == null) return false
return amount <= balance
}
override fun checkNewTransactionAmountAndFee(amountValue: Amount?,
feeValue: Amount?,
isIncludeFee: Boolean
): Boolean {
try {
checkBlockchainDataExists()
} catch (e: java.lang.Exception) {
e.printStackTrace()
return false
}
if (feeValue == null || amountValue == null) return false
if (feeValue.isZero || amountValue.isZero) return false
if (isIncludeFee && (amountValue > balance || amountValue < feeValue)) return false
if (!isIncludeFee && amountValue.add(feeValue) > balance) return false
return true
}
override fun validateBalance(balanceValidator: BalanceValidator): Boolean {
try {
if (ctx.card.offlineBalance == null &&
!ctx.coinData.isBalanceReceived ||
!ctx.coinData.isBalanceReceived &&
ctx.card.remainingSignatures != ctx.card.maxSignatures
) {
balanceValidator.setScore(0)
balanceValidator.firstLine = R.string.balance_validator_first_line_unknown_balance
balanceValidator.setSecondLine(R.string.balance_validator_second_line_unverified_balance)
return false
}
if (coinData!!.isBalanceReceived) {
balanceValidator.setScore(100)
balanceValidator.firstLine = R.string.balance_validator_first_line_verified_balance
balanceValidator.setSecondLine(R.string.balance_validator_second_line_confirmed_in_blockchain)
if (balance!!.isZero) {
balanceValidator.firstLine = R.string.balance_validator_first_line_empty_wallet
balanceValidator.setSecondLine(R.string.empty_string)
}
}
// if (ctx.card.offlineBalance != null &&
// !coinData!!.isBalanceReceived &&
// ctx.card.remainingSignatures ==
// ctx.card.maxSignatures
// ) {
// balanceValidator.setScore(80)
// balanceValidator.firstLine = R.string.balance_validator_first_line_verified_offline
// balanceValidator.setSecondLine(
// R.string.balance_validator_second_line_internet_to_get_balance
// )
// }
return true
} catch (e: java.lang.Exception) {
e.printStackTrace()
return false
}
}
override fun getBalance(): Amount? {
var balanceAmount: Amount? = null
if (hasBalanceInfo()) {
val xtzBalance = BigDecimal
.valueOf(coinData!!.balance!!).movePointLeft(getDecimals())
balanceAmount = Amount(xtzBalance, balanceCurrency)
}
return balanceAmount
}
override fun evaluateFeeEquivalent(fee: String?): String? {
return if (!coinData!!.amountEquivalentDescriptionAvailable) "" else try {
val feeAmount = Amount(fee, feeCurrency)
feeAmount.toEquivalentString(coinData!!.rate.toDouble())
} catch (e: java.lang.Exception) {
""
}
}
override fun getBalanceEquivalent(): String? {
if (coinData == null || !coinData!!.amountEquivalentDescriptionAvailable) return ""
val balance = balance ?: return ""
return balance.toEquivalentString(coinData!!.rate.toDouble())
}
override fun calculateAddress(pkUncompressed: ByteArray): String? {
val publicKeyHash = Blake2b.Blake2b160().digest(pkUncompressed)
val tz1Prefix = Util.hexToBytes("06A19F")
val prefixedHash = tz1Prefix + publicKeyHash
val checksum = CryptoUtil.doubleSha256(prefixedHash).copyOfRange(0, 4)
val prefixedHashWithChecksum = prefixedHash + checksum
return Base58.encodeBase58(prefixedHashWithChecksum)
}
fun calculateTezosPublicKey(pkUncompressed: ByteArray): String {
val edpkPrefix = Util.hexToBytes("0D0F25D9")
val prefixedPubKey = edpkPrefix + pkUncompressed
val checksum = CryptoUtil.doubleSha256(prefixedPubKey).copyOfRange(0, 4)
val prefixedHashWithChecksum = prefixedPubKey + checksum
return Base58.encodeBase58(prefixedHashWithChecksum)
}
override fun convertToAmount(internalAmount: InternalAmount): Amount {
return Amount(internalAmount.movePointLeft(getDecimals()), balanceCurrency)
}
override fun convertToAmount(strAmount: String, currency: String): Amount {
return Amount(strAmount, currency)
}
override fun convertToInternalAmount(amount: Amount): InternalAmount {
return InternalAmount(amount.movePointRight(getDecimals()), "mutez")
}
override fun convertToInternalAmount(bytes: ByteArray?): InternalAmount? {
if (bytes == null) return null
val reversed = ByteArray(bytes.size)
for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1]
return InternalAmount(Util.byteArrayToLong(reversed), "mutez")
}
override fun convertToByteArray(internalAmount: InternalAmount): ByteArray? {
val bytes = Util.longToByteArray(internalAmount.longValueExact())
val reversed = ByteArray(bytes.size)
for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1]
return reversed
}
override fun createCoinData(): CoinData {
return TezosData()
}
override fun getUnspentInputsDescription() = ""
override fun constructTransaction(
amountValue: Amount,
feeValue: Amount,
IncFee: Boolean,
targetAddress: String
): SignTask.TransactionToSign {
checkBlockchainDataExists()
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
StrictMode.setThreadPolicy(policy)
val finalAmount = if (IncFee) {
amountValue.minus(feeValue)
} else {
amountValue
}
val serverApiTezos = ServerApiTezos()
val headerResponse = serverApiTezos.header
val contents = arrayListOf<TezosOperationContent>()
var counter = coinData!!.counter!!
if (!coinData!!.publicKeyReavealed!!) {
counter++
val revealOp = TezosOperationContent(
kind = "reveal",
source = coinData!!.wallet,
fee = "1300",
counter = counter.toString(),
gas_limit = "10000",
storage_limit = "0",
public_key = coinData!!.tezosPublicKey!!
)
contents.add(revealOp)
}
counter++
val transactionOp = TezosOperationContent(
kind = "transaction",
source = coinData!!.wallet,
fee = "1350",
counter = counter.toString(),
gas_limit = "10600",
storage_limit = "277",
destination = targetAddress,
amount = finalAmount.movePointRight(getDecimals()).toBigInteger().toString()
)
contents.add(transactionOp)
val tezosForgeBody = TezosForgeBody(headerResponse.hash!!, contents)
val forgeResponse = serverApiTezos.forgeOperations(tezosForgeBody)
val watermark = "03"
val forgedBytes = Util.hexToBytes(watermark + forgeResponse)
return object : SignTask.TransactionToSign {
override fun isSigningMethodSupported(signingMethod: TangemCard.SigningMethod): Boolean {
return signingMethod == TangemCard.SigningMethod.Sign_Hash
}
@Throws(java.lang.Exception::class)
override fun getHashesToSign(): Array<ByteArray?> {
val dataForSign = arrayOfNulls<ByteArray>(1)
dataForSign[0] = Blake2b.Blake2b256().digest(forgedBytes)
return dataForSign
}
@Throws(java.lang.Exception::class)
override fun getRawDataToSign(): ByteArray? {
throw java.lang.Exception("Signing of raw transaction not supported for " + this.javaClass.simpleName)
}
@Throws(java.lang.Exception::class)
override fun getHashAlgToSign(): String? {
throw java.lang.Exception("Signing of raw transaction not supported for " + this.javaClass.simpleName)
}
@Throws(java.lang.Exception::class)
override fun getIssuerTransactionSignature(dataToSignByIssuer: ByteArray?): ByteArray? {
throw java.lang.Exception("Transaction validation by issuer not supported in this version")
}
@Throws(java.lang.Exception::class)
override fun onSignCompleted(signFromCard: ByteArray): ByteArray? {
val edsigPrefix = Util.hexToBytes("09F5CD8612")
val prefixedSignature = edsigPrefix + signFromCard
val checksum = CryptoUtil.doubleSha256(prefixedSignature).copyOfRange(0, 4)
val prefixedSignatureWithChecksum = prefixedSignature + checksum
val preapplyBody = TezosPreapplyBody(
protocol = headerResponse.protocol!!,
branch = headerResponse.hash!!,
contents = contents,
signature = Base58.encodeBase58(prefixedSignatureWithChecksum)
)
try {
serverApiTezos.peapplyOperations(preapplyBody)
} catch (e: java.lang.Exception) {
ctx.error = e.message
return null
}
val txForSend = Util.hexToBytes(forgeResponse) + signFromCard
notifyOnNeedSendTransaction(txForSend)
return txForSend
}
}
}
override fun requestBalanceAndUnspentTransactions(
blockchainRequestsCallbacks: BlockchainRequestsCallbacks
) {
coinData!!.tezosPublicKey = calculateTezosPublicKey(ctx.card.walletPublicKey)
val serverApiTezos = ServerApiTezos()
val accountObserver = object : DisposableSingleObserver<TezosAccountResponse>() {
override fun onSuccess(response: TezosAccountResponse) {
coinData!!.balance = response.balance
coinData!!.isBalanceReceived = true
coinData!!.counter = response.counter
if (serverApiTezos.isRequestsSequenceCompleted) {
blockchainRequestsCallbacks.onComplete(ctx.hasError())
} else {
blockchainRequestsCallbacks.onProgress()
}
}
override fun onError(e: Throwable) {
Log.e(TAG, "requestBalanceAndUnspentTransactions error " + e.message)
ctx.error = e.message
e.printStackTrace()
if (serverApiTezos.isRequestsSequenceCompleted) {
blockchainRequestsCallbacks.onComplete(false)
} else {
blockchainRequestsCallbacks.onProgress()
}
}
}
val managerKeyObserver = object : DisposableSingleObserver<String>() {
override fun onSuccess(response: String) {
coinData!!.publicKeyReavealed = true
if (serverApiTezos.isRequestsSequenceCompleted) {
blockchainRequestsCallbacks.onComplete(ctx.hasError())
} else {
blockchainRequestsCallbacks.onProgress()
}
}
override fun onError(e: Throwable) {
Log.e(TAG, "requestFee error " + e.message)
e.printStackTrace()
coinData!!.publicKeyReavealed = false // error expected when key is not revealed
if (serverApiTezos.isRequestsSequenceCompleted) {
blockchainRequestsCallbacks.onComplete(ctx.hasError())
} else {
blockchainRequestsCallbacks.onProgress()
}
}
}
serverApiTezos.getAddress(coinData!!.wallet, accountObserver)
serverApiTezos.getMangerKey(coinData!!.wallet, managerKeyObserver)
}
override fun requestFee(
blockchainRequestsCallbacks: BlockchainRequestsCallbacks,
targetAddress: String?,
amount: Amount?
) {
var fee: BigDecimal = BigDecimal.valueOf(0.00135)
if (!coinData!!.publicKeyReavealed!!) {
fee += BigDecimal.valueOf(0.0013)
}
val serverApiTezos = ServerApiTezos()
val accountObserver = object : DisposableSingleObserver<TezosAccountResponse>() {
override fun onSuccess(response: TezosAccountResponse) {
if (response.balance == 0L) {
fee += BigDecimal.valueOf(0.257)
}
val feeAmount = Amount(fee, feeCurrency)
coinData!!.minFee = feeAmount
coinData!!.normalFee = feeAmount
coinData!!.maxFee = feeAmount
blockchainRequestsCallbacks.onComplete(true)
}
override fun onError(e: Throwable) {
Log.e(TAG, "requestFee error " + e.message)
ctx.error = e.message
e.printStackTrace()
blockchainRequestsCallbacks.onComplete(false)
}
}
serverApiTezos.getAddress(targetAddress, accountObserver)
}
override fun requestSendTransaction(
blockchainRequestsCallbacks: BlockchainRequestsCallbacks,
txForSend: ByteArray?
) {
if (txForSend == null) {
blockchainRequestsCallbacks.onComplete(false)
} else {
val injectObserver = object : DisposableSingleObserver<Any>() {
override fun onSuccess(response: Any) {
blockchainRequestsCallbacks.onComplete(true)
}
override fun onError(e: Throwable) {
blockchainRequestsCallbacks.onComplete(false)
}
}
ServerApiTezos().injectOperations(Util.bytesToHex(txForSend), injectObserver)
}
}
override fun allowSelectFeeLevel(): Boolean {
return false
}
override fun pendingTransactionTimeoutInSeconds(): Int {
return 60
}
}

View file

@ -275,11 +275,11 @@ public class XlmAssetEngine extends CoinEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getXlmBalance().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();

View file

@ -243,11 +243,11 @@ public class XlmEngine extends CoinEngine {
// return;
// }
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalance().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
// if(card.getFailedBalanceRequestCounter()!=0) {
// score -= 5 * card.getFailedBalanceRequestCounter();

View file

@ -0,0 +1,133 @@
package com.tangem.wallet.xlmTag
import android.os.Bundle
import android.util.Log
import com.tangem.wallet.CoinData
import com.tangem.wallet.CoinEngine
import com.tangem.wallet.CoinEngine.InternalAmount
import org.stellar.sdk.KeyPair
import org.stellar.sdk.responses.AccountResponse
import org.stellar.sdk.responses.LedgerResponse
import java.math.BigDecimal
class XlmTagData : CoinData() {
class AccountResponseEx internal constructor(accountId: String?, sequenceNumber: Long?) : AccountResponse(KeyPair.fromAccountId(accountId), sequenceNumber)
private var balance: CoinEngine.Amount? = null
private var sequenceNumber: Long? = 0L
private var baseReserve: CoinEngine.Amount? = CoinEngine.Amount("0.5", "XLM")
var baseFee: CoinEngine.Amount? = CoinEngine.Amount("0.00001", "XLM")
private set
var isError404 = false
var isTargetAccountCreated = false
var fundsFromTrustedSource = false
var fundsSentToTrustedSource = false
override fun clearInfo() {
super.clearInfo()
balance = null
isError404 = false
isTargetAccountCreated = false
fundsFromTrustedSource = false
fundsSentToTrustedSource = false
}
fun getBalance(): CoinEngine.Amount? {
return if (balance != null) {
CoinEngine.Amount(balance!!.subtract(reserve), "XLM")
} else {
null
}
}
val reserve: CoinEngine.Amount
get() = CoinEngine.Amount(baseReserve!!.multiply(BigDecimal.valueOf(2)), "XLM")
var accountResponse: AccountResponse
get() = XlmTagData.AccountResponseEx(wallet, sequenceNumber)
set(accountResponse) {
if (accountResponse.balances.size > 0) {
val balanceResponse = accountResponse.balances[0]
balance = CoinEngine.Amount(balanceResponse.balance, "XLM")
}
sequenceNumber = accountResponse.sequenceNumber
isBalanceReceived = true
}
fun setLedgerResponse(ledgerResponse: LedgerResponse) {
val xlmEngine = XlmTagEngine()
baseReserve = xlmEngine.convertToAmount(InternalAmount(ledgerResponse.baseReserveInStroops, "stroops"))
baseFee = xlmEngine.convertToAmount(InternalAmount(ledgerResponse.baseFeeInStroops, "stroops"))
}
fun incSequenceNumber() {
sequenceNumber = sequenceNumber?.inc()
}
override fun loadFromBundle(B: Bundle) {
super.loadFromBundle(B)
balance = if (B.containsKey("BalanceCurrency") && B.containsKey("BalanceDecimal")) {
CoinEngine.Amount(B.getString("BalanceDecimal"), B.getString("BalanceCurrency"))
} else {
null
}
sequenceNumber = if (B.containsKey("sequenceNumber")) {
B.getLong("sequenceNumber")
} else {
0L
}
baseReserve = if (B.containsKey("BaseReserveCurrency") && B.containsKey("BaseReserveDecimal")) {
CoinEngine.Amount(B.getString("BaseReserveDecimal"), B.getString("BaseReserveCurrency"))
} else {
CoinEngine.Amount("0.5", "XLM")
}
baseFee = if (B.containsKey("BaseFeeCurrency") && B.containsKey("BaseFeeDecimal")) {
CoinEngine.Amount(B.getString("BaseFeeDecimal"), B.getString("BaseFeeCurrency"))
} else {
CoinEngine.Amount("0.00001", "XLM")
}
if (B.containsKey("Error404")) isError404 = B.getBoolean("Error404") else isError404 = false
if (B.containsKey("TargetAccountCreated")) isTargetAccountCreated = B.getBoolean("TargetAccountCreated") else isTargetAccountCreated = false
fundsFromTrustedSource = if (B.containsKey("FundsFromTrustedSource")) {
B.getBoolean("FundsFromTrustedSource")
} else {
false
}
fundsSentToTrustedSource = if (B.containsKey("FundsSentToTrustedSource")) {
B.getBoolean("FundsSentToTrustedSource")
} else {
false
}
}
override fun saveToBundle(B: Bundle) {
super.saveToBundle(B)
try {
if (balance != null) {
B.putString("BalanceCurrency", balance!!.currency)
B.putString("BalanceDecimal", balance!!.toValueString())
}
if (sequenceNumber != null) {
B.putLong("sequenceNumber", sequenceNumber!!)
}
if (baseReserve != null) {
B.putString("BaseReserveCurrency", baseReserve!!.currency)
B.putString("BaseReserveDecimal", baseReserve!!.toValueString())
}
if (baseFee != null) {
B.putString("BaseFeeCurrency", baseFee!!.currency)
B.putString("BaseFeeDecimal", baseFee!!.toValueString())
}
if (isError404) B.putBoolean("Error404", true)
if (isTargetAccountCreated) B.putBoolean("TargetAccountCreated", true)
if (fundsFromTrustedSource) B.putBoolean("FundsFromTrustedSource", true)
if (fundsSentToTrustedSource) B.putBoolean("FundsSentToTrustedSource", true)
} catch (e: Exception) {
Log.e("Can't save to bundle ", e.message)
}
}
}

View file

@ -0,0 +1,423 @@
package com.tangem.wallet.xlmTag
import android.net.Uri
import android.os.StrictMode
import android.text.InputFilter
import android.util.Log
import com.tangem.App
import com.tangem.data.Blockchain
import com.tangem.data.network.ServerApiStellar
import com.tangem.data.network.StellarRequest
import com.tangem.data.network.StellarRequest.Ledgers
import com.tangem.data.network.StellarRequest.SubmitTransaction
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_card.tasks.SignTask
import com.tangem.tangem_card.util.Util
import com.tangem.util.DecimalDigitsInputFilter
import com.tangem.wallet.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.stellar.sdk.*
import org.stellar.sdk.requests.RequestBuilder
import org.stellar.sdk.responses.operations.OperationResponse
import org.stellar.sdk.responses.operations.PaymentOperationResponse
import java.io.IOException
import java.math.BigDecimal
class XlmTagEngine : CoinEngine {
var coinData: XlmTagData? = null
val operations = mutableListOf<OperationResponse>()
constructor(context: TangemContext) : super(context) {
if (context.coinData == null) {
coinData = XlmTagData()
context.coinData = coinData
} else if (context.coinData is XlmTagData) {
coinData = context.coinData as XlmTagData
} else {
throw Exception("Invalid type of Blockchain data for XlmEngine")
}
}
constructor() : super() {}
@Throws(Exception::class)
private fun checkBlockchainDataExists() {
if (coinData == null) throw Exception("No blockchain data")
}
override fun awaitingConfirmation(): Boolean {
return App.pendingTransactionsStorage.hasTransactions(ctx.card)
}
override fun getBalanceHTML(): String {
return if (hasBalanceInfo()) {
if (coinData!!.fundsFromTrustedSource) {
// if (coinData!!.fundsSentToTrustedSource) { //TODO: shut down for example tags to appear genuine, turn on for production
// ctx.getString(R.string.tag_claimed)
// } else {
ctx.getString(R.string.tag_genuine)
// }
} else {
ctx.getString(R.string.tag_not_genuine)
}
} else {
""
}
}
override fun getBalanceCurrency(): String {
return "XLM"
}
override fun isBalanceNotZero(): Boolean {
if (coinData == null) return false
return if (coinData!!.getBalance() == null) false else coinData!!.getBalance()!!.notZero()
}
override fun hasBalanceInfo(): Boolean {
return if (coinData == null) false else coinData!!.isBalanceReceived
}
override fun isExtractPossible(): Boolean {
if (!hasBalanceInfo()) {
ctx.setMessage(R.string.loaded_wallet_error_obtaining_blockchain_data)
} else if (!isBalanceNotZero) {
ctx.setMessage(R.string.general_wallet_empty)
} else if (awaitingConfirmation()) {
ctx.setMessage(R.string.loaded_wallet_message_wait)
} else {
return true
}
return false
}
override fun getFeeCurrency(): String {
return "XLM"
}
override fun validateAddress(address: String): Boolean {
try {
val kp = KeyPair.fromAccountId(address)
} catch (e: Exception) {
return false
}
return true
}
override fun isNeedCheckNode(): Boolean {
return false
}
override fun getWalletExplorerUri(): Uri {
return Uri.parse("https://stellar.expert/explorer/public/account/" + ctx.coinData.wallet)
}
override fun getShareWalletUri(): Uri {
return if (ctx.card.denomination != null) {
Uri.parse(ctx.coinData.wallet + "?amount=" + convertToAmount(convertToInternalAmount(ctx.card.denomination)!!).toValueString())
} else {
Uri.parse(ctx.coinData.wallet)
}
}
override fun getAmountInputFilters(): Array<InputFilter> {
return arrayOf(DecimalDigitsInputFilter(decimals))
}
override fun checkNewTransactionAmount(amount: Amount): Boolean {
return true
}
override fun checkNewTransactionAmountAndFee(amountValue: Amount, feeValue: Amount, isIncludeFee: Boolean): Boolean {
return true
}
override fun validateBalance(balanceValidator: BalanceValidator): Boolean {
try {
if (!hasBalanceInfo()) {
balanceValidator.setScore(0)
balanceValidator.firstLine = R.string.balance_validator_first_line_no_connection
balanceValidator.setSecondLine(R.string.balance_validator_second_line_authenticity_not_verified)
return false
}
if (coinData!!.fundsFromTrustedSource) {
balanceValidator.setScore(100)
balanceValidator.firstLine = R.string.balance_validator_first_line_verified_in_blockchain
balanceValidator.setSecondLine(R.string.empty_string)
} else {
balanceValidator.setScore(0)
balanceValidator.firstLine = R.string.balance_validator_first_line_authenticity
balanceValidator.setSecondLine(R.string.empty_string)
}
return true
} catch (e: Exception) {
e.printStackTrace()
return false
}
}
override fun getBalance(): Amount? {
return if (!hasBalanceInfo()) null else coinData!!.getBalance()!!
}
override fun evaluateFeeEquivalent(fee: String): String {
return if (!coinData!!.amountEquivalentDescriptionAvailable) "" else try {
val feeAmount = Amount(fee, feeCurrency)
feeAmount.toEquivalentString(coinData!!.rate.toDouble())
} catch (e: Exception) {
""
}
}
override fun getBalanceEquivalent(): String {
if (coinData == null || !coinData!!.amountEquivalentDescriptionAvailable) return ""
val balance = balance ?: return ""
return balance.toEquivalentString(coinData!!.rate.toDouble())
}
override fun calculateAddress(pkUncompressed: ByteArray): String {
val kp = KeyPair.fromPublicKey(pkUncompressed)
return kp.accountId
}
override fun convertToAmount(internalAmount: InternalAmount): Amount {
val d = internalAmount.divide(multiplier)
return Amount(d, balanceCurrency)
}
override fun convertToAmount(strAmount: String, currency: String): Amount {
return Amount(strAmount, currency)
}
override fun convertToInternalAmount(amount: Amount): InternalAmount {
val d = amount.multiply(multiplier)
return InternalAmount(d, "stroops")
}
override fun convertToInternalAmount(bytes: ByteArray): InternalAmount? {
if (bytes == null) return null
val reversed = ByteArray(bytes.size)
for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1]
return InternalAmount(Util.byteArrayToLong(reversed), "stroops")
}
override fun convertToByteArray(internalAmount: InternalAmount): ByteArray {
val bytes = Util.longToByteArray(internalAmount.longValueExact())
val reversed = ByteArray(bytes.size)
for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1]
return reversed
}
override fun createCoinData(): CoinData {
return XlmTagData()
}
override fun getUnspentInputsDescription(): String {
return ""
}
@Throws(Exception::class)
override fun constructTransaction(amountValue: Amount, feeValue: Amount, IncFee: Boolean, targetAddress: String): SignTask.TransactionToSign {
var amountValue = amountValue
checkBlockchainDataExists()
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
StrictMode.setThreadPolicy(policy)
if (IncFee) {
amountValue = Amount(amountValue.subtract(feeValue), amountValue.currency)
}
val operation: Operation
operation = if (coinData!!.isTargetAccountCreated) PaymentOperation.Builder(KeyPair.fromAccountId(targetAddress), AssetTypeNative(), amountValue.toValueString()).build() else CreateAccountOperation.Builder(KeyPair.fromAccountId(targetAddress), amountValue.toValueString()).build()
val transaction = TransactionEx.buildEx(60, coinData!!.accountResponse, operation)
if (transaction.fee != convertToInternalAmount(feeValue).intValueExact()) {
throw Exception("Invalid fee!")
}
return object : SignTask.TransactionToSign {
override fun isSigningMethodSupported(signingMethod: TangemCard.SigningMethod): Boolean {
return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw
}
@Throws(Exception::class)
override fun getHashesToSign(): Array<ByteArray> {
val dataForSign = arrayOf(transaction.hash())
return dataForSign
}
@Throws(Exception::class)
override fun getRawDataToSign(): ByteArray {
return transaction.signatureBase()
}
override fun getHashAlgToSign(): String {
return "sha-256"
}
@Throws(Exception::class)
override fun getIssuerTransactionSignature(dataToSignByIssuer: ByteArray): ByteArray {
throw Exception("Issuer validation not supported!")
}
@Throws(Exception::class)
override fun onSignCompleted(signFromCard: ByteArray): ByteArray { // Sign the transaction to prove you are actually the person sending it.
transaction.setSign(signFromCard)
val txForSend = transaction.toEnvelopeXdrBase64().toByteArray()
notifyOnNeedSendTransaction(txForSend)
return txForSend
}
}
}
private fun checkTargetAccountCreated(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, targetAddress: String, amount: Amount) {
val serverApi = ServerApiStellar(ctx.blockchain)
val listener: ServerApiStellar.Listener = object : ServerApiStellar.Listener {
override fun onSuccess(request: StellarRequest.Base) {
coinData!!.isTargetAccountCreated = true
blockchainRequestsCallbacks.onComplete(true)
}
override fun onFail(request: StellarRequest.Base) {
Log.i(TAG, "onFail: " + request.javaClass.simpleName + " " + request.error)
if (request.errorResponse != null && request.errorResponse.code == 404) {
coinData!!.isTargetAccountCreated = false
if (amount.compareTo(coinData!!.reserve) >= 0) {
blockchainRequestsCallbacks.onComplete(true)
} else {
ctx.setError(R.string.confirm_transaction_error_not_enough_xlm_for_create)
blockchainRequestsCallbacks.onComplete(false)
}
} else { // suppose account is created if anything goes wrong
coinData!!.isTargetAccountCreated = true
blockchainRequestsCallbacks.onComplete(true)
}
}
}
serverApi.setListener(listener)
serverApi.requestData(ctx, StellarRequest.Balance(targetAddress))
}
private fun requestPayments(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) {
val server = Server("https://horizon.stellar.org/")
val accountKeyPair = KeyPair.fromAccountId(coinData!!.wallet)
try {
var operationsPage = server.payments().forAccount(accountKeyPair).limit(200).order(RequestBuilder.Order.DESC).execute()
Log.e("Stellar", operationsPage.records.toString())
operations.addAll(operationsPage.records)
while (operations.size < OPERATIONS_LIMIT) {
operationsPage = operationsPage.getNextPage(server.httpClient)
operations.addAll(operationsPage.records)
Log.e("Stellar", operationsPage.records.toString())
Log.e("Stellar", "Operations downloaded: " + operationsPage.records.count())
Log.e("Stellar", "Operations overall: " + operations.count())
}
parsePayments(blockchainRequestsCallbacks)
CoroutineScope(Dispatchers.Main).launch { blockchainRequestsCallbacks.onComplete(!ctx.hasError()) }
} catch (e: Exception) {
if (e.message != null) {
ctx.error = e.message
blockchainRequestsCallbacks.onComplete(false)
} else {
ctx.error = e.javaClass.name
blockchainRequestsCallbacks.onComplete(false)
}
}
}
private fun parsePayments(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) {
coinData!!.fundsFromTrustedSource = operations.find { it is PaymentOperationResponse && it.from.accountId == TRUSTED_SOURCE } != null
coinData!!.fundsSentToTrustedSource =
(operations.find { it is PaymentOperationResponse && it.to.accountId == TRUSTED_DESTINATION } != null) && coinData!!.fundsFromTrustedSource
Log.e("Stellar",
"fundsFromTrustedSource $coinData!!.fundsFromTrustedSource, " +
"fundsSentToTrustedSource $coinData!!.fundsSentToTrustedSource")
coinData!!.isBalanceReceived = true
}
override fun requestBalanceAndUnspentTransactions(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) {
CoroutineScope(Dispatchers.IO).launch { requestPayments(blockchainRequestsCallbacks) }
}
@Throws(Exception::class)
override fun requestFee(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, targetAddress: String, amount: Amount) {
coinData!!.maxFee = coinData!!.baseFee
coinData!!.normalFee = coinData!!.maxFee
coinData!!.minFee = coinData!!.normalFee
checkTargetAccountCreated(blockchainRequestsCallbacks, targetAddress, amount)
}
@Throws(IOException::class)
override fun requestSendTransaction(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, txForSend: ByteArray) {
val serverApi = ServerApiStellar(ctx.blockchain)
val listener: ServerApiStellar.Listener = object : ServerApiStellar.Listener {
override fun onSuccess(request: StellarRequest.Base) {
try {
if (!SubmitTransaction::class.java.isInstance(request)) throw Exception("Invalid request logic")
val submitTransactionRequest = request as SubmitTransaction
if (submitTransactionRequest.response.isSuccess) {
ctx.error = null
blockchainRequestsCallbacks.onComplete(true)
} else {
if (submitTransactionRequest.response.extras != null && submitTransactionRequest.response.extras.resultCodes != null) {
var trResult = submitTransactionRequest.response.extras.resultCodes.transactionResultCode
if (submitTransactionRequest.response.extras.resultCodes.operationsResultCodes != null && submitTransactionRequest.response.extras.resultCodes.operationsResultCodes.size > 0) {
trResult += "/" + submitTransactionRequest.response.extras.resultCodes.operationsResultCodes[0]
}
ctx.error = trResult
} else {
ctx.error = "transaction failed"
}
blockchainRequestsCallbacks.onComplete(false)
}
} catch (e: Exception) {
if (e.message != null) {
ctx.error = e.message
blockchainRequestsCallbacks.onComplete(false)
} else {
ctx.error = e.javaClass.name
blockchainRequestsCallbacks.onComplete(false)
}
}
}
override fun onFail(request: StellarRequest.Base) {
ctx.error = request.error
blockchainRequestsCallbacks.onComplete(false)
}
}
serverApi.setListener(listener)
val transaction = TransactionEx.fromEnvelopeXdr(String(txForSend))
coinData!!.incSequenceNumber()
serverApi.requestData(ctx, SubmitTransaction(transaction))
}
override fun needMultipleLinesForBalance(): Boolean {
return true
}
override fun allowSelectFeeLevel(): Boolean {
return false
}
override fun pendingTransactionTimeoutInSeconds(): Int {
return 10
}
companion object {
private val TAG = XlmTagEngine::class.java.simpleName
private val decimals: Int
private get() = 7
private val multiplier = BigDecimal("10000000")
const val OPERATIONS_LIMIT = 200
private val TRUSTED_DESTINATION = "GAYPZMHFZERB42ONEJ4CY6ADDVTINEXMY6OZ5G6CLR4HHVKOSNJSZGMM"
private val TRUSTED_SOURCE = "GAZY7H4BWWEVB6QGB4RV3LW7DH5NO5CD5O6JCEQXA7N2UCGZSAPJFYW2"
}
}

View file

@ -228,11 +228,11 @@ public class XrpEngine extends CoinEngine {
}
}
if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) {
balanceValidator.setScore(80);
balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
}
// if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) {
// balanceValidator.setScore(80);
// balanceValidator.setFirstLine(R.string.balance_validator_first_line_verified_offline);
// balanceValidator.setSecondLine(R.string.balance_validator_second_line_internet_to_get_balance);
// }
return true;
} catch (Exception e) {

View file

@ -198,7 +198,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:fontFamily="@font/maax"
android:text="The card is linked"
android:text="@string/details_linked_card_title"
android:textSize="@dimen/text_size_1_small" />
<TextView

View file

@ -0,0 +1,9 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.tangem.ui.fragment.additional.TagFragment">
<include layout="@layout/fr_loaded_wallet"/>
</FrameLayout>

View file

@ -28,6 +28,9 @@
<action
android:id="@+id/action_main_to_settingsFragment"
app:destination="@id/settingsFragment" />
<action
android:id="@+id/action_main_to_tagFragment"
app:destination="@id/tagFragment" />
</fragment>
<fragment
@ -188,5 +191,10 @@
android:id="@+id/prepareKrakenWithdrawalFragment"
android:name="com.tangem.ui.fragment.additional.PrepareKrakenWithdrawalFragment"
android:label="PrepareKrakenWithdrawalFragment" />
<fragment
android:id="@+id/tagFragment"
android:name="com.tangem.ui.fragment.additional.TagFragment"
android:label="fragment_tag"
tools:layout="@layout/fragment_tag" />
</navigation>

View file

@ -9,9 +9,9 @@
<string name="general_continue">Continuer</string>
<string name="error_empty_pin">Le code PIN est vide</string>
<string name="general_blockchain">Blockchain</string>
<string name="general_notification_scan_again">Réessayez de numériser</string>
<string name="general_notification_scan_again">Réessayez de scanner la carte</string>
<string name="general_error_cannot_erase_wallet_with_non_zero_balance">Impossible d\'effacer le porte-monnaie avec un solde différent de zéro</string>
<string name="general_send_transaction">Envoyer le paiement</string>
<string name="general_send_transaction">Transférer les fonds</string>
<string name="general_from_card">Depuis la carte</string>
<string name="general_on_card">sur la carte</string>
<string name="general_balance">avec solde</string>
@ -49,9 +49,9 @@
<string name="dialog_the_nfc_adapter_length_apdu">Oups .. Il semble que votre smartphone ne supporte pas de tels paquets NFC.</string>
<string name="dialog_the_nfc_adapter_length_apdu_advice">Essayez d\'envoyer une quantité inférieure ou utilisez un smartphone avec prise en charge complète de NFC.</string>
<string name="dialog_title_money_is_at_risk">Votre argent est en jeu!</string>
<string name="dialog_this_card_has_enforced_security_delay">Cette carte a imposé un délai de sécurité</string>
<string name="dialog_hold_card">Veuillez tenir la carte fermement \ n jusqu\'à ce que l\'opération soit terminée…</string>
<string name="dialog_you_may_be_required_to_repeat">Vous devrez peut-être répéter cette opération plusieurs fois en fonction des performances NFC de votre smartphone. \ n Ceci est fait pour sécuriser vos fonds.</string>
<string name="dialog_this_card_has_enforced_security_delay">Cette carte impose un délai de sécurité pour confirmer le transfert des fonds</string>
<string name="dialog_hold_card">Veuillez maintenir la carte dans cette position jusqu\'à ce que l\'opération soit terminée…</string>
<string name="dialog_you_may_be_required_to_repeat">Vous devrez peut-être répéter cette opération plusieurs fois en fonction des performances NFC de votre smartphone.\nCeci est fait pour sécuriser vos fonds.</string>
<!-- menu main -->
<string name="main_menu_manage_pin_1">Gérer le code PIN de l\'utilisateur…</string>
@ -74,12 +74,14 @@
<!-- Main -->
<string name="main_screen_tap_card">Mettez en contact la carte avec votre smartphone</string>
<string name="main_screen_scan_card">Scanner une carte avec votre \n %1$s \n comme indiqué ci-dessus</string>
<string name="main_screen_scan_card">Scanner votre carte avec votre %1$s comme indiqué ci-dessus</string>
<string name="main_screen_phone">phone</string>
<string name="main_screen_erased_wallet">Le porte-monnaie a été effacé</string>
<string name="main_screen_not_personalized">Non personnalisé</string>
<string name="main_screen_new_version_toast">Il existe une nouvelle version de l\'application: %1$s</string>
<string name="main_screen_btn_update">Mettre à jour</string>
<string name="main_screen_visit_store">Vous navez pas de carte?\nVisitez notre boutique sur %1$s</string>
<!-- LoadedWallet -->
<string name="loaded_wallet_no_compatible_wallet">Aucun porte-monnaie compatible installé</string>
@ -146,10 +148,10 @@
<string name="balance_validator_second_line_authenticity_not_verified">L\'authenticité ne peut pas être vérifiée. Balayez vers le bas pour actualiser.</string>
    
<!-- CreateNewWallet, Purge, SignTransaction -->
<string name="now_touch_the_card_with_id"> Maintenant, touchez la carte avec l\'ID </string>
<string name="now_touch_the_card_with_id">Maintenir la carte avec l\ID</string>
<string name="to_create_the_wallet">pour créer le porte-monnaie </string>
<string name="to_erase_the_wallet">pour effacer le porte-monnaie</string>
<string name="to_sign_the_transaction">pour signer le paiement</string>
<string name="to_sign_the_transaction">pour confirmer le transfert des fonds</string>
<string name="to_change_pin_codes">pour changer les codes PIN / PIN2</string>
<string name="nfc_purge_warning">En mettant la carte en contact avec votre smartphone, vous retirerez définitivement le porte-monnaie de la blockchain. Assurez-vous qu\'il n\'y aura pas d\'autres transactions entrantes</string>
<string name="nfc_error_cannot_erase_wallet">Impossible d\'effacer le porte-monnaie. Veillez à entrer le code PIN2 correct!</string>
@ -178,7 +180,7 @@
<!-- PrepareTransaction -->
<string name="prepare_transaction_hint_enter_address">entrez l\'adresse du porte-monnaie</string>
<string name="prepare_transaction_hint_enter_amount">entrer le montant</string>
<string name="prepare_transaction_btn_verify">Vérifiez</string>
<string name="prepare_transaction_btn_verify">Vérifier</string>
<string name="prepare_transaction_error_not_enough_funds">Pas assez de fonds</string>
<string name="prepare_transaction_error_unknown_amount_format">Format de montant inconnu</string>
<string name="prepare_transaction_error_incorrect_destination">Adresse du porte-monnaie de destination incorrecte</string>
@ -223,12 +225,12 @@
<string name="details_category_manufacturer">Fabricant</string>
<string name="details_category_wallet">Porte-monnaie</string>
<string name="details_card_identity">Identité de la carte</string>
<string name="details_attested">Attested</string>
<string name="details_attested">Attestée</string>
<string name="details_not_confirmed">Non confirmé</string>
<string name="details_reusable">Réutilisable</string>
<string name="details_none">None</string>
<string name="details_last_one">Le dernier!</string>
<string name="details_unlimited">Unlimited</string>
<string name="details_unlimited">illimitées</string>
<string name="details_one_off_card">Carte ponctuelle</string>
<string name="details_firmware">Micrologiciel</string>
<string name="details_registration_date">Date d\'enregistrement</string>
@ -238,7 +240,7 @@
<string name="details_category_issuer">Émetteur</string>
<string name="details_private_key">Clé privée</string>
<string name="details_validation_node">Noeud de validation</string>
<string name="details_unspents">Unspents</string>
<string name="details_unspents">Transferts</string>
<string name="details_protected_by_default_pin_1">Cette carte est protégée par le code PIN1 par défaut</string>
<string name="details_protected_by_user_pin_1">Cette carte est protégée par le code PIN1 de lutilisateur</string>
<string name="details_protected_by_default_pin_2">Cette carte est protégée par le code PIN2 par défaut</string>
@ -255,8 +257,9 @@
<string name="details_blockable">Bloquable\n</string>
<string name="details_atomic_commmands">Mode de commande atomique\n</string>
<string name="details_linking_card_supported">La liaison au terminal est prise en charge\n</string>
<string name="details_linked_card_title">La carte est liée</string>
<string name="details_linked_card_title">La carte est associée</string>
<string name="details_linked_card_to_phone">à ce téléphone</string>
<string name="details_unspents_number">%1$d envoyé(s), %2$d reçu(s)</string>
<!-- PinSave -->
<string name="pin_save_btn_save">Enregistrer</string>

View file

@ -82,7 +82,7 @@
<string name="main_screen_new_version_toast">There is a new application version: %1$s</string>
<string name="main_screen_btn_update">Update</string>
<string name="main_screen_visit_store">Don\'t have a card?\n Visit our store at %1$s</string>
<string name="main_screen_store_address">tangemcards.com</string>
<string name="main_screen_store_address" translatable="false">tangemcards.com</string>
<!-- LoadedWallet -->
<string name="loaded_wallet_no_compatible_wallet">No compatible wallets installed</string>
@ -262,6 +262,13 @@
<string name="details_linking_card_supported">Linking to the terminal is supported\n</string>
<string name="details_linked_card_title">The card is linked</string>
<string name="details_linked_card_to_phone">To this phone</string>
<string name="details_unspents_number">%1$d unspents (%2$d received)</string>
<!-- TagFragment -->
<string name="tag_claim">Claim</string>
<string name="tag_genuine">GENUINE</string>
<string name="tag_not_genuine">NOT GENUINE</string>
<string name="tag_claimed">ALREADY CLAIMED</string>
<!-- PinSave -->
<string name="pin_save_btn_save">Save</string>

View file

@ -3,5 +3,6 @@
<tech-list>
<tech>android.nfc.tech.IsoDep</tech>
<tech>android.nfc.tech.Ndef</tech>
<tech>android.nfc.tech.NfcV</tech>
</tech-list>
</resources>

View file

@ -6,7 +6,7 @@ buildscript {
maven { url 'https://maven.fabric.io/public' }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.2'
classpath 'com.android.tools.build:gradle:3.5.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
classpath 'io.fabric.tools:gradle:1.31.0'

View file

@ -47,7 +47,7 @@ class LocalStorage
artworks = HashMap()
}
if (artworks.count() < 29) {
if (artworks.count() < 32) {
// forceSave=true only on the last one
putResourceArtworkToCatalog(R.drawable.card_default, false)
putResourceArtworkToCatalog(R.drawable.card_default_nft, false)
@ -70,6 +70,7 @@ class LocalStorage
putResourceArtworkToCatalog(R.drawable.card_ru031, false)
putResourceArtworkToCatalog(R.drawable.card_ru032, false)
putResourceArtworkToCatalog(R.drawable.card_ru037, false)
putResourceArtworkToCatalog(R.drawable.card_ru038, false)
putResourceArtworkToCatalog(R.drawable.card_ru039, false)
putResourceArtworkToCatalog(R.drawable.card_ru040, false)
putResourceArtworkToCatalog(R.drawable.card_ru041, false)
@ -77,6 +78,8 @@ class LocalStorage
putResourceArtworkToCatalog(R.drawable.card_ru043, false)
putResourceArtworkToCatalog(R.drawable.card_tg044, false)
putResourceArtworkToCatalog(R.drawable.card_tg046, false)
putResourceArtworkToCatalog(R.drawable.card_tgslix, false)
putResourceArtworkToCatalog(R.drawable.card_bc00, false)
putResourceArtworkToCatalog(R.drawable.card_ff32, true)
}
if (batchesFile.exists()) {
@ -237,6 +240,9 @@ class LocalStorage
hexCID in "CB25000000000000".."CB25000000099999" -> R.drawable.card_ru043
hexCID in "CB26000000000000".."CB26000000099999" -> R.drawable.card_tg044
//Sergio's business card
hexCID in "BC00000000000000".."BC99999999999999" -> R.drawable.card_bc00
card.batch == "0004" -> R.drawable.card_ru006
card.batch == "0006" -> R.drawable.card_ru006
card.batch == "0010" -> R.drawable.card_ru006

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View file

@ -29,6 +29,7 @@ public class TangemCard {
private byte[] terminalPrivateKey;
private byte[] terminalPublicKey;
private boolean terminalIsLinked = false;
private byte[] tagSignature = null;
public String getBlockchainID() {
return blockchainID;
@ -629,6 +630,14 @@ public class TangemCard {
DenominationText = null;
}
public byte[] getTagSignature() {
return tagSignature;
}
public void setTagSignature(byte[] tagSignature) {
this.tagSignature = tagSignature;
}
// public Bundle getAsBundle() {
// Bundle B = new Bundle();
// saveToBundle(B);

View file

@ -17,4 +17,5 @@ public interface NfcReader {
void connect();
boolean isConnected();
}

View file

@ -69,4 +69,20 @@ public class TLVList extends ArrayList<TLV> {
while (tlv != null);
return tlvList;
}
public static TLVList tryFromBytes(byte[] mData) {
TLVList tlvList = new TLVList();
ByteArrayInputStream stream = new ByteArrayInputStream(mData);
TLV tlv = null;
do {
try {
tlv = TLV.ReadFromStream(stream);
if (tlv != null) tlvList.add(tlv);
} catch (IOException e) {
break;
}
}
while (tlv != null);
return tlvList;
}
}

View file

@ -10,7 +10,8 @@ data class CardEnvironment(
val pin2: String = DEFAULT_PIN2,
val cardId: String? = null,
val terminalKeys: KeyPair? = null,
val encryptionKey: ByteArray? = null
val encryptionKey: ByteArray? = null,
val cvc: ByteArray? = null
) {
companion object {

View file

@ -122,6 +122,37 @@ class CardManager(
runTask(task, cardId, callback)
}
/**
* 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.
* @param cardId CID, Unique Tangem card ID number.
*/
fun createWallet(cardId: String,
callback: (result: TaskEvent<CreateWalletResponse>) -> Unit) {
val createWalletCommand = CreateWalletCommand(cardId)
val task = SingleCommandTask(createWalletCommand)
runTask(task, cardId, callback)
}
/**
* 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.
* @param cardId CID, Unique Tangem card ID number.
*/
fun purgeWallet(cardId: String,
callback: (result: TaskEvent<PurgeWalletResponse>) -> Unit) {
val purgeWalletCommand = PurgeWalletCommand(cardId)
val task = SingleCommandTask(purgeWalletCommand)
runTask(task, cardId, callback)
}
/**
*/
@ -157,6 +188,6 @@ class CardManager(
}
private fun fetchCardEnvironment(cardId: String?): CardEnvironment {
return cardEnvironmentRepository[cardId] ?: CardEnvironment()
return cardEnvironmentRepository[cardId] ?: CardEnvironment(cardId = cardId)
}
}

View file

@ -0,0 +1,71 @@
package com.tangem.commands
import com.tangem.CardEnvironment
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.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.tasks.TaskError
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(
private val cardId: String
) : CommandSerializer<CreateWalletResponse>() {
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvData = mutableListOf(
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
Tlv(TlvTag.CardId, cardId.hexToBytes()),
Tlv(TlvTag.Pin2, cardEnvironment.pin2.calculateSha256())
)
if (cardEnvironment.cvc != null) {
tlvData.add(Tlv(TlvTag.Cvc, cardEnvironment.cvc))
}
return CommandApdu(Instruction.CreateWallet, tlvData)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): CreateWalletResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
return try {
val mapper = TlvMapper(tlvData)
CreateWalletResponse(
cardId = mapper.map(TlvTag.CardId),
status = mapper.map(TlvTag.Status),
walletPublicKey = mapper.map(TlvTag.WalletPublicKey)
)
} catch (exception: Exception) {
throw TaskError.SerializeCommandError()
}
}
}

View file

@ -0,0 +1,57 @@
package com.tangem.commands
import com.tangem.CardEnvironment
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.TlvMapper
import com.tangem.common.tlv.TlvTag
import com.tangem.tasks.TaskError
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(
private val cardId: String
) : CommandSerializer<PurgeWalletResponse>() {
override fun serialize(cardEnvironment: CardEnvironment): CommandApdu {
val tlvData = mutableListOf(
Tlv(TlvTag.Pin, cardEnvironment.pin1.calculateSha256()),
Tlv(TlvTag.CardId, cardId.hexToBytes()),
Tlv(TlvTag.Pin2, cardEnvironment.pin2.calculateSha256())
)
return CommandApdu(Instruction.PurgeWallet, tlvData)
}
override fun deserialize(cardEnvironment: CardEnvironment, responseApdu: ResponseApdu): PurgeWalletResponse? {
val tlvData = responseApdu.getTlvData() ?: return null
return try {
val mapper = TlvMapper(tlvData)
PurgeWalletResponse(
cardId = mapper.map(TlvTag.CardId),
status = mapper.map(TlvTag.Status))
} catch (exception: Exception) {
throw TaskError.SerializeCommandError()
}
}
}

View file

@ -52,7 +52,7 @@ enum class TlvTag(val code: Int) {
Pause(0x1C),
ManufactureId(0x20),
ManufacturerSignature(0x21),
ManufacturerSignature(0x86),
IssuerDataPublicKey(0x30),
IssuerTransactionPublicKey(0x31),

View file

@ -45,6 +45,8 @@ class MainActivity : AppCompatActivity() {
tv_card_cid?.text = cardId
btn_sign.isEnabled = true
btn_read_issuer_data.isEnabled = true
btn_purge_wallet.isEnabled = true
btn_create_wallet.isEnabled = true
}
}
}
@ -103,6 +105,32 @@ class MainActivity : AppCompatActivity() {
}
}
}
btn_purge_wallet?.setOnClickListener { _ ->
cardManager.purgeWallet(
cardId) {
when (it) {
is TaskEvent.Completion -> {
if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName }
}
is TaskEvent.Event -> runOnUiThread {
tv_card_cid?.text = it.data.status.name
}
}
}
}
btn_create_wallet?.setOnClickListener { _ ->
cardManager.createWallet(
cardId) {
when (it) {
is TaskEvent.Completion -> {
if (it.error != null) runOnUiThread { tv_card_cid?.text = it.error!!::class.simpleName }
}
is TaskEvent.Event -> runOnUiThread {
tv_card_cid?.text = it.data.status.name
}
}
}
}
}
private fun createSampleHashes(): Array<ByteArray> {

View file

@ -9,12 +9,12 @@
<TextView
android:paddingBottom="48dp"
android:layout_marginTop="48dp"
android:id="@+id/tv_card_cid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
android:layout_marginBottom="80dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
@ -25,6 +25,7 @@
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Read and verify card"
app:layout_constraintVertical_bias="0.3"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
@ -63,5 +64,27 @@
app:layout_constraintTop_toBottomOf="@id/btn_read_issuer_data"
android:enabled="false"/>
<Button
android:id="@+id/btn_purge_wallet"
android:layout_marginTop="24dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Purge wallet"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_write_issuer_data"
android:enabled="false"/>
<Button
android:id="@+id/btn_create_wallet"
android:layout_marginTop="24dp"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Create wallet"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_purge_wallet"
android:enabled="false"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -24,7 +24,8 @@ class NfcManager(private val activity: FragmentActivity, private val readerCallb
val TAG: String = NfcManager::class.java.simpleName
// reader mode flags: listen for type A (not B), skipping ndef check
private const val READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK or NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS
private const val READER_FLAGS = NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_NFC_V or
NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK or NfcAdapter.FLAG_READER_NO_PLATFORM_SOUNDS
private const val DELAY_PRESENCE = 1500
private const val REQUEST_NFC_PERMISSIONS = 1

View file

@ -38,4 +38,8 @@ data class NfcReader(
isoDep.timeout = timeout
}
override fun isConnected(): Boolean {
return isoDep.isConnected
}
}

View file

@ -0,0 +1,39 @@
package com.tangem.tangem_sdk.android.reader
import android.nfc.tech.NfcV
data class NfcVReader(
var nfcV: NfcV?
) : com.tangem.tangem_card.reader.NfcReader {
override fun getId(): ByteArray? {
return nfcV?.tag?.id
}
override fun setTimeout(timeout: Int) {
}
override fun getTimeout(): Int {
return 0
}
override fun transceive(data: ByteArray?): ByteArray? {
return nfcV?.transceive(data)
}
override fun ignoreTag() {
nfcV?.close()
nfcV = null
}
override fun notifyReadResult(success: Boolean) {
}
override fun connect() {
nfcV?.connect()
}
override fun isConnected(): Boolean {
return nfcV?.isConnected == true
}
}

View file

@ -0,0 +1,120 @@
package com.tangem.tangem_sdk.android.reader
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import com.tangem.tangem_card.reader.NfcReader
import com.tangem.tangem_card.reader.TLV
import com.tangem.tangem_card.reader.TLVList
import com.tangem.tangem_card.util.Util
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.*
class ReadSlixTagTask(val nfcReader: NfcReader) {
private var lastErrorCode: Int = 0
fun read(): ReadResult {
if (!nfcReader.isConnected) {
try {
nfcReader.connect()
} catch (e: Exception) {
return ReadResult.Failure(e)
}
}
return try {
runRead()
} catch (e: Exception) {
nfcReader.ignoreTag()
ReadResult.Failure(e)
}
}
private fun runRead(): ReadResult {
val ndefMessage = runReadNDEF()
val records: Array<NdefRecord> = ndefMessage.records
for (record in records) {
if (record.toUri() != null && record.toUri().toString() == "vnd.android.nfc://ext/tangem.com:wallet") {
val payload = record.payload
val tlvNDEF = TLVList.fromBytes(Arrays.copyOfRange(payload, 2, payload.size))
nfcReader.ignoreTag()
return ReadResult.Success(tlvNDEF)
}
}
return ReadResult.Failure(Exception("No parcelable Tlv has been found."))
}
private fun runReadNDEF(): NdefMessage {
val answerCC = readSingleBlock(0x00)
if (answerCC.size != 4 || answerCC[0].toInt() != 0xE1 || ((answerCC[1].toInt() and 0xF0) != 0x40)) {
} else {
throw Exception("Failed! Invalid CC read " + Util.bytesToHex(answerCC))
}
if ((answerCC[3].toInt() and 0x01) != 0x01) {
throw Exception("Multiple block read unsupported!")
}
val areaSize = 8 * answerCC[2]
val blocksCount = areaSize / 4
val areaBuf = readMultipleBlocks(1, blocksCount)
val tlvNDEF = TLVList.tryFromBytes(areaBuf)
return NdefMessage(tlvNDEF.getTLV(TLV.Tag.TAG_CardPublicKey).Value)
}
private fun readSingleBlock(blockNo: Int): ByteArray {
return doTransceive(0x20, blockNo)!!
}
private fun readMultipleBlocks(startBlock: Int, blocksCount: Int): ByteArray {
val resultBuf = ByteArrayOutputStream()
val maxBlocksAtOnce = 32
var blocksRemaining = blocksCount
var firstBlockToRead = startBlock
while (blocksRemaining > 0) {
val blocksToRead = if (blocksRemaining > maxBlocksAtOnce) {
maxBlocksAtOnce
} else {
blocksRemaining
}
val blocks = doTransceive(0x23.toByte(), firstBlockToRead, blocksToRead - 1)
blocksRemaining -= blocksToRead
firstBlockToRead += blocksToRead
resultBuf.write(blocks!!)
}
return resultBuf.toByteArray()
}
private fun doTransceive(cmd: Byte, p1: Int?, p2: Int? = null, params: ByteArray? = null): ByteArray? {
val command: ByteArray
val res: ByteArray?
lastErrorCode = -1
val os = ByteArrayOutputStream()
os.write(REQ_FLAG.toInt())
os.write(cmd.toInt())
p1?.let { os.write(p1) }
p2?.let { os.write(it) }
params?.let { os.write(params, 0, params.size) }
command = os.toByteArray()
if (!nfcReader.isConnected) throw IOException("Connection lost")
res = nfcReader.transceive(command)
lastErrorCode = res[0].toInt()
if (lastErrorCode != 0) {
throw IOException("Error! Code: " + String.format("0x%02x", lastErrorCode))
}
return res.copyOfRange(1, res.size)
}
companion object{
// iso15693 flags
private const val REQ_FLAG: Byte = 0x02
}
}
sealed class ReadResult {
data class Success(val tlvs: TLVList) : ReadResult()
data class Failure(val exception: Exception) : ReadResult()
}

View file

@ -85,6 +85,8 @@ fun TangemCard.loadFromBundle(B: Bundle) {
if (B.containsKey("terminalPublicKey")) terminalPublicKey = B.getByteArray("terminalPublicKey")
terminalIsLinked = B.getBoolean("terminalIsLinked")
if (B.containsKey("tagSignature")) tagSignature = B.getByteArray("tagSignature")
}
val TangemCard.asBundle: Bundle
@ -112,11 +114,10 @@ fun TangemCard.saveToBundle(B: Bundle) {
B.putInt("Health", health)
if (settingsMask != null) B.putInt("settingsMask", settingsMask)
B.putInt("pauseBeforePIN2", pauseBeforePIN2)
if( allowedSigningMethod!=null ){
var iSigningMethod=0x80
for(sM in allowedSigningMethod)
{
iSigningMethod=iSigningMethod.or(0x01.shl(sM.ID))
if (allowedSigningMethod != null) {
var iSigningMethod = 0x80
for (sM in allowedSigningMethod) {
iSigningMethod = iSigningMethod.or(0x01.shl(sM.ID))
}
B.putInt("signingMethod", iSigningMethod)
}
@ -163,6 +164,8 @@ fun TangemCard.saveToBundle(B: Bundle) {
B.putByteArray("terminalPublicKey", terminalPublicKey)
B.putBoolean("terminalIsLinked", terminalIsLinked)
if (tagSignature != null) B.putByteArray("tagSignature", tagSignature)
} catch (e: Exception) {
Log.e("Can't save to bundle ", e.message)
}