tangem-app-android-audited/app/src/main/java/com/tangem/presentation/activity/ConfirmPaymentActivity.kt
2018-10-30 12:25:49 +03:00

516 lines
No EOL
21 KiB
Kotlin

package com.tangem.presentation.activity
import android.app.Activity
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.text.Editable
import android.text.Html
import android.text.TextWatcher
import android.util.Log
import android.view.KeyEvent
import android.view.View
import android.widget.*
import com.tangem.data.network.ServerApiHelper
import com.tangem.data.network.ServerApiHelperElectrum
import com.tangem.data.network.model.InfuraResponse
import com.tangem.data.network.ElectrumRequest
import com.tangem.domain.cardReader.NfcManager
import com.tangem.domain.wallet.*
import com.tangem.util.*
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.activity_confirm_payment.*
import org.json.JSONException
import java.io.IOException
import java.math.BigDecimal
import java.math.BigInteger
import java.text.DecimalFormat
import java.util.*
class ConfirmPaymentActivity : AppCompatActivity(), NfcAdapter.ReaderCallback {
companion object {
private const val REQUEST_CODE_SIGN_PAYMENT = 1
private const val REQUEST_CODE_REQUEST_PIN2 = 2
}
private var nfcManager: NfcManager? = null
private var serverApiHelper: ServerApiHelper = ServerApiHelper()
private var serverApiHelperElectrum: ServerApiHelperElectrum = ServerApiHelperElectrum()
//private var card: TangemCard? = null
private lateinit var ctx: TangemContext
private var feeRequestSuccess = false
private var balanceRequestSuccess = false
private var minFee: String? = null
private var maxFee: String? = null
private var normalFee: String? = null
private var isIncludeFee: Boolean = true
private var minFeeInInternalUnits: CoinEngine.InternalAmount? = CoinEngine.InternalAmount(0, "")
private var requestPIN2Count = 0
private var nodeCheck = false
private var dtVerified: Date? = null
private var calcSize: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_confirm_payment)
MainActivity.commonInit(applicationContext)
nfcManager = NfcManager(this, this)
ctx = TangemContext.loadFromBundle(this, intent.extras)
// card = TangemCard(intent.getStringExtra("UID"))
// card!!.loadFromBundle(intent.extras!!.getBundle("Card"))
//
val engine = CoinEngineFactory.create(ctx)
val html = Html.fromHtml(engine!!.balanceHTML)
tvBalance.text = html
isIncludeFee = intent.getBooleanExtra("IncFee", true)
if (isIncludeFee)
tvIncFee.setText(R.string.including_fee)
else
tvIncFee.setText(R.string.not_including_fee)
if (ctx.blockchain == Blockchain.Token)
tvIncFee.visibility = View.INVISIBLE
else
tvIncFee.visibility = View.VISIBLE
etAmount.setText(intent.getStringExtra(SignPaymentActivity.EXTRA_AMOUNT))
tvCurrency.text = Html.fromHtml(engine.balanceCurrencyHTML)
tvCurrency2.text = Html.fromHtml(engine.feeCurrencyHTML)
tvCardID.text = ctx.card!!.cidDescription
etWallet.setText(intent.getStringExtra("Wallet"))
etFee.setText("")
btnSend.visibility = View.INVISIBLE
feeRequestSuccess = false
balanceRequestSuccess = false
if (ctx.blockchain == Blockchain.Ethereum || ctx.blockchain == Blockchain.EthereumTestNet || ctx.blockchain == Blockchain.Token) {
rgFee.isEnabled = false
requestInfura(ServerApiHelper.INFURA_ETH_GAS_PRICE)
} else {
rgFee.isEnabled = true
requestElectrum(ctx.card, ElectrumRequest.checkBalance(ctx.card!!.wallet))
calcSize = 256
try {
calcSize = buildSize(etWallet!!.text.toString(), "0.00", etAmount.text.toString())
} catch (ex: Exception) {
Log.e("Build Fee error", ex.message)
}
ctx.coinData!!.resetFailedBalanceRequestCounter()
progressBar.visibility = View.VISIBLE
requestEstimateFee()
}
// 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 engine = CoinEngineFactory.create(ctx.blockchain)
val eqFee = engine!!.evaluateFeeEquivalent(etFee!!.text.toString())
tvFeeEquivalent.text = eqFee
if (!ctx.coinData!!.amountEquivalentDescriptionAvailable) {
tvFeeEquivalent.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 {
val calendar = Calendar.getInstance()
calendar.add(Calendar.MINUTE, -1)
if (dtVerified == null || dtVerified!!.before(calendar.time)) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.the_obtained_data_is_outdated_try_again))
return@setOnClickListener
}
val engineCoin = CoinEngineFactory.create(ctx)
if (engineCoin!!.isNeedCheckNode && !nodeCheck) {
Toast.makeText(baseContext, getString(R.string.cannot_reach_current_active_blockchain_node_try_again), 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.checkUnspentTransaction()) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.please_wait_for_confirmation_of_incoming_transaction))
return@setOnClickListener
}
if (!engineCoin.checkNewTransactionAmountAndFee(txAmount, txFee, isIncludeFee, minFeeInInternalUnits)) {
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("mode", PinRequestActivity.Mode.RequestPIN2.toString())
ctx.saveToBundle(intent.extras)
// intent.putExtra(TangemCard.EXTRA_UID, card!!.uid)
// intent.putExtra(TangemCard.EXTRA_CARD, card!!.asBundle)
intent.putExtra("IncFee", isIncludeFee)
startActivityForResult(intent, REQUEST_CODE_REQUEST_PIN2)
}
// request electrum listener
val electrumBodyListener: ServerApiHelperElectrum.ElectrumRequestDataListener = object : ServerApiHelperElectrum.ElectrumRequestDataListener {
override fun onSuccess(electrumRequest: ElectrumRequest?) {
if (electrumRequest!!.isMethod(ElectrumRequest.METHOD_GetBalance)) {
try {
etFee.setText(getString(R.string.empty))
val engine = CoinEngineFactory.create(ctx)
val balance= engine.convertToAmount(CoinEngine.InternalAmount(electrumRequest.result.getLong("confirmed") + electrumRequest.result.getLong("unconfirmed"), "Satoshi"))
val amount = CoinEngine.Amount(etAmount.text.toString(), ctx.blockchain.currency)
if (balance < amount) {
etFee.error = getString(R.string.not_enough_funds)
} else {
etFee.error = null
balanceRequestSuccess = true
if (feeRequestSuccess && balanceRequestSuccess) {
btnSend.visibility = View.VISIBLE
}
dtVerified = Date()
nodeCheck = true
}
} catch (e: JSONException) {
e.printStackTrace()
requestElectrum(ctx.card!!, ElectrumRequest.checkBalance(ctx.card!!.wallet))
}
}
}
override fun onFail(message: String?) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.cannot_check_balance_no_connection_with_blockchain_nodes))
}
}
serverApiHelperElectrum.setElectrumRequestData(electrumBodyListener)
// request infura eth gasPrice listener
val infuraBodyListener: ServerApiHelper.InfuraBodyListener = object : ServerApiHelper.InfuraBodyListener {
override fun onSuccess(method: String, infuraResponse: InfuraResponse) {
when (method) {
ServerApiHelper.INFURA_ETH_GAS_PRICE -> {
var gasPrice = infuraResponse.result
gasPrice = gasPrice.substring(2)
//TODO - remove Gwei
// rounding gas price to integer gwei
val l = BigInteger(gasPrice, 16).divide(BigInteger.valueOf(1000000000L)).multiply(BigInteger.valueOf(1000000000L))
val m = if (ctx.blockchain == Blockchain.Token) BigInteger.valueOf(60000) else BigInteger.valueOf(21000)
val minFeeInGwei = (ctx.coinData!! as EthData).getAmountInGwei(l.multiply(m).toString())
val normalFeeInGwei = (ctx.coinData!! as EthData).getAmountInGwei(l.multiply(BigInteger.valueOf(12)).divide(BigInteger.valueOf(10)).multiply(m).toString())
val maxFeeInGwei = (ctx.coinData!! as EthData).getAmountInGwei(l.multiply(BigInteger.valueOf(15)).divide(BigInteger.valueOf(10)).multiply(m).toString())
minFee = minFeeInGwei
normalFee = normalFeeInGwei
maxFee = maxFeeInGwei
etFee.setText(normalFeeInGwei.replace(',', '.'))
etFee.error = null
btnSend.visibility = View.VISIBLE
feeRequestSuccess = true
balanceRequestSuccess = true
dtVerified = Date()
minFeeInInternalUnits = CoinEngine.InternalAmount(normalFeeInGwei,"Gwei")
}
}
}
override fun onFail(method: String, message: String) {
when (method) {
ServerApiHelper.INFURA_ETH_GAS_PRICE -> {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.cannot_obtain_data_from_blockchain))
}
}
}
}
serverApiHelper.setInfuraResponse(infuraBodyListener)
// request estimate fee listener
val estimateFeeListener: ServerApiHelper.EstimateFeeListener = object : ServerApiHelper.EstimateFeeListener {
override fun onSuccess(blockCount: Int, estimateFeeResponse: String?) {
var fee: BigDecimal?
fee = BigDecimal(estimateFeeResponse) // BTC per 1 kb
if (fee == BigDecimal.ZERO) {
progressBar.visibility = View.INVISIBLE
requestEstimateFee()
}
if (calcSize.toLong() != 0L) {
fee = fee.multiply(BigDecimal(calcSize.toLong())).divide(BigDecimal(1024)) // per Kb -> per byte
} else {
requestEstimateFee()
}
progressBar.visibility = View.INVISIBLE
val df = DecimalFormat()
df.maximumFractionDigits = 7
df.minimumFractionDigits = 3
df.isGroupingUsed = false
val strFee = df.format(fee)
when (blockCount) {
ServerApiHelper.ESTIMATE_FEE_MINIMAL -> {
minFee = strFee
//TODO - we must know currency of fee
minFeeInInternalUnits = CoinEngine.InternalAmount(strFee, tvCurrency2.text.toString())
}
ServerApiHelper.ESTIMATE_FEE_NORMAL -> {
normalFee = strFee
doSetFee(rgFee.checkedRadioButtonId)
}
ServerApiHelper.ESTIMATE_FEE_PRIORITY -> {
maxFee = strFee
}
}
etFee.error = null
feeRequestSuccess = true
if (feeRequestSuccess && balanceRequestSuccess)
btnSend.visibility = View.VISIBLE
dtVerified = Date()
}
override fun onFail(message: String?) {
finishWithError(Activity.RESULT_CANCELED, getString(R.string.cannot_calculate_fee_wrong_data_received_from_node))
}
}
serverApiHelper.setEstimateFee(estimateFeeListener)
}
public override fun onResume() {
super.onResume()
nfcManager!!.onResume()
}
public override fun onPause() {
super.onPause()
nfcManager!!.onPause()
}
public override fun onStop() {
super.onStop()
nfcManager!!.onStop()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_SIGN_PAYMENT) {
if (data != null && data.extras != null) {
if (data.extras!!.containsKey("UID") && data.extras!!.containsKey("Card")) {
val updatedCard = TangemCard(data.getStringExtra("UID"))
updatedCard.loadFromBundle(data.getBundleExtra("Card"))
ctx.card = updatedCard
}
}
if (resultCode == SignPaymentActivity.RESULT_INVALID_PIN && requestPIN2Count < 2) {
requestPIN2Count++
val intent = Intent(baseContext, PinRequestActivity::class.java)
intent.putExtra("mode", PinRequestActivity.Mode.RequestPIN2.toString())
ctx.saveToBundle(intent.extras)
// intent.putExtra("UID", card!!.uid)
// intent.putExtra("Card", card!!.asBundle)
intent.putExtra("IncFee", isIncludeFee)
startActivityForResult(intent, REQUEST_CODE_REQUEST_PIN2)
return
}
setResult(resultCode, data)
finish()
} else if (requestCode == REQUEST_CODE_REQUEST_PIN2) {
if (resultCode == Activity.RESULT_OK) {
val intent = Intent(baseContext, SignPaymentActivity::class.java)
ctx.saveToBundle(intent.extras)
// intent.putExtra("UID", card!!.uid)
// intent.putExtra("Card", card!!.asBundle)
intent.putExtra("Wallet", etWallet!!.text.toString())
intent.putExtra(SignPaymentActivity.EXTRA_AMOUNT, etAmount.text.toString())
intent.putExtra("Fee", etFee.text.toString())
intent.putExtra("IncFee", isIncludeFee)
startActivityForResult(intent, REQUEST_CODE_SIGN_PAYMENT)
} else
Toast.makeText(baseContext, R.string.pin_2_is_required_to_sign_the_payment, Toast.LENGTH_LONG).show()
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_BACK -> {
val intent = Intent()
setResult(Activity.RESULT_CANCELED, intent)
finish()
return true
}
}
return super.onKeyDown(keyCode, event)
}
override fun onTagDiscovered(tag: Tag) {
try {
nfcManager!!.ignoreTag(tag)
} catch (e: IOException) {
e.printStackTrace()
}
}
// TODO - move to BtcEngine
@Throws(Exception::class)
internal fun buildSize(outputAddress: String, outFee: String, outAmount: String): Int {
val myAddress = ctx.card!!.wallet
val pbKey = ctx.card!!.walletPublicKey
val pbComprKey = ctx.card!!.walletPublicKeyRar
// build script for our address
val rawTxList = (ctx.coinData!! as BtcData).unspentTransactions
val outputScriptWeAreAbleToSpend = Transaction.Script.buildOutput(myAddress).bytes
// collect unspent
val unspentOutputs = BTCUtils.getOutputs(rawTxList, outputScriptWeAreAbleToSpend)
var fullAmount: Long = 0
for (i in unspentOutputs.indices) {
fullAmount += unspentOutputs[i].value
}
// get first unspent
val outPut = unspentOutputs[0]
val outPutIndex = outPut.outputIndex
// get prev TX id;
val prevTXID = rawTxList[0].txID//"f67b838d6e2c0c587f476f583843e93ff20368eaf96a798bdc25e01f53f8f5d2";
val fees = FormatUtil.ConvertStringToLong(outFee)
var amount = FormatUtil.ConvertStringToLong(outAmount)
amount -= fees
val change = fullAmount - fees - amount
if (amount + fees > fullAmount) {
throw Exception(String.format("Balance (%d) < amount (%d) + (%d)", fullAmount, change, amount))
}
val hashesForSign = arrayOfNulls<ByteArray>(unspentOutputs.size)
for (i in unspentOutputs.indices) {
val newTX = BTCUtils.buildTXForSign(myAddress, outputAddress, myAddress, unspentOutputs, i, amount, change)
val hashData = Util.calculateSHA256(newTX)
val doubleHashData = Util.calculateSHA256(hashData)
// Log.e("TX_BODY_1", BTCUtils.toHex(newTX))
// Log.e("TX_HASH_1", BTCUtils.toHex(hashData))
// Log.e("TX_HASH_2", BTCUtils.toHex(doubleHashData))
unspentOutputs[i].bodyDoubleHash = doubleHashData
unspentOutputs[i].bodyHash = hashData
hashesForSign[i] = doubleHashData
}
val signFromCard = ByteArray(64 * unspentOutputs.size)
for (i in unspentOutputs.indices) {
val r = BigInteger(1, Arrays.copyOfRange(signFromCard, 0 + i * 64, 32 + i * 64))
val s = BigInteger(1, Arrays.copyOfRange(signFromCard, 32 + i * 64, 64 + i * 64))
val encodingSign = DerEncodingUtil.packSignDer(r, s, pbKey)
unspentOutputs[i].scriptForBuild = encodingSign
}
val realTX = BTCUtils.buildTXForSend(outputAddress, myAddress, unspentOutputs, amount, change)
return realTX.size
}
private fun requestElectrum(card: TangemCard, electrumRequest: ElectrumRequest) {
if (UtilHelper.isOnline(this)) {
serverApiHelperElectrum.electrumRequestData(card, electrumRequest)
} else
finishWithError(Activity.RESULT_CANCELED, getString(R.string.cannot_obtain_data_from_blockchain))
}
private fun requestInfura(method: String) {
if (UtilHelper.isOnline(this)) {
serverApiHelper.infura(method, 67, ctx.card!!.wallet, "", "")
} else
finishWithError(Activity.RESULT_CANCELED, getString(R.string.cannot_obtain_data_from_blockchain))
}
private fun requestEstimateFee() {
serverApiHelper.estimateFee(ServerApiHelper.ESTIMATE_FEE_PRIORITY)
serverApiHelper.estimateFee(ServerApiHelper.ESTIMATE_FEE_NORMAL)
serverApiHelper.estimateFee(ServerApiHelper.ESTIMATE_FEE_MINIMAL)
}
private fun doSetFee(checkedRadioButtonId: Int) {
var txtFee = ""
when (checkedRadioButtonId) {
R.id.rbMinimalFee ->
if (minFee != null)
txtFee = minFee.toString()
else
finishWithError(Activity.RESULT_CANCELED, getString(R.string.cannot_obtain_data_from_blockchain))
R.id.rbNormalFee ->
if (normalFee != null)
txtFee = normalFee.toString()
else
finishWithError(Activity.RESULT_CANCELED, getString(R.string.cannot_obtain_data_from_blockchain))
R.id.rbMaximumFee ->
if (maxFee != null)
txtFee = maxFee.toString()
else
finishWithError(Activity.RESULT_CANCELED, getString(R.string.cannot_obtain_data_from_blockchain))
}
etFee.setText(txtFee.replace(',', '.'))
}
private fun finishWithError(errorCode: Int, message: String) {
val intent = Intent()
intent.putExtra("message", message)
setResult(errorCode, intent)
finish()
}
}