diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index 782234d4ed..e858887213 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -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; diff --git a/app/src/main/java/com/tangem/data/network/InsightApi.java b/app/src/main/java/com/tangem/data/network/InsightApi.java index 63eb570f1c..4095326dc5 100644 --- a/app/src/main/java/com/tangem/data/network/InsightApi.java +++ b/app/src/main/java/com/tangem/data/network/InsightApi.java @@ -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 insightAddress(@Path("address") String address); @GET(ServerApiInsight.INSIGHT_UNSPENT_OUTPUTS) - Call> insightUnspent(@Path("address") String address); + Call> insightUnspent(@Path("address") String address); @GET(ServerApiInsight.INSIGHT_TRANSACTION) Call insightTransaction(@Path("txId") String txId); diff --git a/app/src/main/java/com/tangem/data/network/ServerApiInsight.java b/app/src/main/java/com/tangem/data/network/ServerApiInsight.java index e3eae3384a..5a768471e6 100644 --- a/app/src/main/java/com/tangem/data/network/ServerApiInsight.java +++ b/app/src/main/java/com/tangem/data/network/ServerApiInsight.java @@ -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 utxoList); + void onSuccess(String method, List 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> call = insightApi.insightUnspent(wallet); - call.enqueue(new Callback>() { + Call> call = insightApi.insightUnspent(wallet); + call.enqueue(new Callback>() { @Override - public void onResponse(@NonNull Call> call, @NonNull Response> response) { + public void onResponse(@NonNull Call> call, @NonNull Response> response) { requestsCount--; if (response.code() == 200) { @@ -77,7 +78,7 @@ public class ServerApiInsight { } @Override - public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + public void onFailure(@NonNull Call> 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)); diff --git a/app/src/main/java/com/tangem/data/network/ServerApiStellar.java b/app/src/main/java/com/tangem/data/network/ServerApiStellar.java index f364820eb1..77427e3fc4 100644 --- a/app/src/main/java/com/tangem/data/network/ServerApiStellar.java +++ b/app/src/main/java/com/tangem/data/network/ServerApiStellar.java @@ -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 { diff --git a/app/src/main/java/com/tangem/data/network/ServerApiTezos.java b/app/src/main/java/com/tangem/data/network/ServerApiTezos.java new file mode 100644 index 0000000000..b5300912ca --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/ServerApiTezos.java @@ -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 accountObserver) { + requestsCount++; + Log.i(TAG, "new getAddress request"); + + Single accountSingle = tezosApi.getAccount(wallet) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnEvent((object, throwable) -> requestsCount--); + + accountSingle.subscribe(accountObserver); + } + + public void getMangerKey(String wallet, SingleObserver accountObserver) { + requestsCount++; + Log.i(TAG, "new getManagerKey request"); + + Single 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 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 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 tezosPreapplyBodyList = new ArrayList<>(); + tezosPreapplyBodyList.add(tezosPreapplyBody); + Response 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 injectObserver) { + requestsCount++; + Log.i(TAG, "new injectOperations request"); + + Single injectSingle = tezosApi.injectOperations(txForSend) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnEvent((object, throwable) -> requestsCount--); + + injectSingle.subscribe(injectObserver); + } +} diff --git a/app/src/main/java/com/tangem/data/network/TezosApi.java b/app/src/main/java/com/tangem/data/network/TezosApi.java new file mode 100644 index 0000000000..e61809b3d7 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/TezosApi.java @@ -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 getAccount(@Path("address") String address); + + @GET(ServerApiTezos.TEZOS_HEADER) + Call getHeader(); + + @GET(ServerApiTezos.TEZOS_MANAGER_KEY) + Single getManagerKey(@Path("address") String address); + + @POST(ServerApiTezos.TEZOS_FORGE_OPERATIONS) + Call forgeOperations(@Body TezosForgeBody tezosForgeBody); + + @POST(ServerApiTezos.TEZOS_PREAPPLY_OPERATIONS) + Call preapplyOperations(@Body List tezosPreapplyBodyList); + + @POST(ServerApiTezos.TEZOS_INJECT_OPERATIONS) + Single injectOperations(@Body String txForSend); +} diff --git a/app/src/main/java/com/tangem/data/network/model/InsightResponse.kt b/app/src/main/java/com/tangem/data/network/model/InsightResponse.kt index 4910b36695..a16c535649 100644 --- a/app/src/main/java/com/tangem/data/network/model/InsightResponse.kt +++ b/app/src/main/java/com/tangem/data/network/model/InsightResponse.kt @@ -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 ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/model/TezosBody.kt b/app/src/main/java/com/tangem/data/network/model/TezosBody.kt new file mode 100644 index 0000000000..16bf34b145 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/TezosBody.kt @@ -0,0 +1,25 @@ +package com.tangem.data.network.model + +data class TezosForgeBody( + val branch: String, + val contents: List +) + +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, + val signature: String +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/model/TezosResponse.kt b/app/src/main/java/com/tangem/data/network/model/TezosResponse.kt new file mode 100644 index 0000000000..db3bece398 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/TezosResponse.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt index a8b9b4ac02..3c4b5285c5 100644 --- a/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt +++ b/app/src/main/java/com/tangem/ui/fragment/MainFragment.kt @@ -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() diff --git a/app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt b/app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt new file mode 100644 index 0000000000..8514ccf37c --- /dev/null +++ b/app/src/main/java/com/tangem/ui/fragment/additional/TagFragment.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt index 10aaf161f1..1f888ee616 100644 --- a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt +++ b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt @@ -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) { diff --git a/app/src/main/java/com/tangem/wallet/TangemContext.java b/app/src/main/java/com/tangem/wallet/TangemContext.java index 07cb229329..1f71dc9873 100644 --- a/app/src/main/java/com/tangem/wallet/TangemContext.java +++ b/app/src/main/java/com/tangem/wallet/TangemContext.java @@ -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 + "
" + getBlockchain().getOfficialName() + " smart contract token"; + return card.getTokenSymbol()+ "
" + getBlockchain().getOfficialName() + " smart contract token"; } if (blockchain == Blockchain.NftToken) { - return card.getTokenSymbol().substring(4) + "
" + getBlockchain().getOfficialName() + " NFT token"; + return card.getTokenSymbol().substring(4) + "
" + getBlockchain().getOfficialName() + " non-fungible token"; } if (blockchain == Blockchain.StellarAsset) { - return card.getTokenSymbol() + "
" + getBlockchain().getOfficialName() + " asset"; + return card.getTokenSymbol() + "
" + getBlockchain().getOfficialName() + " asset"; + } + if (blockchain == Blockchain.StellarTag) { + return "TANGEM TAG
"+ getBlockchain().getOfficialName() + " non-fungible token "; } return blockchain.getOfficialName(); } diff --git a/app/src/main/java/com/tangem/wallet/Transaction.java b/app/src/main/java/com/tangem/wallet/Transaction.java index 0d4e54fd51..07dc9b42e5 100644 --- a/app/src/main/java/com/tangem/wallet/Transaction.java +++ b/app/src/main/java/com/tangem/wallet/Transaction.java @@ -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); } diff --git a/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java b/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java index 03a2d3c2ce..f5614c7d03 100644 --- a/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java +++ b/app/src/main/java/com/tangem/wallet/bch/BtcCashEngine.java @@ -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 diff --git a/app/src/main/java/com/tangem/wallet/binance/BinanceData.java b/app/src/main/java/com/tangem/wallet/binance/BinanceData.java index de90f5c65a..bfe3b630cf 100644 --- a/app/src/main/java/com/tangem/wallet/binance/BinanceData.java +++ b/app/src/main/java/com/tangem/wallet/binance/BinanceData.java @@ -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) { diff --git a/app/src/main/java/com/tangem/wallet/binance/BinanceEngine.java b/app/src/main/java/com/tangem/wallet/binance/BinanceEngine.java index 9d4a9be074..48ae73ef42 100644 --- a/app/src/main/java/com/tangem/wallet/binance/BinanceEngine.java +++ b/app/src/main/java/com/tangem/wallet/binance/BinanceEngine.java @@ -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) { diff --git a/app/src/main/java/com/tangem/wallet/btc/BtcData.java b/app/src/main/java/com/tangem/wallet/btc/BtcData.java index 71c1d7c8f1..0d0eca1d85 100644 --- a/app/src/main/java/com/tangem/wallet/btc/BtcData.java +++ b/app/src/main/java/com/tangem/wallet/btc/BtcData.java @@ -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; } } diff --git a/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java b/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java index 021ce73e38..49da3cd0a5 100644 --- a/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java +++ b/app/src/main/java/com/tangem/wallet/btc/BtcEngine.java @@ -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 diff --git a/app/src/main/java/com/tangem/wallet/btc/Unspents.kt b/app/src/main/java/com/tangem/wallet/btc/Unspents.kt new file mode 100644 index 0000000000..12029683e3 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/btc/Unspents.kt @@ -0,0 +1,3 @@ +package com.tangem.wallet.btc + +data class Unspents(val unspetns: Int, val gatheredUnspents: Int) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/cardano/CardanoData.java b/app/src/main/java/com/tangem/wallet/cardano/CardanoData.java index e3ba66dc73..44e69eed30 100644 --- a/app/src/main/java/com/tangem/wallet/cardano/CardanoData.java +++ b/app/src/main/java/com/tangem/wallet/cardano/CardanoData.java @@ -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) { diff --git a/app/src/main/java/com/tangem/wallet/cardano/CardanoEngine.java b/app/src/main/java/com/tangem/wallet/cardano/CardanoEngine.java index c9a700254f..e51226a99b 100644 --- a/app/src/main/java/com/tangem/wallet/cardano/CardanoEngine.java +++ b/app/src/main/java/com/tangem/wallet/cardano/CardanoEngine.java @@ -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 diff --git a/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java b/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java index e108b9373b..f79127ae7f 100644 --- a/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java +++ b/app/src/main/java/com/tangem/wallet/ducatus/DucatusEngine.java @@ -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 unspentOutputs; + ArrayList unspentOutputs = new ArrayList<>(); checkBlockchainDataExists(); String myAddress = ctx.getCoinData().getWallet(); byte[] pbKey = ctx.getCard().getWalletPublicKey(); - // Build script for our address - List rawTxList = coinData.getUnspentTransactions(); - byte[] outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(myAddress).bytes; +// // Build script for our address +// List 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 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 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 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 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 utxoList) { + public void onSuccess(String method, List utxoList) { Log.e(TAG, "Wrong response body, InsightResponse expected"); } diff --git a/app/src/main/java/com/tangem/wallet/eos/EosEngine.java b/app/src/main/java/com/tangem/wallet/eos/EosEngine.java index 47ee91442f..f667cfd0ef 100644 --- a/app/src/main/java/com/tangem/wallet/eos/EosEngine.java +++ b/app/src/main/java/com/tangem/wallet/eos/EosEngine.java @@ -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; diff --git a/app/src/main/java/com/tangem/wallet/eth/EthEngine.java b/app/src/main/java/com/tangem/wallet/eth/EthEngine.java index 839d066aa1..76e23e4771 100644 --- a/app/src/main/java/com/tangem/wallet/eth/EthEngine.java +++ b/app/src/main/java/com/tangem/wallet/eth/EthEngine.java @@ -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; diff --git a/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java b/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java index a2a6abc339..45e9749077 100644 --- a/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java +++ b/app/src/main/java/com/tangem/wallet/ltc/LtcEngine.java @@ -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 diff --git a/app/src/main/java/com/tangem/wallet/tezos/TezosData.kt b/app/src/main/java/com/tangem/wallet/tezos/TezosData.kt new file mode 100644 index 0000000000..0fe80b1c4a --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/tezos/TezosData.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/tezos/TezosEngine.kt b/app/src/main/java/com/tangem/wallet/tezos/TezosEngine.kt new file mode 100644 index 0000000000..2160bd4b86 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/tezos/TezosEngine.kt @@ -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? { + 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() + + 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 { + val dataForSign = arrayOfNulls(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() { + 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() { + 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() { + 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() { + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/xlm/XlmAssetEngine.java b/app/src/main/java/com/tangem/wallet/xlm/XlmAssetEngine.java index 626b41ff00..394806cda9 100644 --- a/app/src/main/java/com/tangem/wallet/xlm/XlmAssetEngine.java +++ b/app/src/main/java/com/tangem/wallet/xlm/XlmAssetEngine.java @@ -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(); diff --git a/app/src/main/java/com/tangem/wallet/xlm/XlmEngine.java b/app/src/main/java/com/tangem/wallet/xlm/XlmEngine.java index c707517abc..3e2f6c8f77 100644 --- a/app/src/main/java/com/tangem/wallet/xlm/XlmEngine.java +++ b/app/src/main/java/com/tangem/wallet/xlm/XlmEngine.java @@ -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(); diff --git a/app/src/main/java/com/tangem/wallet/xlmTag/XlmTagData.kt b/app/src/main/java/com/tangem/wallet/xlmTag/XlmTagData.kt new file mode 100644 index 0000000000..e8287ccd86 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/xlmTag/XlmTagData.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/xlmTag/XlmTagEngine.kt b/app/src/main/java/com/tangem/wallet/xlmTag/XlmTagEngine.kt new file mode 100644 index 0000000000..a21f69f934 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/xlmTag/XlmTagEngine.kt @@ -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() + + 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 { + 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 { + 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" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java b/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java index 9ae763d5fa..73db5449a5 100644 --- a/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java +++ b/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java @@ -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) { diff --git a/app/src/main/res/layout/fr_verify_card.xml b/app/src/main/res/layout/fr_verify_card.xml index 09cbf321ca..189186e5f5 100644 --- a/app/src/main/res/layout/fr_verify_card.xml +++ b/app/src/main/res/layout/fr_verify_card.xml @@ -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" /> + + + + diff --git a/app/src/main/res/navigation/main_graph.xml b/app/src/main/res/navigation/main_graph.xml index 7409283746..a65f23db22 100644 --- a/app/src/main/res/navigation/main_graph.xml +++ b/app/src/main/res/navigation/main_graph.xml @@ -28,6 +28,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0bd711498a..70870483ed 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -9,9 +9,9 @@ Continuer Le code PIN est vide Blockchain - Réessayez de numériser + Réessayez de scanner la carte Impossible d\'effacer le porte-monnaie avec un solde différent de zéro - Envoyer le paiement + Transférer les fonds Depuis la carte sur la carte avec solde @@ -49,9 +49,9 @@ Oups .. Il semble que votre smartphone ne supporte pas de tels paquets NFC. Essayez d\'envoyer une quantité inférieure ou utilisez un smartphone avec prise en charge complète de NFC. Votre argent est en jeu! - Cette carte a imposé un délai de sécurité - Veuillez tenir la carte fermement \ n jusqu\'à ce que l\'opération soit terminée… - 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. + Cette carte impose un délai de sécurité pour confirmer le transfert des fonds + Veuillez maintenir la carte dans cette position jusqu\'à ce que l\'opération soit terminée… + 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. Gérer le code PIN de l\'utilisateur… @@ -74,12 +74,14 @@ Mettez en contact la carte avec votre smartphone - Scanner une carte avec votre \n %1$s \n comme indiqué ci-dessus + Scanner votre carte avec votre %1$s comme indiqué ci-dessus phone Le porte-monnaie a été effacé Non personnalisé Il existe une nouvelle version de l\'application: %1$s Mettre à jour + Vous n’avez pas de carte?\nVisitez notre boutique sur %1$s + Aucun porte-monnaie compatible installé @@ -146,10 +148,10 @@ L\'authenticité ne peut pas être vérifiée. Balayez vers le bas pour actualiser.      - Maintenant, touchez la carte avec l\'ID + Maintenir la carte avec l\’ID pour créer le porte-monnaie pour effacer le porte-monnaie - pour signer le paiement + pour confirmer le transfert des fonds pour changer les codes PIN / PIN2 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 Impossible d\'effacer le porte-monnaie. Veillez à entrer le code PIN2 correct! @@ -178,7 +180,7 @@ entrez l\'adresse du porte-monnaie entrer le montant - Vérifiez + Vérifier Pas assez de fonds Format de montant inconnu Adresse du porte-monnaie de destination incorrecte @@ -223,12 +225,12 @@ Fabricant Porte-monnaie Identité de la carte - Attested + Attestée Non confirmé Réutilisable None Le dernier! - Unlimited + illimitées Carte ponctuelle Micrologiciel Date d\'enregistrement @@ -238,7 +240,7 @@ Émetteur Clé privée Noeud de validation - Unspents + Transferts Cette carte est protégée par le code PIN1 par défaut Cette carte est protégée par le code PIN1 de l’utilisateur Cette carte est protégée par le code PIN2 par défaut @@ -255,8 +257,9 @@ Bloquable\n Mode de commande atomique\n La liaison au terminal est prise en charge\n - La carte est liée + La carte est associée à ce téléphone + %1$d envoyé(s), %2$d reçu(s) Enregistrer diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ebf79111a2..645ff3bfac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,7 +82,7 @@ There is a new application version: %1$s Update Don\'t have a card?\n Visit our store at %1$s - tangemcards.com + tangemcards.com No compatible wallets installed @@ -262,6 +262,13 @@ Linking to the terminal is supported\n The card is linked To this phone + %1$d unspents (%2$d received) + + + Claim + GENUINE + NOT GENUINE + ALREADY CLAIMED Save diff --git a/app/src/main/res/xml/nfc_tech_filter.xml b/app/src/main/res/xml/nfc_tech_filter.xml index c10d3b1038..e2921c8701 100644 --- a/app/src/main/res/xml/nfc_tech_filter.xml +++ b/app/src/main/res/xml/nfc_tech_filter.xml @@ -3,5 +3,6 @@ android.nfc.tech.IsoDep android.nfc.tech.Ndef + android.nfc.tech.NfcV \ No newline at end of file diff --git a/build.gradle b/build.gradle index e01893d33f..c4edca4c38 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt b/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt index 5521ab0217..3fa43bfe6b 100644 --- a/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt +++ b/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt @@ -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 diff --git a/server-android/src/main/res/drawable/card_bc00.png b/server-android/src/main/res/drawable/card_bc00.png new file mode 100644 index 0000000000..fdf643b4e8 Binary files /dev/null and b/server-android/src/main/res/drawable/card_bc00.png differ diff --git a/server-android/src/main/res/drawable/card_tgslix.png b/server-android/src/main/res/drawable/card_tgslix.png new file mode 100644 index 0000000000..fe2f544598 Binary files /dev/null and b/server-android/src/main/res/drawable/card_tgslix.png differ diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java index 4fd4cc1b0f..8fe19575d6 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/data/TangemCard.java @@ -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); diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/NfcReader.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/NfcReader.java index d93099f6ac..d0ad24b4b5 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/NfcReader.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/NfcReader.java @@ -17,4 +17,5 @@ public interface NfcReader { void connect(); + boolean isConnected(); } \ No newline at end of file diff --git a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLVList.java b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLVList.java index d071484cbd..40c36a777b 100644 --- a/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLVList.java +++ b/tangem-card-old/src/main/java/com/tangem/tangem_card/reader/TLVList.java @@ -69,4 +69,20 @@ public class TLVList extends ArrayList { 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; + } } diff --git a/tangem-core/src/main/java/com/tangem/CardEnvironment.kt b/tangem-core/src/main/java/com/tangem/CardEnvironment.kt index 8c32ac506f..39e077006e 100644 --- a/tangem-core/src/main/java/com/tangem/CardEnvironment.kt +++ b/tangem-core/src/main/java/com/tangem/CardEnvironment.kt @@ -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 { diff --git a/tangem-core/src/main/java/com/tangem/CardManager.kt b/tangem-core/src/main/java/com/tangem/CardManager.kt index 197c82891a..e381f57e3d 100644 --- a/tangem-core/src/main/java/com/tangem/CardManager.kt +++ b/tangem-core/src/main/java/com/tangem/CardManager.kt @@ -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) -> 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) -> 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) } } \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt b/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt new file mode 100644 index 0000000000..833795cbbf --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/CreateWalletCommand.kt @@ -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() { + + 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() + } + } +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/commands/PurgeWalletCommand.kt b/tangem-core/src/main/java/com/tangem/commands/PurgeWalletCommand.kt new file mode 100644 index 0000000000..f8c3feaf7f --- /dev/null +++ b/tangem-core/src/main/java/com/tangem/commands/PurgeWalletCommand.kt @@ -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() { + + 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() + } + } +} \ No newline at end of file diff --git a/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt b/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt index 491509ae08..ca292d37f4 100644 --- a/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt +++ b/tangem-core/src/main/java/com/tangem/common/tlv/TlvTag.kt @@ -52,7 +52,7 @@ enum class TlvTag(val code: Int) { Pause(0x1C), ManufactureId(0x20), - ManufacturerSignature(0x21), + ManufacturerSignature(0x86), IssuerDataPublicKey(0x30), IssuerTransactionPublicKey(0x31), diff --git a/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt b/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt index 018ca60ce5..52b2951309 100644 --- a/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt +++ b/tangem-demo/src/main/java/com/tangem/tangemtest/MainActivity.kt @@ -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 { diff --git a/tangem-demo/src/main/res/layout/activity_main.xml b/tangem-demo/src/main/res/layout/activity_main.xml index a1cecca79b..5759633c21 100644 --- a/tangem-demo/src/main/res/layout/activity_main.xml +++ b/tangem-demo/src/main/res/layout/activity_main.xml @@ -9,12 +9,12 @@ +