Updated on 2026-08-14

This commit is contained in:
Tangem 2020-03-10 18:25:58 +03:00
commit ac4e58aa0c
74 changed files with 961 additions and 1702 deletions

View file

@ -57,12 +57,6 @@ android {
applicationId "com.tangem.wallet"
signingConfig signingConfigs.debug
}
tangemCardano {
versionCode 1
versionName "0.1.0"
applicationId "com.tangem.cardano"
signingConfig signingConfigs.debug
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8

View file

@ -28,7 +28,15 @@ public class Server {
public static final String URL_INFURA = ServerURL.API_INFURA;
public static class Method {
static final String MAIN = URL_INFURA + "v3/613a0b14833145968b1f656240c7d245";
public static final String MAIN = "v3/613a0b14833145968b1f656240c7d245";
}
}
public static class ApiInfuraTestnet {
public static final String URL_INFURA_TESTNET = ServerURL.API_INFURA_TESTNET;
public static class Method {
public static final String MAIN = "v3/613a0b14833145968b1f656240c7d245";
}
}

View file

@ -5,6 +5,7 @@ import android.util.Log;
import androidx.annotation.NonNull;
import com.tangem.App;
import com.tangem.data.Blockchain;
import com.tangem.data.network.model.InfuraBody;
import com.tangem.data.network.model.InfuraResponse;
@ -34,6 +35,16 @@ public class ServerApiInfura {
private int requestsCount=0;
private InfuraApi infuraApi = App.Companion.getNetworkComponent().getRetrofitInfura().create(InfuraApi.class);
public ServerApiInfura() {}
public ServerApiInfura(Blockchain blockchain) {
if (blockchain == Blockchain.EthereumTestNet) {
infuraApi = App.Companion.getNetworkComponent().getRetrofitInfuraTestnet().create(InfuraApi.class);
}
}
public boolean isRequestsSequenceCompleted() {
Log.i(TAG, String.format("isRequestsSequenceCompleted: %s (%d requests left)", String.valueOf(requestsCount <= 0), requestsCount));
return requestsCount <= 0;
@ -53,7 +64,7 @@ public class ServerApiInfura {
public void requestData(String method, int id, String wallet, String contract, String tx) {
requestsCount++;
InfuraApi infuraApi = App.Companion.getNetworkComponent().getRetrofitInfura().create(InfuraApi.class);
InfuraBody infuraBody;
switch (method) {

View file

@ -4,6 +4,7 @@ class ServerURL {
static final String API_TANGEM = "https://verify.tangem.com/";
static final String API_COINMARKETCAP = "https://pro-api.coinmarketcap.com/";
static final String API_INFURA = "https://mainnet.infura.io/";
static final String API_INFURA_TESTNET = "https://rinkeby.infura.io/";
static final String API_SOCHAIN_V2 = "https://chain.so/";
static final String API_ESTIMATEFEE = "https://estimatefee.com/";
static final String API_UPDATE_VERSION = "https://raw.githubusercontent.com/";

View file

@ -14,6 +14,9 @@ interface NetworkComponent {
@get:Named(Server.ApiInfura.URL_INFURA)
val retrofitInfura: Retrofit
@get:Named(Server.ApiInfuraTestnet.URL_INFURA_TESTNET)
val retrofitInfuraTestnet: Retrofit
@get:Named(Server.ApiMaticTesnet.URL_MATIC_TESTNET)
val retrofitMaticTesnet: Retrofit

View file

@ -32,6 +32,18 @@ internal class NetworkModule {
return builder.build()
}
@Singleton
@Provides
@Named(Server.ApiInfuraTestnet.URL_INFURA_TESTNET)
fun provideRetrofitInfuraTestnet(): Retrofit {
val builder = Retrofit.Builder()
.baseUrl(Server.ApiInfuraTestnet.URL_INFURA_TESTNET)
.addConverterFactory(GsonConverterFactory.create())
if (BuildConfig.DEBUG)
builder.client(createOkHttpClient())
return builder.build()
}
@Singleton
@Provides
@Named(Server.ApiRootstock.URL_ROOTSTOCK)

View file

@ -466,7 +466,7 @@ public class EthEngine extends CoinEngine {
@Override
public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) {
final ServerApiInfura serverApiInfura = new ServerApiInfura();
final ServerApiInfura serverApiInfura = new ServerApiInfura(ctx.getBlockchain());
final ServerApiBlockcypher serverApiBlockcypher = new ServerApiBlockcypher();
ServerApiInfura.ResponseListener responseListener = new ServerApiInfura.ResponseListener() {
@ -561,11 +561,21 @@ public class EthEngine extends CoinEngine {
public void onSuccess(String method, BlockcypherFee blockcypherFee) {
Log.e(TAG, "Wrong response type for requestBalanceAndUnspentTransactions");
if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
@Override
public void onFail(String method, String message) {
Log.i(TAG, "onFail: " + method + " " + message);
if (serverApiInfura.isRequestsSequenceCompleted()&& serverApiBlockcypher.isRequestsSequenceCompleted()) {
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
} else {
blockchainRequestsCallbacks.onProgress();
}
}
};
serverApiBlockcypher.setResponseListener(blockcypherListener);
@ -579,7 +589,7 @@ public class EthEngine extends CoinEngine {
@Override
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) {
ServerApiInfura serverApiInfura = new ServerApiInfura();
ServerApiInfura serverApiInfura = new ServerApiInfura(ctx.getBlockchain());
// request requestData eth gasPrice listener
ServerApiInfura.ResponseListener responseListener = new ServerApiInfura.ResponseListener() {
@Override
@ -629,7 +639,7 @@ public class EthEngine extends CoinEngine {
String txStr = String.format("0x%s", BTCUtils.toHex(txForSend));
ServerApiInfura serverApiInfura = new ServerApiInfura();
ServerApiInfura serverApiInfura = new ServerApiInfura(ctx.getBlockchain());
// request requestData eth gasPrice listener
ServerApiInfura.ResponseListener responseListener = new ServerApiInfura.ResponseListener() {
@Override

View file

@ -13,6 +13,7 @@ import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import com.tangem.Constant
import com.tangem.data.Blockchain
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.fragment.qr.CameraPermissionManager
@ -151,6 +152,9 @@ class PrepareTransactionFragment : BaseFragment(), NavigationResultListener, Nfc
// etAmount?.setText(amount)
// rgIncFee.check(R.id.rbFeeOut)
// }
} else if (ctx.blockchain == Blockchain.Ripple && schemeSplit[0] == "ripple") {
val uri = Uri.parse(schemeSplit[1])
etWallet?.setText(uri.path)
} else {
etWallet?.setText(code)
}

View file

@ -1,4 +0,0 @@
package com.tangem;
public class Test1 {
}

View file

@ -1,310 +0,0 @@
package com.tangem.ui
import android.app.Activity
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.text.Html
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.core.os.bundleOf
import com.tangem.Constant
import com.tangem.tangem_sdk.android.data.PINStorage
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
import com.tangem.tangem_card.data.TangemCard
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.event.TransactionFinishWithError
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.fragment.pin.PinRequestFragment
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.UtilHelper
import com.tangem.wallet.CoinEngine
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import kotlinx.android.synthetic.tangemCardano.fragment_confirm_transaction.*
import org.greenrobot.eventbus.EventBus
import java.io.IOException
import java.util.*
class ConfirmTransactionFragment : BaseFragment(), NavigationResultListener, NfcAdapter.ReaderCallback {
override val layoutId = R.layout.fragment_confirm_transaction
private val ctx: TangemContext by lazy { TangemContext() }
private lateinit var amount: CoinEngine.Amount
private var isIncludeFee: Boolean = true
private var requestPIN2Count = 0
private var nodeCheck = true
private var dtVerified: Date? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navigateBackWithResult(Activity.RESULT_CANCELED)
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val engine = CoinEngineFactory.createCardano(ctx)
@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
isIncludeFee = arguments?.getBoolean(Constant.EXTRA_FEE_INCLUDED, true) ?: true
if (isIncludeFee)
tvIncFee.setText(R.string.confirm_transaction_including_fee)
else
tvIncFee.setText(R.string.confirm_transaction_not_including_fee)
amount = CoinEngine.Amount(arguments?.getString(Constant.EXTRA_AMOUNT) ?: "0",
arguments?.getString(Constant.EXTRA_AMOUNT_CURRENCY) ?: "")
if (engine.allowSelectFeeInclusion())
tvIncFee.visibility = View.VISIBLE
else
tvIncFee.visibility = View.INVISIBLE
// if (ctx.card.blockchainID == Blockchain.Token.id) {
// // for Blockchain.Token limit decimals
// etAmount.setText(amount.toValueString(ctx.card.tokensDecimal))
// } else {
// for others
etAmount.setText(amount.toValueString())
// }
tvCurrency.text = engine.balanceCurrency
tvCurrency2.text = engine.feeCurrency
// tvCardID.text = ctx.card.cidDescription
etWallet.setText(arguments?.getString(Constant.EXTRA_TARGET_ADDRESS))
btnSend.visibility = View.INVISIBLE
if (!engine.allowSelectFeeLevel()) {
rgFee.visibility = View.GONE
}
// set listeners
rgFee.setOnCheckedChangeListener { _, checkedId -> doSetFee(checkedId) }
etFee.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
try {
val eqFee = engine.evaluateFeeEquivalent(etFee!!.text.toString())
tvFeeEquivalent.text = eqFee
if (!ctx.coinData!!.amountEquivalentDescriptionAvailable) {
tvFeeEquivalent.error = getString(R.string.confirm_transaction_error_service_unavailable)
tvCurrency2.visibility = View.GONE
tvFeeEquivalent.visibility = View.GONE
} else
tvFeeEquivalent.error = null
} catch (e: Exception) {
e.printStackTrace()
tvFeeEquivalent.text = ""
}
}
override fun afterTextChanged(s: Editable) {
}
})
btnSend.setOnClickListener {
if (UtilHelper.isOnline(requireContext())) {
val calendar = Calendar.getInstance()
calendar.add(Calendar.MINUTE, -1)
if (dtVerified == null || dtVerified!!.before(calendar.time)) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.confirm_transaction_error_data_is_outdated))
return@setOnClickListener
}
val engineCoin = CoinEngineFactory.createCardano(ctx)
if (engineCoin!!.isNeedCheckNode && !nodeCheck) {
Toast.makeText(context, getString(R.string.confirm_transaction_error_cannot_reach_node), Toast.LENGTH_LONG).show()
return@setOnClickListener
}
val txFee = engineCoin.convertToAmount(etFee.text.toString(), tvCurrency2.text.toString())
val txAmount = engineCoin.convertToAmount(etAmount.text.toString(), tvCurrency.text.toString())
// if (!engineCoin.hasBalanceInfo()) {
// finishWithError(Activity.RESULT_CANCELED, getString(R.string.cannot_check_balance_no_connection_with_blockchain_nodes))
// return@setOnClickListener
//
// } else if (!engineCoin.isBalanceNotZero) {
// finishWithError(Activity.RESULT_CANCELED, getString(R.string.the_wallet_is_empty))
// return@setOnClickListener
//
// } else if (!engineCoin.isExtractPossible) {
// finishWithError(Activity.RESULT_CANCELED, getString(R.string.please_wait_for_confirmation_of_incoming_transaction))
// return@setOnClickListener
// }
//
// if (!engineCoin.checkNewTransactionAmountAndFee(txAmount, txFee, isIncludeFee)) {
// finishWithError(Activity.RESULT_CANCELED, getString(R.string.not_enough_funds_on_your_card))
// return@setOnClickListener
// }
requestPIN2Count = 0
// val intent = Intent(baseContext, PinRequestActivity::class.java)
// intent.putExtra(Constant.EXTRA_MODE, PinRequestActivity.Mode.RequestPIN2.toString())
// ctx.saveToIntent(intent)
// intent.putExtra(Constant.EXTRA_FEE_INCLUDED, isIncludeFee)
// startActivityForResult(intent, Constant.REQUEST_CODE_REQUEST_PIN2_)
//TODO - PIN2 workaround
PINStorage.setPIN2(PINStorage.getDefaultPIN2())
val data = Bundle()
ctx.saveToBundle(data)
data.putString(Constant.EXTRA_TARGET_ADDRESS, etWallet!!.text.toString())
data.putString(Constant.EXTRA_AMOUNT, etAmount.text.toString())
data.putString(Constant.EXTRA_AMOUNT_CURRENCY, tvCurrency.text.toString())
data.putString(Constant.EXTRA_FEE, etFee.text.toString())
data.putString(Constant.EXTRA_FEE_CURRENCY, tvCurrency2.text.toString())
data.putBoolean(Constant.EXTRA_FEE_INCLUDED, isIncludeFee)
navigateForResult(
Constant.REQUEST_CODE_SIGN_TRANSACTION,
R.id.action_confirmTransactionFragment_to_signTransactionFragment,
data)
} else
Toast.makeText(context, getString(R.string.general_error_no_connection), Toast.LENGTH_SHORT).show()
}
progressBar.visibility = View.VISIBLE
if (!navigatedBack) requestFee(engine)
}
private fun requestFee(engine: CoinEngine) {
engine.requestFee(
object : CoinEngine.BlockchainRequestsCallbacks {
override fun onComplete(success: Boolean) {
if (success) {
onProgress()
progressBar.visibility = View.INVISIBLE
dtVerified = Date()
} else {
finishWithError(Activity.RESULT_CANCELED, ctx.error)
}
}
override fun onProgress() {
doSetFee(rgFee.checkedRadioButtonId)
}
override fun allowAdvance(): Boolean {
return UtilHelper.isOnline(requireContext())
}
},
etWallet.text.toString(),
amount)
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
if (requestCode == Constant.REQUEST_CODE_SIGN_TRANSACTION) {
if (data != null) {
if (data.containsKey(EXTRA_TANGEM_CARD_UID) && data.containsKey(EXTRA_TANGEM_CARD)) {
val updatedCard = TangemCard(data.getString(EXTRA_TANGEM_CARD_UID))
updatedCard.loadFromBundle(data.getBundle(EXTRA_TANGEM_CARD)!!)
ctx.card = updatedCard
}
}
if (resultCode == Constant.RESULT_INVALID_PIN_ && requestPIN2Count < 2) {
requestPIN2Count++
val bundle = Bundle()
bundle.putString(Constant.EXTRA_MODE, PinRequestFragment.Mode.RequestPIN2.toString())
ctx.saveToBundle(bundle)
bundle.putBoolean(Constant.EXTRA_FEE_INCLUDED, isIncludeFee)
navigateForResult(Constant.REQUEST_CODE_REQUEST_PIN2_,
R.id.action_confirmTransactionFragment_to_pinRequestFragment,
bundle)
return
}
navigateBackWithResult(resultCode, data)
} else if (requestCode == Constant.REQUEST_CODE_REQUEST_PIN2_) {
if (resultCode == Activity.RESULT_OK) {
val bundle = Bundle()
ctx.saveToBundle(bundle)
bundle.putString(Constant.EXTRA_TARGET_ADDRESS, etWallet!!.text.toString())
bundle.putString(Constant.EXTRA_AMOUNT, etAmount.text.toString())
bundle.putString(Constant.EXTRA_AMOUNT_CURRENCY, tvCurrency.text.toString())
bundle.putString(Constant.EXTRA_FEE, etFee.text.toString())
bundle.putString(Constant.EXTRA_FEE_CURRENCY, tvCurrency2.text.toString())
bundle.putBoolean(Constant.EXTRA_FEE_INCLUDED, isIncludeFee)
navigateForResult(Constant.REQUEST_CODE_SIGN_TRANSACTION,
R.id.action_confirmTransactionFragment_to_signTransactionFragment,
bundle)
} else
Toast.makeText(context, R.string.confirm_transaction_error_pin_2_is_required, Toast.LENGTH_LONG).show()
}
}
override fun onTagDiscovered(tag: Tag) {
try {
(activity as MainActivity).nfcManager.ignoreTag(tag)
} catch (e: IOException) {
e.printStackTrace()
}
}
private fun doSetFee(checkedRadioButtonId: Int) {
var txtFee = ""
when (checkedRadioButtonId) {
R.id.rbMinimalFee ->
if (ctx.coinData.minFee != null) {
txtFee = ctx.coinData.minFee!!.toValueString()
btnSend.visibility = View.VISIBLE
} else
btnSend.visibility = View.INVISIBLE
R.id.rbNormalFee ->
if (ctx.coinData.normalFee != null) {
txtFee = ctx.coinData.normalFee!!.toValueString()
btnSend.visibility = View.VISIBLE
} else
btnSend.visibility = View.INVISIBLE
R.id.rbMaximumFee ->
if (ctx.coinData.maxFee != null) {
txtFee = ctx.coinData.maxFee!!.toValueString()
btnSend.visibility = View.VISIBLE
} else
btnSend.visibility = View.INVISIBLE
}
etFee.setText(txtFee.replace(',', '.'))
}
private fun finishWithError(errorCode: Int, message: String) {
val transactionFinishWithError = TransactionFinishWithError()
transactionFinishWithError.message = message
EventBus.getDefault().post(transactionFinishWithError)
navigateBackWithResult(errorCode, bundleOf(Constant.EXTRA_MESSAGE to message))
}
}

View file

@ -1,207 +0,0 @@
package com.tangem.ui
import android.app.Activity
import android.content.Context
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Build
import android.os.Bundle
import android.text.Html
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import com.tangem.Constant
import com.tangem.data.dp.PrefsManager
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.event.TransactionFinishWithError
import com.tangem.ui.event.TransactionFinishWithSuccess
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.UtilHelper
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import kotlinx.android.synthetic.tangemCardano.fragment_prepare_transaction.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import java.io.IOException
class PrepareTransactionFragment : BaseFragment(), NavigationResultListener, NfcAdapter.ReaderCallback {
override val layoutId = R.layout.fragment_prepare_transaction
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val engine = CoinEngineFactory.createCardano(TangemContext())
@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
if (!engine!!.allowSelectFeeInclusion()) {
rgIncFee.visibility = View.INVISIBLE
} else {
rgIncFee.visibility = View.VISIBLE
}
tvCurrency.text = engine.balance?.currency
etAmount.setText(engine.balance?.toValueString())
// limit number of symbols after comma
etAmount.filters = engine.amountInputFilters
etWallet.setText(PrefsManager.getInstance().lastWalletAddress)
// set listeners
etAmount.setOnEditorActionListener { lv, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE) {
val imm = lv.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(lv.windowToken, 0)
lv.clearFocus()
true
} else {
false
}
}
btnVerify.setOnClickListener {
if (!UtilHelper.isOnline(requireContext())) {
Toast.makeText(context, R.string.general_error_no_connection, Toast.LENGTH_LONG).show()
return@setOnClickListener
}
// check on empty wallet address
if (etWallet.text.toString() == "") {
etWallet.error = getString(R.string.general_wallet_empty)
return@setOnClickListener
}
// check on empty amount
if (etAmount.text.toString() == "") {
etAmount.error = getString(R.string.prepare_transaction_error_amount_empty)
return@setOnClickListener
}
val strAmount: String = etAmount.text.toString().replace(",", ".")
val amount = engine.convertToAmount(etAmount.text.toString(), tvCurrency.text.toString())
// check amount
try {
if (!engine.checkNewTransactionAmount(amount))
etAmount.error = getString(R.string.prepare_transaction_error_not_enough_funds)
else
etAmount.error = null
} catch (e: Exception) {
etAmount.error = getString(R.string.prepare_transaction_error_unknown_amount_format)
}
// check wallet address
if (!engine.validateAddress(etWallet.text.toString())) {
etWallet.error = getString(R.string.prepare_transaction_error_incorrect_destination)
return@setOnClickListener
} else
etWallet.error = null
if (!etAmount.error.isNullOrEmpty() || !etWallet.error.isNullOrEmpty()) {
return@setOnClickListener
}
PrefsManager.getInstance().saveLastWalletAddress(etWallet.text.toString())
val data = Bundle()
data.putString(Constant.EXTRA_TARGET_ADDRESS, etWallet!!.text.toString())
data.putBoolean(Constant.EXTRA_FEE_INCLUDED, (rgIncFee!!.checkedRadioButtonId == R.id.rbFeeIn))
data.putString(Constant.EXTRA_AMOUNT, strAmount)
data.putString(Constant.EXTRA_AMOUNT_CURRENCY, tvCurrency.text.toString())
navigateForResult(
Constant.REQUEST_CODE_SEND_TRANSACTION__,
R.id.action_prepareTransactionFragment_to_confirmTransactionFragment,
data)
}
ivCamera.setOnClickListener {
etWallet.text.clear()
navigateForResult(Constant.REQUEST_CODE_SCAN_QR, R.id.action_prepareTransactionFragment_to_qrScanFragment)
}
etWallet.setOnTouchListener(OnTouchListener { _, event ->
if (event.action == MotionEvent.ACTION_UP) {
if (event.rawX >= etWallet.right - etWallet.compoundDrawables[2].bounds.width()) {
etWallet.text.clear()
return@OnTouchListener true
}
}
false
})
}
override fun onStart() {
super.onStart()
if (!EventBus.getDefault().isRegistered(this))
EventBus.getDefault().register(this)
}
override fun onDestroy() {
EventBus.getDefault().unregister(this)
super.onDestroy()
}
@Subscribe
fun onTransactionFinishWithSuccess(transactionFinishWithSuccess: TransactionFinishWithSuccess) {
transactionFinishWithSuccess.message?.let { message ->
(activity as MainActivity).toastHelper.showSnackbarSuccess(requireContext(), cl, message)
}
}
@Subscribe
fun onTransactionFinishWithError(transactionFinishWithError: TransactionFinishWithError) {
transactionFinishWithError.message?.let { message ->
(activity as MainActivity).toastHelper.showSnackbarError(requireContext(), cl, message)
}
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
if (requestCode == Constant.REQUEST_CODE_SCAN_QR && resultCode == Activity.RESULT_OK && data != null && data.containsKey("QRCode")) {
val code = data.getString("QRCode")
// when (ctx.blockchain) {
// Blockchain.Bitcoin -> {
// if (code.contains("bitcoin:")) {
// val tmp = code.split("bitcoin:".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
// code = tmp[1]
// }
// }
// Blockchain.Ethereum, Blockchain.Token -> {
// if (code.contains("ethereum:")) {
// val tmp = code.split("ethereum:".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
// code = tmp[1]
// } else if (code.contains("blockchain:")) {
// val tmp = code.split("blockchain:".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
// code = tmp[1]
// }
// }
// else -> {
// }
// }
etWallet?.setText(code)
// } else if (requestCode == Constant.REQUEST_CODE_SEND_TRANSACTION__) {
// setResult(resultCode, data)
// finish()
}
}
override fun onTagDiscovered(tag: Tag) {
try {
(activity as MainActivity).nfcManager.ignoreTag(tag)
} catch (e: IOException) {
e.printStackTrace()
}
}
}

View file

@ -1,362 +0,0 @@
package com.tangem.ui
import android.app.Activity
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.media.MediaPlayer
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.IsoDep
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import com.tangem.App
import com.tangem.Constant
import com.tangem.tangem_sdk.android.nfc.NfcDeviceAntennaLocation
import com.tangem.tangem_sdk.android.reader.NfcManager
import com.tangem.tangem_sdk.android.reader.NfcReader
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.asBundle
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_card.reader.CardCrypto
import com.tangem.tangem_card.reader.CardProtocol
import com.tangem.tangem_card.tasks.CustomReadCardTask
import com.tangem.tangem_card.tasks.OneTouchSignTask
import com.tangem.tangem_card.tasks.SignTask
import com.tangem.tangem_card.util.Log
import com.tangem.wallet.CoinEngine
import com.tangem.wallet.CoinEngineFactory
import com.tangem.wallet.TangemContext
import com.tangem.ui.activity.MainActivity
import com.tangem.ui.dialog.NoExtendedLengthSupportDialog
import com.tangem.ui.dialog.WaitSecurityDelayDialog
import com.tangem.ui.fragment.BaseFragment
import com.tangem.ui.navigation.NavigationResultListener
import com.tangem.util.LOG
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.layout_progress_horizontal.*
import kotlinx.android.synthetic.main.layout_touch_card.*
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class SignTransactionFragment : BaseFragment(), NavigationResultListener,
NfcAdapter.ReaderCallback, CardProtocol.Notifications {
companion object {
val TAG: String = SignTransactionFragment::class.java.simpleName
}
override val layoutId = R.layout.fragment_sign_transaction
private lateinit var ctx: TangemContext
private lateinit var mpFinishSignSound: MediaPlayer
private lateinit var nfcDeviceAntenna: NfcDeviceAntennaLocation
private var task: CustomReadCardTask? = null
private lateinit var amount: CoinEngine.Amount
private lateinit var fee: CoinEngine.Amount
private var isIncludeFee = true
private var outAddressStr: String? = null
private var lastReadSuccess = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ctx = TangemContext.loadFromBundle(context, arguments)
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navigateBackWithResult(Activity.RESULT_CANCELED)
}
}
requireActivity().onBackPressedDispatcher.addCallback(this, callback)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mpFinishSignSound = MediaPlayer.create(context, R.raw.scan_card_sound)
// init NFC Antenna
nfcDeviceAntenna = NfcDeviceAntennaLocation(requireContext(), ivHandCardHorizontal, ivHandCardVertical, llHand, llNfc)
nfcDeviceAntenna.init()
amount = CoinEngine.Amount(arguments?.getString(Constant.EXTRA_AMOUNT), arguments?.getString(Constant.EXTRA_AMOUNT_CURRENCY))
fee = CoinEngine.Amount(arguments?.getString(Constant.EXTRA_FEE), arguments?.getString(Constant.EXTRA_FEE_CURRENCY))
isIncludeFee = arguments?.getBoolean(Constant.EXTRA_FEE_INCLUDED, true) ?: true
outAddressStr = arguments?.getString(Constant.EXTRA_TARGET_ADDRESS)
// tvCardID.text = ctx.card!!.cidDescription
progressBar.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar.visibility = View.INVISIBLE
}
override fun onPause() {
task?.cancel(true)
super.onPause()
}
override fun onStop() {
task?.cancel(true)
super.onStop()
}
override fun onNavigationResult(requestCode: String, resultCode: Int, data: Bundle?) {
if (requestCode == Constant.REQUEST_CODE_SEND_TRANSACTION_) {
navigateBackWithResult(resultCode, data)
}
}
override fun onTagDiscovered(tag: Tag) {
try {
// get IsoDep handle and run cardReader thread
val isoDep = IsoDep.get(tag)
isoDep.timeout = 65000
val coinEngine = CoinEngineFactory.createCardano(ctx)
coinEngine?.setOnNeedSendTransaction { tx ->
if (tx != null) {
val data = Bundle()
ctx.saveToBundle(data)
data.putByteArray(Constant.EXTRA_TX, tx)
navigateForResult(
Constant.REQUEST_CODE_SEND_TRANSACTION_,
R.id.action_signTransactionFragment_to_sendTransactionFragment,
data)
}
}
val tx: OneTouchSignTask.TransactionToSign = object : OneTouchSignTask.TransactionToSign {
var txToSign: SignTask.TransactionToSign? = null
suspend fun requestBalanceAndUnspentTransactions(): Boolean = suspendCoroutine { cont ->
coinEngine!!.requestBalanceAndUnspentTransactions(object : CoinEngine.BlockchainRequestsCallbacks {
override fun onComplete(success: Boolean?) {
cont.resume(success!!)
}
override fun onProgress() {
}
override fun allowAdvance(): Boolean {
return true
}
})
}
fun initData(card: TangemCard) {
if (ctx.card == null) {
ctx.card = card
}
if (ctx.coinData == null) {
ctx.coinData = CoinEngineFactory.createCardanoData()
}
if (ctx.coinData.wallet.isNullOrEmpty()) {
coinEngine?.defineWallet()
runBlocking { requestBalanceAndUnspentTransactions() }
Log.e(MainActivity.TAG, "requestBalanceAndUnspentTransactions completed")
}
if (txToSign == null) txToSign = coinEngine!!.constructTransaction(amount, fee, isIncludeFee, outAddressStr)
}
override fun isSigningOnCardSupported(card: TangemCard?): Boolean {
initData(card!!)
return CoinEngineFactory.isCardano(card?.blockchainID) and (txToSign!!.isSigningMethodSupported(card?.signingMethod))
}
override fun isIssuerCanSignData(card: TangemCard?): Boolean {
return try {
card?.issuer?.privateDataKey!=null
} catch (e: java.lang.Exception) {
false
}
}
override fun isIssuerCanSignTransaction(card: TangemCard?): Boolean {
return try {
card?.issuer?.privateTransactionKey!=null
} catch (e: java.lang.Exception) {
false
}
}
override fun getHashesToSign(card: TangemCard?): Array<ByteArray> {
initData(card!!)
return txToSign!!.hashesToSign
}
override fun getRawDataToSign(card: TangemCard?): ByteArray {
initData(card!!)
return txToSign!!.rawDataToSign
}
override fun getHashAlgToSign(card: TangemCard?): String {
initData(card!!)
return txToSign!!.hashAlgToSign
}
override fun getIssuerTransactionSignature(card: TangemCard?, dataToSignByIssuer: ByteArray?): ByteArray {
initData(card!!)
if( card.issuer!=null && card.issuer.privateTransactionKey!=null)
{
return CardCrypto.Signature(card.issuer.privateTransactionKey, dataToSignByIssuer)
}
return txToSign!!.getIssuerTransactionSignature(dataToSignByIssuer)
}
override fun onSignCompleted(card: TangemCard?, signature: ByteArray?) {
initData(card!!)
txToSign!!.onSignCompleted(signature)
}
}
task = OneTouchSignTask(NfcReader((activity as MainActivity).nfcManager, isoDep),
App.localStorage, App.pinStorage, this, tx)
task!!.start()
} catch (e: CardProtocol.TangemException_WrongAmount) {
try {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_wrong_amount))
data.putString(EXTRA_TANGEM_CARD_UID, ctx.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, ctx.card.asBundle)
navigateBackWithResult(Activity.RESULT_CANCELED, data)
} catch (e: Exception) {
e.printStackTrace()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onReadStart(cardProtocol: CardProtocol) {
rlProgressBar.post { rlProgressBar.visibility = View.VISIBLE }
progressBar?.post {
progressBar?.visibility = View.VISIBLE
progressBar?.progress = 5
}
}
override fun onReadProgress(protocol: CardProtocol, progress: Int) {
progressBar?.post { progressBar!!.progress = progress }
}
override fun onReadFinish(cardProtocol: CardProtocol?) {
task = null
if (cardProtocol != null) {
if (cardProtocol.error == null) {
rlProgressBar.post { rlProgressBar.visibility = View.GONE }
progressBar?.post {
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.GREEN)
}
mpFinishSignSound.start()
} else {
lastReadSuccess = false
if (cardProtocol.error.javaClass == CardProtocol.TangemException_InvalidPIN::class.java) {
progressBar?.post {
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
}
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_cannot_sign))
data.putString(EXTRA_TANGEM_CARD_UID, cardProtocol.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, cardProtocol.card.asBundle)
navigateBackWithResult(Constant.RESULT_INVALID_PIN_, data)
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
} else {
if (cardProtocol.error is CardProtocol.TangemException_WrongAmount) {
try {
val data = Bundle()
data.putString(Constant.EXTRA_MESSAGE, getString(R.string.send_transaction_error_wrong_amount))
data.putString(EXTRA_TANGEM_CARD_UID, cardProtocol.card.uid)
data.putBundle(EXTRA_TANGEM_CARD, cardProtocol.card.asBundle)
navigateBackWithResult(Activity.RESULT_CANCELED, data)
} catch (e: Exception) {
e.printStackTrace()
}
}
progressBar?.post {
if (cardProtocol.error is CardProtocol.TangemException_ExtendedLengthNotSupported) {
if (!NoExtendedLengthSupportDialog.allReadyShowed) {
NoExtendedLengthSupportDialog.message = getText(R.string.dialog_the_nfc_adapter_length_apdu).toString() + "\n" + getText(R.string.dialog_the_nfc_adapter_length_apdu_advice).toString()
NoExtendedLengthSupportDialog().show(requireFragmentManager(), NoExtendedLengthSupportDialog.TAG)
}
} else {
Toast.makeText(context, R.string.general_notification_scan_again, Toast.LENGTH_LONG).show()
}
progressBar?.progress = 100
progressBar?.progressTintList = ColorStateList.valueOf(Color.RED)
}
}
}
}
rlProgressBar.postDelayed({
try {
rlProgressBar.visibility = View.GONE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadCancel() {
task = null
progressBar?.postDelayed({
try {
progressBar?.progress = 0
progressBar?.progressTintList = ColorStateList.valueOf(Color.DKGRAY)
progressBar?.visibility = View.INVISIBLE
} catch (e: Exception) {
e.printStackTrace()
}
}, 500)
}
override fun onReadBeforeRequest(timeout: Int) {
LOG.i(TAG, "onReadBeforeRequest timeout $timeout")
WaitSecurityDelayDialog.onReadBeforeRequest(activity, timeout)
}
override fun onReadAfterRequest() {
LOG.i(TAG, "onReadAfterRequest")
WaitSecurityDelayDialog.onReadAfterRequest(activity)
}
override fun onReadWait(msec: Int) {
LOG.i(TAG, "onReadWait msec $msec")
WaitSecurityDelayDialog.onReadWait(activity, msec)
}
}

View file

@ -1,344 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="Autofill">
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:paddingTop="8dp"
android:paddingBottom="4dp"
android:text="@string/general_send_transaction"
android:textAlignment="center"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_large"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/llTransaction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/btn_light"
android:orientation="vertical"
android:paddingStart="5dp"
android:paddingTop="8dp"
android:paddingEnd="5dp"
android:paddingBottom="8dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="@string/general_from_banknote"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
android:visibility="gone" />
<TextView
android:id="@+id/tvCardID"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:fontFamily="@font/maax"
android:textColor="@color/black"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
android:visibility="gone"
tools:text="BB00 0000 1210 0233" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:fontFamily="@font/maax"
android:text="@string/general_balance"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
android:visibility="gone" />
<TextView
android:id="@+id/tvBalance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:fontFamily="@font/maax"
android:textColor="@color/black"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
android:visibility="gone"
tools:text="4.51735 BTC" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:fontFamily="@font/maax"
android:text="@string/general_send_to_wallet"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<EditText
android:id="@+id/etWallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:enabled="false"
android:fontFamily="@font/maax"
android:hint="@string/confirm_transaction_hint_target_address"
android:inputType="textMultiLine"
android:maxLines="2"
android:padding="12dp"
android:singleLine="false"
android:textColor="@color/black"
android:textSize="@dimen/text_size_1_small"
android:textStyle="bold" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="@font/maax"
android:text="@string/general_amount"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvIncFee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginBottom="1dp"
android:fontFamily="@font/maax"
android:textColor="@color/primary"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@+id/textView3"
app:layout_constraintLeft_toRightOf="@+id/textView3"
tools:text="(including fee)" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/etAmount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:digits="0123456789.,"
android:fontFamily="@font/maax"
android:hint="@string/prepare_transaction_hint_enter_amount"
android:imeOptions="actionDone"
android:inputType="numberDecimal"
android:padding="12dp"
android:textSize="@dimen/text_size_1_large"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tvCurrency"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="150dp"
tools:text="1.34343434343443434343434" />
<TextView
android:id="@+id/tvCurrency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:fontFamily="@font/maax"
android:text="@string/general_btc"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_1_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/etAmount"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/llFee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/llTransaction">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="4dp"
android:fontFamily="@font/maax"
android:text="@string/confirm_transaction_fee"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<info.hoang8f.android.segmented.SegmentedGroup
android:id="@+id/rgFee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:checkedButton="@+id/rbNormalFee"
android:gravity="center"
android:orientation="horizontal"
app:sc_border_width="2dp"
app:sc_corner_radius="10dp"
app:sc_tint_color="@color/colorPrimary">
<RadioButton
android:id="@+id/rbMinimalFee"
style="@style/RadioButton"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_fee_minimal"
android:textSize="@dimen/text_size_medium" />
<RadioButton
android:id="@+id/rbNormalFee"
style="@style/RadioButton"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_fee_normal"
android:textSize="@dimen/text_size_medium" />
<RadioButton
android:id="@+id/rbMaximumFee"
style="@style/RadioButton"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_fee_priority"
android:textSize="@dimen/text_size_medium" />
</info.hoang8f.android.segmented.SegmentedGroup>
</LinearLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/etFee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:enabled="false"
android:fontFamily="@font/maax"
android:hint="@string/confirm_transaction_hint_fee_amount"
android:inputType="numberDecimal"
android:padding="12dp"
android:textColor="@color/black"
android:textColorLink="@android:color/holo_blue_dark"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="100dp" />
<TextView
android:id="@+id/tvCurrency2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:fontFamily="@font/maax"
android:text="@string/general_btc"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/etFee"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tvFeeEquivalent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:textColor="@android:color/darker_gray"
android:textSize="@dimen/text_size_1_small"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@+id/tvCurrency2"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
<Button
android:id="@+id/btnSend"
style="@style/AppTheme.RoundedCornerMaterialButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="16dp"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/send_1"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<ProgressBar
android:id="@+id/progressBar"
style="@android:style/Widget.Holo.Light.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="30dp"
android:layout_marginTop="4dp"
android:elevation="1dp"
android:indeterminate="true"
android:indeterminateTint="@color/colorPrimary"
android:visibility="invisible"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@+id/llFee" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,267 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/cl"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="contentDescription">
<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="@string/general_send_transaction"
android:textAlignment="center"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_large"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:id="@+id/llFrom"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/btn_light"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingTop="24dp"
android:paddingEnd="8dp"
android:paddingBottom="24dp"
android:visibility="gone"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_marginTop="8dp"
android:fontFamily="@font/maax"
android:text="@string/general_from_banknote"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium" />
<TextView
android:id="@+id/tvCardID"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:fontFamily="@font/maax"
android:textColor="@color/black"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
tools:text="BB00 0000 1210 0233" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_marginTop="12dp"
android:fontFamily="@font/maax"
android:text="@string/general_balance"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tvBalance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="2dp"
android:fontFamily="@font/maax"
android:textColor="@color/black"
android:textSize="@dimen/text_size_medium"
android:textStyle="bold"
tools:text="4.51735 Btc" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/llTo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:backgroundTint="@color/card_state_loaded_with_zero"
android:orientation="vertical"
android:paddingTop="32dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/llFrom">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:fontFamily="@font/maax"
android:text="@string/general_send_to_wallet"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/ivCamera"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="12dp"
android:src="@drawable/qr_scan"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
<EditText
android:id="@+id/etWallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:drawableEnd="@android:drawable/ic_menu_close_clear_cancel"
android:fontFamily="@font/maax"
android:hint="@string/prepare_transaction_hint_enter_address"
android:imeOptions="actionNext"
android:inputType="text|textMultiLine|textNoSuggestions"
android:padding="12dp"
android:singleLine="false"
android:textColor="@color/colorPrimaryDark"
android:textColorLink="@android:color/holo_blue_dark"
android:textSize="@dimen/text_size_medium"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/ivCamera"
tools:layout_editor_absoluteY="2dp" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="7dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="8dp"
android:fontFamily="@font/maax"
android:text="@string/general_amount"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_medium"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp">
<EditText
android:id="@+id/etAmount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:digits="0123456789.,"
android:fontFamily="@font/maax"
android:hint="@string/prepare_transaction_hint_enter_amount"
android:imeOptions="actionDone"
android:inputType="numberDecimal"
android:padding="12dp"
android:textColor="@color/colorPrimaryDark"
android:textSize="@dimen/text_size_1_large"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/tvCurrency"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_min="150dp" />
<TextView
android:id="@+id/tvCurrency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:fontFamily="@font/maax"
android:text="@string/general_btc"
android:textColor="@color/primary"
android:textSize="@dimen/text_size_1_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/etAmount"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<info.hoang8f.android.segmented.SegmentedGroup
android:id="@+id/rgIncFee"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:checkedButton="@+id/rbFeeIn"
android:gravity="center"
android:orientation="horizontal"
app:sc_border_width="2dp"
app:sc_corner_radius="10dp"
app:sc_tint_color="@color/colorPrimary"
tools:layout_editor_absoluteX="10dp">
<RadioButton
android:id="@+id/rbFeeIn"
style="@style/RadioButton"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_including_fee"
android:textSize="@dimen/text_size_medium" />
<RadioButton
android:id="@+id/rbFeeOut"
style="@style/RadioButton"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/confirm_transaction_btn_not_including_fee"
android:textSize="@dimen/text_size_medium" />
</info.hoang8f.android.segmented.SegmentedGroup>
</LinearLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnVerify"
style="@style/AppTheme.RoundedCornerMaterialButton"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="16dp"
android:fontFamily="@font/saira_condensed_bold"
android:text="@string/verify"
android:textSize="@dimen/text_size_large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,62 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
layout="@layout/layout_touch_card"
android:layout_width="400dp"
android:layout_height="250dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.75">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="@string/now_touch_the_banknote_with_id"
android:textAlignment="center"
android:textSize="@dimen/text_size_medium" />
<TextView
android:id="@+id/tvCardID"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:fontFamily="@font/maax"
android:textAlignment="center"
android:textColor="@color/black"
android:textSize="@dimen/text_size_large"
android:visibility="gone"
tools:text="CB02 0000 0002 5000" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/maax"
android:text="@string/to_sign_the_transaction"
android:textAlignment="center"
android:textSize="@dimen/text_size_medium" />
</LinearLayout>
<include layout="@layout/layout_progress_horizontal" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">Cardano</string>
<string name="verify">Confirm</string>
<string name="send_1">Checkout</string>
<string name="now_touch_the_banknote_with_id">Now touch the card</string>
</resources>

1
blockchain-demo/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,63 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "com.example.blockchain_demo"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
packagingOptions {
exclude 'lib/x86_64/darwin/libscrypt.dylib'
exclude 'lib/x86_64/freebsd/libscrypt.so'
exclude 'lib/x86_64/linux/libscrypt.so'
}
viewBinding {
enabled = true
}
}
dependencies {
implementation project(':tangem-core')
implementation project(':tangem-sdk')
implementation project(':blockchain')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.material:material:1.1.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

21
blockchain-demo/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,22 @@
package com.tangem.blockchain_demo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.blockchain_demo", appContext.packageName)
}
}

View file

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tangem.blockchain_demo">
<uses-feature
android:name="android.hardware.nfc"
android:required="true" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name="com.tangem.blockchain_demo.BlockchainDemoActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="www.tangem.com"
android:scheme="http" />
<data
android:host="www.tangem.com"
android:scheme="https" />
<data
android:host="tangem.com"
android:scheme="http" />
<data
android:host="tangem.com"
android:scheme="https" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TECH_DISCOVERED" />
</intent-filter>
<meta-data
android:name="android.nfc.action.TECH_DISCOVERED"
android:resource="@xml/nfc_tech_filter" />
</activity>
</application>
</manifest>

View file

@ -0,0 +1,188 @@
package com.tangem.blockchain_demo
import android.os.Bundle
import android.text.Editable
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.tangem.CardManager
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.Signer
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain_demo.databinding.ActivityBlockchainDemoBinding
import com.tangem.commands.Card
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tasks.ScanEvent
import com.tangem.tasks.TaskError
import com.tangem.tasks.TaskEvent
import kotlinx.coroutines.*
import java.math.BigDecimal
import kotlin.coroutines.CoroutineContext
class BlockchainDemoActivity : AppCompatActivity() {
private lateinit var cardManager: CardManager
private lateinit var signer: TransactionSigner
private lateinit var card: Card
private lateinit var walletManager: WalletManager
private var issuerDataCounter: Int = 1
private lateinit var fee: BigDecimal
private lateinit var binding: ActivityBlockchainDemoBinding
private val parentJob = Job()
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
handleError(throwable.localizedMessage)
}
private val coroutineContext: CoroutineContext
get() = parentJob + Dispatchers.IO + exceptionHandler
private val scope = CoroutineScope(coroutineContext)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityBlockchainDemoBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
cardManager = CardManager.init(this)
signer = Signer(cardManager)
binding.btnScan.setOnClickListener { scan() }
binding.btnCheckFee.setOnClickListener { requestFee() }
binding.btnSend.setOnClickListener { send() }
}
private fun scan() {
cardManager.scanCard { taskEvent ->
when (taskEvent) {
is TaskEvent.Event -> {
when (taskEvent.data) {
is ScanEvent.OnReadEvent -> {
card = (taskEvent.data as ScanEvent.OnReadEvent).card
walletManager = WalletManagerFactory.makeWalletManager(card)!!
getInfo()
}
}
}
is TaskEvent.Completion -> {
if (taskEvent.error != null) {
if (taskEvent.error !is TaskError.UserCancelled) {
handleError(taskEvent.error.toString())
}
}
}
}
}
}
private fun getInfo() {
scope.launch {
walletManager.update()
withContext(Dispatchers.Main) {
binding.tvBalance.text =
"${walletManager.wallet.balances[AmountType.Coin]?.value
?: "error"} ${walletManager.blockchain.currency}"
val token = walletManager.wallet.balances[AmountType.Token]
if (token != null) {
binding.tvBalance.text = token.currencySymbol + " " + token.value
binding.etSumToSend.text = Editable.Factory.getInstance().newEditable(
token.value.toString()
)
} else {
binding.etSumToSend.text =
Editable.Factory.getInstance().newEditable(
walletManager.wallet.balances[AmountType.Coin]?.value.toString()
)
}
binding.btnCheckFee.isEnabled = true
}
}
}
private fun requestFee() {
if (binding.etReceiverAddress.text.isBlank()) {
Toast.makeText(this, "Please enter receiver address", Toast.LENGTH_LONG).show()
return
} else if (binding.etSumToSend.text.isBlank()) {
Toast.makeText(this, "Choose sum to send", Toast.LENGTH_LONG).show()
return
}
scope.launch {
val feeResult = (walletManager as FeeProvider).getFee(
walletManager.wallet.balances[AmountType.Token]
?: walletManager.wallet.balances[AmountType.Coin]!!,
binding.etReceiverAddress.text.toString())
withContext(Dispatchers.Main) {
when (feeResult) {
is Result.Failure -> {
handleError(feeResult.error?.localizedMessage ?: "Error")
}
is Result.Success -> {
binding.btnSend.isEnabled = true
val fees = feeResult.data
if (fees.size == 1) {
binding.tvFee.text = fees[0].value.toString()
fee = fees[0].value ?: BigDecimal(0)
} else {
binding.tvFee.text = fees[0].value.toString() + "\n" +
fees[1].value.toString() + "\n" +
fees[2].value.toString()
fee = fees[1].value ?: BigDecimal(0)
}
}
}
}
}
}
private fun send() {
scope.launch {
val result = (walletManager as TransactionSender).send(
formTransactionData(),
signer)
withContext(Dispatchers.Main) {
when (result) {
is SimpleResult.Failure -> {
handleError(result.error?.localizedMessage ?: "Error")
}
is SimpleResult.Success -> {
binding.tvFee.text = "Success"
}
}
}
}
}
private fun formTransactionData(): TransactionData {
val amount = if (walletManager.wallet.balances[AmountType.Token] != null) {
walletManager.wallet.balances[AmountType.Token]!!
} else {
walletManager.wallet.balances[AmountType.Coin]!!.copy(
value = binding.etSumToSend.text.toString().toBigDecimal() - fee
)
}
return TransactionData(
amount,
walletManager.wallet.balances[AmountType.Coin]!!.copy(value = fee),
walletManager.wallet.balances[AmountType.Coin]!!.address!!,
binding.etReceiverAddress.text.toString()
)
}
private fun handleError(error: String?) {
Toast.makeText(this, error, Toast.LENGTH_LONG).show()
}
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
}

View file

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View file

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#008577"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View file

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BlockchainDemoActivity">
<TextView
android:id="@+id/tv_balance"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:layout_marginBottom="80dp"
android:layout_weight="3"
android:padding="16dp"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
android:textColor="@android:color/black"
android:textSize="26sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="BTC 0.500000034" />
<TextView
android:id="@+id/tv_fee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="48dp"
android:padding="16dp"
android:paddingBottom="48dp"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintBottom_toTopOf="@id/et_receiver_address"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
tools:text="MinFee: 200, Fee: 300, Priority: 400" />
<EditText
android:id="@+id/et_receiver_address"
android:layout_width="250dp"
android:layout_height="60dp"
android:hint="Send to:"
app:layout_constraintBottom_toTopOf="@id/et_sum_to_send"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<EditText
android:id="@+id/et_sum_to_send"
android:layout_width="250dp"
android:layout_height="60dp"
android:layout_marginBottom="32dp"
android:hint="Enter amount to send"
app:layout_constraintBottom_toTopOf="@id/btn_scan"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<Button
android:id="@+id/btn_scan"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="Scan card and get info"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.6" />
<Button
android:id="@+id/btn_check_fee"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:enabled="false"
android:text="Check fee"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_scan" />
<Button
android:id="@+id/btn_send"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:enabled="false"
android:text="Send transaction"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@id/btn_check_fee" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#527Aff</color>
<color name="colorPrimaryDark">#027AFF</color>
<color name="colorAccent">#027AFF</color>
<color name="fab">#4f98c0</color>
</resources>

View file

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Blockchain Demo</string>
</resources>

View file

@ -0,0 +1,11 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<tech-list>
<tech>android.nfc.tech.IsoDep</tech>
<tech>android.nfc.tech.Ndef</tech>
<tech>android.nfc.tech.NfcV</tech>
</tech-list>
</resources>

View file

@ -0,0 +1,16 @@
package com.tangem.blockchain_demo
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View file

@ -2,6 +2,7 @@ apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 29
buildToolsVersion "29.0.2"
@ -24,6 +25,13 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
@ -33,11 +41,12 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'com.squareup.retrofit2:retrofit:2.7.0'
implementation 'com.squareup.retrofit2:converter-moshi:2.6.0'
implementation 'com.squareup.moshi:moshi:1.9.2'
implementation "com.squareup.moshi:moshi-kotlin:1.9.2"
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.9.2")
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
@ -45,21 +54,21 @@ dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.3.3"
implementation 'org.bitcoinj:bitcoinj-core:0.15.2'
implementation 'com.github.stellar:java-stellar-sdk:0.13.0'
implementation 'com.github.stellar:java-stellar-sdk:0.14.0'
implementation "com.madgag.spongycastle:core:1.58.0.0"
implementation "com.madgag.spongycastle:prov:1.58.0.0"
ext.kethereum_version = '0.79.5'
implementation "com.github.walleth.kethereum:functions:$kethereum_version"
ext.kethereum_version = '0.81.2'
implementation "com.github.walleth.kethereum:extensions_kotlin:$kethereum_version"
implementation "com.github.walleth.kethereum:extensions_transactions:$kethereum_version"
implementation "com.github.walleth.kethereum:erc55:$kethereum_version"
implementation "com.github.walleth.kethereum:keccak_shortcut:$kethereum_version"
implementation "com.github.walleth.kethereum:wallet:$kethereum_version"
implementation "com.github.walleth.kethereum:crypto_impl_spongycastle:$kethereum_version"
implementation "com.github.walleth.kethereum:crypto:$kethereum_version"
implementation "com.github.walleth.kethereum:crypto_api:$kethereum_version"
implementation "com.github.walleth.kethereum:model:$kethereum_version"
implementation 'com.github.komputing.khex:core:1.0.0-RC6'
implementation 'com.github.komputing.khex:extensions:1.0.0-RC6'
implementation 'co.nstant.in:cbor:0.8'

View file

@ -2,7 +2,6 @@ package com.tangem.blockchain.bitcoin
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.toCanonicalised
import org.bitcoinj.core.*
import org.bitcoinj.crypto.TransactionSignature
import org.bitcoinj.script.Script

View file

@ -15,7 +15,7 @@ import java.math.BigDecimal
class BitcoinWalletManager(
private val cardId: String,
private val walletPublicKey: ByteArray,
walletConfig: WalletConfig,
override var wallet: CurrencyWallet,
isTestNet: Boolean = false
) : WalletManager,
TransactionSender,
@ -23,11 +23,13 @@ class BitcoinWalletManager(
override val blockchain = if (isTestNet) Blockchain.BitcoinTestnet else Blockchain.Bitcoin
private val address = blockchain.makeAddress(walletPublicKey)
private val currencyWallet = CurrencyWallet(walletConfig, address)
override var wallet: Wallet = currencyWallet
private val transactionBuilder = BitcoinTransactionBuilder(isTestNet)
private val networkManager = BitcoinNetworkManager(isTestNet)
init {
wallet.balances[AmountType.Coin] = Amount(null, blockchain)
}
override suspend fun update() {
val response = networkManager.getInfo(address)
when (response) {
@ -38,23 +40,24 @@ class BitcoinWalletManager(
private fun updateWallet(response: BitcoinAddressResponse) {
Log.d(this::class.java.simpleName, "Balance is ${response.balance}")
currencyWallet.balances[AmountType.Coin]?.value = response.balance
wallet.balances[AmountType.Coin]?.value = response.balance
transactionBuilder.unspentOutputs = response.unspentTransactions
if (response.hasUnconfirmed) {
if (currencyWallet.pendingTransactions.isEmpty()) {
currencyWallet.pendingTransactions.add(TransactionData(
if (wallet.pendingTransactions.isEmpty()) {
wallet.pendingTransactions.add(TransactionData(
Amount(blockchain.currency, decimals = blockchain.decimals),
null,
"unknown",
currencyWallet.address))
wallet.address))
}
} else {
currencyWallet.pendingTransactions.clear()
wallet.pendingTransactions.clear()
}
}
private fun updateError(error: Throwable?) {
Log.e(this::class.java.simpleName, error?.message ?: "")
if (error != null) throw error
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
@ -73,7 +76,7 @@ class BitcoinWalletManager(
}
}
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {
when (val result = networkManager.getFee()) {
is Result.Failure -> return result
is Result.Success -> {

View file

@ -1,6 +1,5 @@
package com.tangem.blockchain.cardano
import android.util.Base64
import android.util.Log
import com.tangem.blockchain.cardano.network.CardanoAddressResponse
import com.tangem.blockchain.cardano.network.CardanoNetworkManager
@ -10,20 +9,17 @@ import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.extensions.encodeBase64NoWrap
import com.tangem.blockchain.wallets.CurrencyWallet
import com.tangem.tasks.TaskEvent
import java.math.BigDecimal
class CardanoWalletManager(
private val cardId: String,
private val walletPublicKey: ByteArray,
walletConfig: WalletConfig
override var wallet: CurrencyWallet
) : WalletManager,
TransactionSender,
FeeProvider {
override val blockchain = Blockchain.Cardano
private val address = blockchain.makeAddress(walletPublicKey)
private val currencyWallet = CurrencyWallet(walletConfig, address)
override var wallet: Wallet = currencyWallet
private val transactionBuilder = CardanoTransactionBuilder()
private val networkManager = CardanoNetworkManager()
@ -37,13 +33,14 @@ class CardanoWalletManager(
private fun updateWallet(response: CardanoAddressResponse) {
Log.d(this::class.java.simpleName, "Balance is ${response.balance.toString()}")
currencyWallet.balances[AmountType.Coin]?.value =
wallet.balances[AmountType.Coin]?.value =
response.balance.toBigDecimal().movePointLeft(blockchain.decimals.toInt())
transactionBuilder.unspentOutputs = response.unspentOutputs
}
private fun updateError(error: Throwable?) {
Log.e(this::class.java.simpleName, error?.message ?: "")
if (error != null) throw error
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
@ -58,13 +55,13 @@ class CardanoWalletManager(
}
}
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {
val a = 0.155381
val b = 0.000043946
val size = transactionBuilder.getEstimateSize(
TransactionData(amount, null, source, destination), walletPublicKey
TransactionData(amount, null, address, destination), walletPublicKey
)
val fee = (a + b * size).toBigDecimal()
return Result.Success(listOf(Amount(blockchain.currency, fee, source, blockchain.decimals)))
return Result.Success(listOf(Amount(blockchain.currency, fee, address, blockchain.decimals)))
}
}

View file

@ -2,13 +2,15 @@ package com.tangem.blockchain.common
import com.tangem.blockchain.bitcoin.BitcoinAddressFactory
import com.tangem.blockchain.bitcoin.BitcoinAddressValidator
import com.tangem.blockchain.ethereum.EthereumAddressFactory
import com.tangem.blockchain.ethereum.EthereumAddressValidator
import com.tangem.blockchain.cardano.CardanoAddressFactory
import com.tangem.blockchain.cardano.CardanoAddressValidator
import com.tangem.blockchain.ethereum.EthereumAddressFactory
import com.tangem.blockchain.ethereum.EthereumAddressValidator
import com.tangem.blockchain.stellar.StellarAddressFactory
import com.tangem.blockchain.xrp.XrpAddressFactory
import com.tangem.blockchain.xrp.XrpAddressValidator
import java.math.BigDecimal
enum class Blockchain(
@ -24,7 +26,7 @@ enum class Blockchain(
Ethereum("ETH", "ETH", 18, "Ethereum", 0),
Rootstock("", "", 18, "", 0),
Cardano("CARDANO", "ADA", 6, "Cardano", 0),
XRP("", "XRP", 6, "XRP Ledger", 0),
XRP("XRP", "XRP", 6, "XRP Ledger", 0),
Binance("", "", 8, "", 0),
Stellar("XLM", "XLM", 7, "Stellar", 0);

View file

@ -30,6 +30,9 @@ data class Amount(
address: String? = null,
type: AmountType = AmountType.Coin
) : this(blockchain.currency, value, address, blockchain.decimals, type)
constructor(token: Token, value: BigDecimal? = null) :
this(token.symbol, value, token.contractAddress, token.decimals, AmountType.Token)
}
data class TransactionData(

View file

@ -2,12 +2,12 @@ package com.tangem.blockchain.common
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.wallets.CurrencyWallet
import com.tangem.commands.SignResponse
import com.tangem.tasks.TaskEvent
import kotlinx.coroutines.flow.Flow
interface WalletManager {
var wallet: Wallet
var wallet: CurrencyWallet
val blockchain: Blockchain
suspend fun update()
@ -22,5 +22,5 @@ interface TransactionSigner {
}
interface FeeProvider {
suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>>
suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>>
}

View file

@ -1,10 +1,12 @@
package com.tangem.blockchain.common
import com.tangem.blockchain.bitcoin.BitcoinWalletManager
import com.tangem.blockchain.cardano.CardanoWalletManager
import com.tangem.blockchain.ethereum.Chain
import com.tangem.blockchain.ethereum.EthereumWalletManager
import com.tangem.blockchain.cardano.CardanoWalletManager
import com.tangem.blockchain.stellar.StellarWalletManager
import com.tangem.blockchain.wallets.CurrencyWallet
import com.tangem.blockchain.xrp.XrpWalletManager
import com.tangem.commands.Card
@ -13,53 +15,56 @@ object WalletManagerFactory {
fun makeWalletManager(card: Card): WalletManager? {
val walletPublicKey: ByteArray = card.walletPublicKey ?: return null
val blockchainName: String = card.cardData?.blockchainName ?: return null
val blockchain = Blockchain.fromId(blockchainName)
when {
blockchainName == Blockchain.Bitcoin.id -> {
val token = getToken(card)
val wallet = CurrencyWallet.newInstance(blockchain, blockchain.makeAddress(walletPublicKey), token)
when (blockchain) {
Blockchain.Bitcoin -> {
return BitcoinWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(true, true)
wallet = wallet
)
}
blockchainName == Blockchain.BitcoinTestnet.id -> {
Blockchain.BitcoinTestnet -> {
return BitcoinWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(true, true),
wallet = wallet,
isTestNet = true
)
}
blockchainName == Blockchain.Ethereum.id -> {
Blockchain.Ethereum -> {
val chain = Chain.Mainnet
return EthereumWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(true, true),
wallet = wallet,
chain = chain
)
}
blockchainName == Blockchain.Stellar.id -> {
val token = getToken(card)
Blockchain.Stellar -> {
return StellarWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(true, token == null),
token = token
wallet = wallet
)
}
blockchainName == Blockchain.Cardano.id -> {
Blockchain.Cardano -> {
return CardanoWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(false, true)
wallet = wallet
)
}
blockchainName == Blockchain.XRP.id -> {
Blockchain.XRP -> {
return XrpWalletManager(
cardId = card.cardId,
walletPublicKey = walletPublicKey,
walletConfig = WalletConfig(true, true)
wallet = wallet
)
}
else -> return null

View file

@ -4,12 +4,11 @@ import com.tangem.CardManager
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.commands.SignResponse
import com.tangem.tasks.TaskEvent
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
suspend fun <T> retryIO(
times: Int = 3,
@ -56,11 +55,11 @@ sealed class SimpleResult {
}
class Signer(private val cardManager: CardManager) : TransactionSigner {
override suspend fun sign(hashes: Array<ByteArray>, cardId: String): TaskEvent<SignResponse> = coroutineScope {
async {
suspendCancellableCoroutine<TaskEvent<SignResponse>> { continuation ->
cardManager.sign(hashes, cardId) { if (continuation.isActive) continuation.resume(it) }
override suspend fun sign(hashes: Array<ByteArray>, cardId: String): TaskEvent<SignResponse> =
suspendCancellableCoroutine { continuation ->
cardManager.sign(hashes, cardId) { result ->
if (continuation.isActive) continuation.resume(result)
}
}
}.await()
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.blockchain.common.network
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.blockchain.BuildConfig
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
@ -18,11 +20,16 @@ private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor {
return logging
}
private val moshi: Moshi by lazy {
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
}
fun createRetrofitInstance(baseUrl: String): Retrofit =
Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(MoshiConverterFactory.create())
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(okHttpClient)
.build()

View file

@ -1,7 +1,7 @@
package com.tangem.blockchain.ethereum
import org.kethereum.crypto.toAddress
import org.kethereum.functions.isValid
import org.kethereum.erc55.isValid
import org.kethereum.model.Address
import org.kethereum.model.PublicKey

View file

@ -1,6 +1,8 @@
package com.tangem.blockchain.ethereum
import android.util.Log
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.ethereum.network.EthereumNetworkManager
@ -12,7 +14,7 @@ import org.kethereum.DEFAULT_GAS_LIMIT
import org.kethereum.crypto.api.ec.ECDSASignature
import org.kethereum.crypto.determineRecId
import org.kethereum.crypto.impl.ec.canonicalise
import org.kethereum.functions.encodeRLP
import org.kethereum.extensions.transactions.encodeRLP
import org.kethereum.keccakshortcut.keccak
import org.kethereum.model.*
import java.math.BigDecimal
@ -22,22 +24,32 @@ class EthereumWalletManager(
private val cardId: String,
private val walletPublicKey: ByteArray,
chain: Chain,
walletConfig: WalletConfig
token: Token? = null,
override var wallet: CurrencyWallet
) : WalletManager,
TransactionSender,
FeeProvider {
override val blockchain: Blockchain = Blockchain.Ethereum
private val address = blockchain.makeAddress(walletPublicKey)
private val currencyWallet = CurrencyWallet(walletConfig, address)
override var wallet: Wallet = currencyWallet
private val builder = EthereumTransactionBuilder(chain)
private val networkManager = EthereumNetworkManager()
private var pendingTxCount = -1L
private var txCount = -1L
init {
if (token != null) wallet.balances[AmountType.Token] =
Amount(
token.symbol,
null,
token.contractAddress,
token.decimals,
AmountType.Token)
wallet.balances[AmountType.Coin] = Amount(null, blockchain)
}
override suspend fun update() {
val result = networkManager.getInfo(address, currencyWallet.balances[AmountType.Token]?.address)
val result = networkManager.getInfo(address, wallet.balances[AmountType.Token]?.address)
when (result) {
is Result.Failure -> updateError(result.error)
is Result.Success -> updateWallet(result.data)
@ -45,23 +57,24 @@ class EthereumWalletManager(
}
private fun updateWallet(data: EthereumResponse) {
currencyWallet.balances[AmountType.Coin]?.value = data.balance
currencyWallet.balances[AmountType.Token]?.value = data.tokenBalance
wallet.balances[AmountType.Coin]?.value = data.balance
wallet.balances[AmountType.Token]?.value = data.tokenBalance
txCount = data.txCount
pendingTxCount = data.pendingTxCount
if (txCount == pendingTxCount) {
currencyWallet.pendingTransactions.forEach { it.status = TransactionStatus.Confirmed }
} else if (currencyWallet.pendingTransactions.isEmpty()) {
currencyWallet.pendingTransactions.add(TransactionData(
wallet.pendingTransactions.forEach { it.status = TransactionStatus.Confirmed }
} else if (wallet.pendingTransactions.isEmpty()) {
wallet.pendingTransactions.add(TransactionData(
Amount(blockchain.currency, decimals = blockchain.decimals),
null,
"unknown",
currencyWallet.address))
wallet.address))
}
}
private fun updateError(error: Throwable?) {
Log.e(this::class.java.simpleName, error?.message ?: "")
if (error != null) throw error
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
@ -76,7 +89,7 @@ class EthereumWalletManager(
}
}
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {
val result = networkManager.getFee(getGasLimit(amount).value)
when (result) {
is Result.Success -> {

View file

@ -12,7 +12,6 @@ import kotlinx.coroutines.coroutineScope
import org.kethereum.ETH_IN_WEI
import java.math.BigDecimal
import java.math.BigInteger
import java.math.RoundingMode
class EthereumNetworkManager {

View file

@ -54,7 +54,7 @@ class StellarNetworkManager(isTestNet: Boolean) {
coroutineScope {
val accountResponseDefered = async(Dispatchers.IO) { stellarServer.accounts().account(accountId) }
val ledgerResponseDeferred = async(Dispatchers.IO) {
val latestLedger: Int = stellarServer.root().coreLatestLedger
val latestLedger: Int = stellarServer.root().historyLatestLedger
stellarServer.ledgers().ledger(latestLedger.toLong())
}
@ -67,7 +67,7 @@ class StellarNetworkManager(isTestNet: Boolean) {
null
} else {
accountResponse.balances
.find { it.assetType != "native" && it.assetCode == assetCode }
.find { it.assetType != "native" && it.assetIssuer == assetCode }
?.balance?.toBigDecimal()
?: return@coroutineScope Result.Failure(Exception("Stellar Balance not found"))
}

View file

@ -1,5 +1,6 @@
package com.tangem.blockchain.stellar
import android.util.Log
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
@ -11,8 +12,7 @@ import java.util.*
class StellarWalletManager(
private val cardId: String,
walletPublicKey: ByteArray,
walletConfig: WalletConfig,
token: Token? = null,
override var wallet: CurrencyWallet,
isTestNet: Boolean = false
) : WalletManager,
TransactionSender,
@ -20,26 +20,14 @@ class StellarWalletManager(
override val blockchain: Blockchain = Blockchain.Stellar
private val address = blockchain.makeAddress(walletPublicKey)
private val currencyWallet = CurrencyWallet(walletConfig, address)
override var wallet: Wallet = currencyWallet
private val networkManager = StellarNetworkManager(isTestNet)
private val builder = StellarTransactionBuilder(networkManager, walletPublicKey)
private var baseFee = BASE_FEE
private var baseReserve = BASE_RESERVE
private var sequence = 0L
init {
if (token != null) currencyWallet.balances[AmountType.Token] =
Amount(
token.symbol,
null,
token.contractAddress,
token.decimals,
AmountType.Token)
}
override suspend fun update() {
val result = networkManager.getInfo(address, currencyWallet.balances[AmountType.Token]?.address)
val result = networkManager.getInfo(address, wallet.balances[AmountType.Token]?.address)
when (result) {
is Result.Failure -> updateError(result.error)
is Result.Success -> updateWallet(result.data)
@ -47,15 +35,15 @@ class StellarWalletManager(
}
private fun updateWallet(data: StellarResponse) {
currencyWallet.balances[AmountType.Coin]?.value = data.balance
currencyWallet.balances[AmountType.Token]?.value = data.assetBalance
currencyWallet.balances[AmountType.Reserve]?.value = data.baseReserve
wallet.balances[AmountType.Coin]?.value = data.balance
wallet.balances[AmountType.Token]?.value = data.assetBalance
wallet.balances[AmountType.Reserve]?.value = data.baseReserve
sequence = data.sequence
baseFee = data.baseFee
baseReserve = data.baseReserve
val currentTime = Calendar.getInstance().timeInMillis
currencyWallet.pendingTransactions.forEach { transaction ->
wallet.pendingTransactions.forEach { transaction ->
if (transaction.date?.timeInMillis ?: 0 - currentTime > 10) {
transaction.status = TransactionStatus.Confirmed
}
@ -63,7 +51,8 @@ class StellarWalletManager(
}
private fun updateError(error: Throwable?) {
Log.e(this::class.java.simpleName, error?.message ?: "")
if (error != null) throw error
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
@ -77,7 +66,7 @@ class StellarWalletManager(
}
}
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {
return Result.Success(listOf(
Amount(baseFee, blockchain)
))

View file

@ -4,17 +4,63 @@ import com.tangem.blockchain.common.*
import java.util.*
class CurrencyWallet(
override val config: WalletConfig,
override val address: String,
override val exploreUrl: String? = null,
override val shareUrl: String? = null,
val pendingTransactions: MutableList<TransactionData> = mutableListOf(),
val balances: MutableMap<AmountType, Amount> = mutableMapOf(),
val isTestnet: Boolean = false
val blockchain: Blockchain,
address: String,
override val config: WalletConfig
) : Wallet, TransactionValidator {
override val exploreUrl: String? = null
override val shareUrl: String? = null
override val address: String
get() = balances[AmountType.Coin]!!.address!!
val pendingTransactions: MutableList<TransactionData> = mutableListOf()
val balances: MutableMap<AmountType, Amount> = mutableMapOf()
init {
addAmount(Amount(null, blockchain, address))
}
override fun validateTransaction(amount: Amount, fee: Amount?): EnumSet<ValidationError> {
TODO("not implemented")
}
fun addAmount(amount: Amount) {
balances[amount.type] = amount
}
fun addPendingTransaction(transaction: TransactionData) {
pendingTransactions.add(transaction.copy(date = Calendar.getInstance()))
}
companion object {
fun newInstance(blockchain: Blockchain, address: String, token: Token?): CurrencyWallet {
return when (blockchain) {
Blockchain.Bitcoin, Blockchain.XRP -> {
val config = WalletConfig(true, true)
CurrencyWallet(blockchain, address, config)
}
Blockchain.Cardano -> {
val config = WalletConfig(false, true)
CurrencyWallet(blockchain, address, config)
}
Blockchain.Ethereum -> {
val config = WalletConfig(true, token == null)
val wallet = CurrencyWallet(blockchain, address, config)
if (token != null) wallet.addAmount(Amount(token))
wallet
}
Blockchain.Stellar -> {
val config = WalletConfig(false, token == null)
val wallet = CurrencyWallet(blockchain, address, config)
if (token != null) wallet.addAmount(Amount(token))
wallet.addAmount(Amount(null, blockchain, address, AmountType.Reserve))
wallet
}
else -> throw Exception("Unsupported blockchain")
}
}
}
}

View file

@ -12,18 +12,21 @@ import com.tangem.tasks.TaskEvent
class XrpWalletManager(
private val cardId: String,
private val walletPublicKey: ByteArray,
walletConfig: WalletConfig
override var wallet: CurrencyWallet
) : WalletManager,
TransactionSender,
FeeProvider {
override val blockchain = Blockchain.XRP
private val address = blockchain.makeAddress(walletPublicKey)
private val currencyWallet = CurrencyWallet(walletConfig, address)
override var wallet: Wallet = currencyWallet
private val transactionBuilder = XrpTransactionBuilder(walletPublicKey)
private val networkManager = XrpNetworkManager()
init {
wallet.balances[AmountType.Coin] = Amount(null, blockchain)
wallet.balances[AmountType.Reserve] = Amount(null, blockchain, type = AmountType.Reserve)
}
override suspend fun update() {
val result = networkManager.getInfo(address)
when (result) {
@ -34,30 +37,31 @@ class XrpWalletManager(
private fun updateWallet(response: XrpInfoResponse) {
Log.d(this::class.java.simpleName, "Balance is ${response.balance}")
currencyWallet.balances[AmountType.Reserve]?.value = response.reserveBase
wallet.balances[AmountType.Reserve]?.value = response.reserveBase
if (!response.accountFound) {
updateError(Exception("Account not found")) //TODO rework, add reserve
return
}
currencyWallet.balances[AmountType.Coin]?.value = response.balance - response.reserveBase
wallet.balances[AmountType.Coin]?.value = response.balance - response.reserveBase
transactionBuilder.sequence = response.sequence
if (response.hasUnconfirmed) {
if (currencyWallet.pendingTransactions.isEmpty()) {
currencyWallet.pendingTransactions.add(TransactionData(
if (wallet.pendingTransactions.isEmpty()) {
wallet.pendingTransactions.add(TransactionData(
Amount(blockchain.currency, decimals = blockchain.decimals),
null,
"unknown",
currencyWallet.address))
wallet.address))
}
} else {
currencyWallet.pendingTransactions.clear()
wallet.pendingTransactions.clear()
}
}
private fun updateError(error: Throwable?) {
Log.e(this::class.java.simpleName, error?.message ?: "")
if (error != null) throw error
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
@ -72,7 +76,7 @@ class XrpWalletManager(
}
}
override suspend fun getFee(amount: Amount, source: String, destination: String): Result<List<Amount>> {
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {
val result = networkManager.getFee()
when (result) {
is Result.Failure -> return result

View file

@ -1,6 +1,5 @@
package com.tangem.blockchain.xrp.network
import com.tangem.blockchain.bitcoin.network.BitcoinFee
import com.tangem.blockchain.common.extensions.Result
import com.tangem.blockchain.common.extensions.SimpleResult
import com.tangem.blockchain.common.network.API_RIPPLED

View file

@ -8,7 +8,6 @@ import com.tangem.blockchain.xrp.network.XrpFeeResponse
import com.tangem.blockchain.xrp.network.XrpInfoResponse
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import java.math.BigDecimal
class RippledProvider(private val api: RippledApi) {
private val decimals = Blockchain.XRP.decimals.toInt()

View file

@ -9,6 +9,7 @@ buildscript {
dependencies {
classpath "com.android.tools.build:gradle:$versions.build_gradle"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin"
classpath "com.github.dcendents:android-maven-gradle-plugin:2.1"
classpath "io.fabric.tools:gradle:1.31.0"
}
}

View file

@ -1,4 +1,4 @@
ext.versions = [
kotlin : '1.3.61',
kotlin : '1.3.70',
build_gradle: '3.6.1',
]

View file

@ -20,9 +20,7 @@ platform :android do
desc "Runs all the tests"
lane :test do
gradle(
task: "test",
flavor: "tangemAccess",
build_type: "DebugUnitTest")
task: "test")
end
desc "Build a signed release APK"

View file

@ -47,7 +47,7 @@ class LocalStorage
artworks = HashMap()
}
if (artworks.count() < 39) {
if (artworks.count() < 43) {
// forceSave=true only on the last one
putResourceArtworkToCatalog(R.drawable.card_default, false)
putResourceArtworkToCatalog(R.drawable.card_default_nft, false)
@ -78,9 +78,13 @@ class LocalStorage
putResourceArtworkToCatalog(R.drawable.card_ru043, false)
putResourceArtworkToCatalog(R.drawable.card_tg044, false)
putResourceArtworkToCatalog(R.drawable.card_tg046, false)
putResourceArtworkToCatalog(R.drawable.card_tg049, false)
putResourceArtworkToCatalog(R.drawable.card_tg051, false)
putResourceArtworkToCatalog(R.drawable.card_tg052, false)
putResourceArtworkToCatalog(R.drawable.card_tg054, false)
putResourceArtworkToCatalog(R.drawable.card_tg055, false)
putResourceArtworkToCatalog(R.drawable.card_tg057, false)
putResourceArtworkToCatalog(R.drawable.card_tg058, false)
putResourceArtworkToCatalog(R.drawable.card_tg060, false)
putResourceArtworkToCatalog(R.drawable.card_tg061, false)
putResourceArtworkToCatalog(R.drawable.card_tg062, false)
@ -281,8 +285,12 @@ class LocalStorage
card.batch == "0029" -> R.drawable.card_ru041
card.batch == "0031" -> R.drawable.card_ru042
card.batch == "0034" -> R.drawable.card_tg046
card.batch == "0037" -> R.drawable.card_tg049
card.batch == "0038" -> R.drawable.card_tg051
card.batch == "0039" -> R.drawable.card_tg052
card.batch == "0041" -> R.drawable.card_tg054
card.batch == "0042" -> R.drawable.card_tg055
card.batch == "0044" -> R.drawable.card_tg058
card.batch == "0046" -> R.drawable.card_tg054
card.batch == "0047" -> R.drawable.card_tg055
card.batch == "0045" -> R.drawable.card_tg057

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View file

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

View file

@ -1,5 +1,6 @@
apply plugin: "kotlin"
apply plugin: 'org.jetbrains.dokka'
apply plugin: 'com.github.dcendents.android-maven'
apply from: '../dependencies.gradle'
apply from: '../jitpack.gradle'
@ -7,14 +8,13 @@ group = "$jitpackSdk.group"
version "$jitpackSdk.version"
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin"
implementation "com.madgag.spongycastle:core:1.58.0.0"
implementation "com.madgag.spongycastle:prov:1.58.0.0"
implementation 'net.i2p.crypto:eddsa:0.3.0'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
testImplementation "com.google.truth:truth:1.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:1.3.61"
implementation "org.jetbrains.kotlin:kotlin-reflect:$versions.kotlin"
}
sourceCompatibility = "8"

View file

@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
buildToolsVersion "29.0.0"
buildToolsVersion "29.0.2"
defaultConfig {

View file

@ -1,6 +1,7 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'com.github.dcendents.android-maven'
apply from: '../dependencies.gradle'
apply from: '../jitpack.gradle'
@ -9,7 +10,7 @@ version "$jitpackSdk.version"
android {
compileSdkVersion 29
buildToolsVersion "29.0.0"
buildToolsVersion "29.0.2"
defaultConfig {