Updated on 2026-08-14

This commit is contained in:
Tangem 2020-04-15 16:06:23 +03:00
commit 9bfdafd678
27 changed files with 651 additions and 125 deletions

View file

@ -158,4 +158,7 @@ dependencies {
//dependencies for flow demo
implementation 'io.grpc:grpc-okhttp:1.28.0'
implementation 'io.grpc:grpc-stub:1.28.0'
//dependencies for TokenEmvEngine
implementation 'com.github.walleth.kethereum:extensions_kotlin:0.81.4'
}

View file

@ -6,7 +6,7 @@ import com.tangem.tangem_sdk.R;
* Created by dvol on 06.08.2017.
*/
public enum Blockchain {
Unknown("", "", 1.0, R.drawable.ic_logo_unknown, ""),
Unknown("", "", 1.0, R.drawable.ic_logo_unknown, "Unknown"),
Bitcoin("BTC", "BTC", 100000000.0, R.drawable.ic_logo_bitcoin, "Bitcoin"),
BitcoinTestNet("BTC/test", "BTC", 100000000.0, R.drawable.ic_logo_bitcoin_testnet, "Bitcoin Testnet"),
BitcoinDual("BTC/dual", "BTC", 100000000.0, R.drawable.ic_logo_bitcoin, "Bitcoin"),
@ -32,7 +32,8 @@ public enum Blockchain {
Eos("EOS", "EOS", 10000.0, R.drawable.tangem2, "EOS"),
Ducatus("DUC", "DUC", 100000000.0, R.drawable.tangem2, "Ducatus"),
Tezos("TEZOS", "XTZ", 10000000.0, R.drawable.ic_logo_tezos, "Tezos"),
FlowDemo("FLOW/demo", "", 1.0, R.drawable.tangem2, "Flow demo");
FlowDemo("FLOW/demo", "", 1.0, R.drawable.tangem2, "Flow demo"),
TokenEmv("TTW", "ETH", 1.0, R.drawable.ic_logo_ethereum, "Ethereum");
Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) {
mID = ID;
@ -67,7 +68,7 @@ public enum Blockchain {
for (Blockchain blockchain : values()) {
if (blockchain.getID().equals(id)) return blockchain;
}
return null;
return Blockchain.Unknown;
}
public static Blockchain fromCurrency(String currency) {

View file

@ -40,6 +40,14 @@ public class Server {
}
}
public static class ApiInfuraRopsten {
public static final String URL_INFURA_ROPSTEN = ServerURL.API_INFURA_ROPSTEN;
public static class Method {
public static final String MAIN = "v3/613a0b14833145968b1f656240c7d245";
}
}
public static class ApiSoChain {
public static final String URL = ServerURL.API_SOCHAIN_V2;

View file

@ -42,6 +42,8 @@ public class ServerApiInfura {
public ServerApiInfura(Blockchain blockchain) {
if (blockchain == Blockchain.EthereumTestNet) {
infuraApi = App.Companion.getNetworkComponent().getRetrofitInfuraTestnet().create(InfuraApi.class);
} else if (blockchain == Blockchain.TokenEmv) {
infuraApi = App.Companion.getNetworkComponent().getRetrofitInfuraRopsten().create(InfuraApi.class);
}
}

View file

@ -0,0 +1,53 @@
package com.tangem.data.network
import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import com.tangem.data.network.model.TokenEmvGetTransferFeeAnswer
import com.tangem.data.network.model.TokenEmvGetTransferFeeBody
import com.tangem.data.network.model.TokenEmvTransferAnswer
import com.tangem.data.network.model.TokenEmvTransferBody
import com.tangem.tangem_card.util.Log
import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import java.util.concurrent.TimeUnit
class ServerApiTokenEmv {
private val TAG = ServerApiTokenEmv::class.java.simpleName
private val tangemServer = "https://emvsupport.appspot.com/"
private val tokenEmvApi = Retrofit.Builder()
.baseUrl(tangemServer)
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(ScalarsConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) //logging for testing
.client(OkHttpClient.Builder().addInterceptor(
HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)
).build())
.build()
.create(TokenEmvApi::class.java)
fun transfer(tokenEmvTransferBody: TokenEmvTransferBody, transferObserver: SingleObserver<TokenEmvTransferAnswer>) {
Log.i(TAG, "new transfer request")
tokenEmvApi.transfer(tokenEmvTransferBody)
.timeout(30, TimeUnit.SECONDS)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(transferObserver)
}
fun getTransferFee(tokenEmvGetTransferFeeBody: TokenEmvGetTransferFeeBody, transferObserver: SingleObserver<TokenEmvGetTransferFeeAnswer>) {
Log.i(TAG, "new get transfer fee request")
tokenEmvApi.getTransferFee(tokenEmvGetTransferFeeBody)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(transferObserver)
}
}

View file

@ -5,6 +5,7 @@ class ServerURL {
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_INFURA_ROPSTEN = "https://ropsten.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

@ -0,0 +1,17 @@
package com.tangem.data.network
import com.tangem.data.network.model.TokenEmvGetTransferFeeAnswer
import com.tangem.data.network.model.TokenEmvGetTransferFeeBody
import com.tangem.data.network.model.TokenEmvTransferAnswer
import com.tangem.data.network.model.TokenEmvTransferBody
import io.reactivex.Completable
import io.reactivex.Single
import retrofit2.http.Body
import retrofit2.http.POST
interface TokenEmvApi {
@POST("./card/transfer")
fun transfer(@Body tokenEmvTransferBody: TokenEmvTransferBody): Single<TokenEmvTransferAnswer>
@POST("./card/transfer/fee")
fun getTransferFee(@Body tokenEmvGetTransferFeeBody: TokenEmvGetTransferFeeBody): Single<TokenEmvGetTransferFeeAnswer>
}

View file

@ -0,0 +1,36 @@
package com.tangem.data.network.model
import com.google.gson.annotations.SerializedName
data class TokenEmvTransferBody(
val CID: String,
val publicKey: String,
val amount: String,
val currency: String,
val recipient: String,
@SerializedName("fee_limit")
val feeLimit: String,
val sequence: Int,
val signature: String
)
data class TokenEmvTransferAnswer(
val error: String?,
val errorCode: Int?,
val success: Boolean?,
val tx_id: String?,
val blockchain_tx_id: String?
)
data class TokenEmvGetTransferFeeBody(
val CID: String,
val publicKey: String
)
data class TokenEmvGetTransferFeeAnswer(
val error: String?,
val errorCode: Int?,
val success: Boolean?,
val fee: String?,
val currency: String?
)

View file

@ -17,6 +17,9 @@ interface NetworkComponent {
@get:Named(Server.ApiInfuraTestnet.URL_INFURA_TESTNET)
val retrofitInfuraTestnet: Retrofit
@get:Named(Server.ApiInfuraRopsten.URL_INFURA_ROPSTEN)
val retrofitInfuraRopsten: Retrofit
@get:Named(Server.ApiMaticTesnet.URL_MATIC_TESTNET)
val retrofitMaticTesnet: Retrofit

View file

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

View file

@ -8,6 +8,7 @@ import android.nfc.Tag
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProviders
import androidx.navigation.findNavController
import com.scottyab.rootbeer.RootBeer
@ -16,6 +17,7 @@ import com.tangem.di.ToastHelper
import com.tangem.tangem_sdk.android.nfc.NfcLifecycleObserver
import com.tangem.tangem_sdk.android.reader.NfcManager
import com.tangem.ui.dialog.RootFoundDialog
import com.tangem.ui.fragment.MainFragment
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import javax.inject.Inject
@ -44,6 +46,8 @@ class MainActivity : AppCompatActivity(), NfcAdapter.ReaderCallback {
}
private fun navigateSafelyToMainFragment() {
if (getActiveFragment() is MainFragment) return
try {
findNavController(R.id.nav_host_fragment).popBackStack()
} catch (e: IllegalArgumentException) {
@ -73,8 +77,7 @@ class MainActivity : AppCompatActivity(), NfcAdapter.ReaderCallback {
}
override fun onTagDiscovered(tag: Tag) {
val activeFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
?.childFragmentManager?.primaryNavigationFragment
val activeFragment = getActiveFragment()
if (activeFragment is NfcAdapter.ReaderCallback) {
activeFragment.onTagDiscovered(tag)
} else {
@ -82,4 +85,8 @@ class MainActivity : AppCompatActivity(), NfcAdapter.ReaderCallback {
}
}
private fun getActiveFragment(): Fragment? {
return supportFragmentManager.findFragmentById(R.id.nav_host_fragment)
?.childFragmentManager?.primaryNavigationFragment
}
}

View file

@ -20,6 +20,7 @@ import com.tangem.wallet.rsk.RskEngine
import com.tangem.wallet.rsk.RskTokenEngine
import com.tangem.wallet.token.TokenEngine
import com.tangem.wallet.tezos.TezosEngine
import com.tangem.wallet.tokenEmv.TokenEmvEngine
import com.tangem.wallet.xlm.XlmAssetEngine
import com.tangem.wallet.xlm.XlmEngine
import com.tangem.wallet.xlmTag.XlmTagEngine
@ -60,6 +61,7 @@ object CoinEngineFactory {
Blockchain.Tezos -> TezosEngine()
Blockchain.BitcoinDual -> BtcMultisigEngine()
Blockchain.FlowDemo -> FlowDemoEngine()
Blockchain.TokenEmv -> TokenEmvEngine()
else -> null
}
}
@ -109,6 +111,8 @@ object CoinEngineFactory {
BtcMultisigEngine(context)
else if (Blockchain.FlowDemo == context.blockchain)
FlowDemoEngine(context)
else if (Blockchain.TokenEmv == context.blockchain)
TokenEmvEngine(context)
else
return null
} catch (e: Exception) {

View file

@ -110,7 +110,7 @@ public class EthTransaction {
return kec.digest(plainMsg);
}
public int BruteRecoveryID2(ECDSASignatureETH sig, byte[] messageHash, byte[] thisKey) {
public static int BruteRecoveryID2(ECDSASignatureETH sig, byte[] messageHash, byte[] thisKey) {
Log.e("ETH_KZ", BTCUtils.toHex(thisKey));
int recId = -1;
for (int i = 0; i < 4; i++) {

View file

@ -8,31 +8,46 @@ import com.tangem.wallet.eth.EthData;
public class TokenData extends EthData {
private CoinEngine.InternalAmount balanceAlter = null;
// private Integer sequence = null;
@Override
public void clearInfo() {
super.clearInfo();
balanceAlter = null;
// sequence = null;
}
public CoinEngine.InternalAmount getBalanceAlterInInternalUnits() {
return balanceAlter;
}
public void setBalanceAlterInInternalUnits(CoinEngine.InternalAmount value) {
balanceAlter = value;
}
// public Integer getSequence() {
// return sequence;
// }
//
// public void setSequence(Integer sequence) {
// this.sequence = sequence;
// }
@Override
public void loadFromBundle(Bundle B) {
super.loadFromBundle(B);
if( B.containsKey("BalanceDecimalAlter" )) {
if (B.containsKey("BalanceDecimalAlter")) {
balanceAlter = new CoinEngine.InternalAmount(B.getString("BalanceDecimalAlter"), "wei");
}else{
balanceAlter=null;
} else {
balanceAlter = null;
}
// if (B.containsKey("Sequence")) {
// sequence = B.getInt("Sequence");
// } else {
// sequence = null;
// }
}
@Override
@ -40,9 +55,12 @@ public class TokenData extends EthData {
super.saveToBundle(B);
try {
if( balanceAlter!=null ) {
if (balanceAlter != null) {
B.putString("BalanceDecimalAlter", balanceAlter.toString());
}
// if (sequence != null) {
// B.putInt("Sequence", sequence);
// }
} catch (Exception e) {
Log.e("Can't save to bundle ", e.getMessage());
}

View file

@ -0,0 +1,349 @@
package com.tangem.wallet.tokenEmv
import android.net.Uri
import android.text.InputFilter
import android.util.Log
import com.google.gson.Gson
import com.tangem.data.Blockchain
import com.tangem.data.network.ServerApiInfura
import com.tangem.data.network.ServerApiTokenEmv
import com.tangem.data.network.model.*
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_card.reader.CardProtocol.TangemException
import com.tangem.tangem_card.reader.TLV
import com.tangem.tangem_card.reader.TLVList
import com.tangem.tangem_card.tasks.SignTask
import com.tangem.tangem_card.util.Util
import com.tangem.util.DecimalDigitsInputFilter
import com.tangem.wallet.EthTransaction
import com.tangem.wallet.Keccak256
import com.tangem.wallet.R
import com.tangem.wallet.TangemContext
import com.tangem.wallet.token.TokenEngine
import io.reactivex.observers.DisposableSingleObserver
import org.apache.commons.lang3.SerializationUtils
import org.kethereum.extensions.toBytesPadded
import java.math.BigInteger
class TokenEmvEngine : TokenEngine {
constructor() : super()
constructor(context: TangemContext) : super(context)
private val TAG = TokenEmvEngine::class.java.simpleName
private fun hasLinkedContract(): Boolean {
return ctx.card.issuerData != null && ctx.card.issuerData.size == 44
}
override fun getBlockchain(): Blockchain {
return Blockchain.TokenEmv
}
override fun getChainIdNum(): Int {
return EthTransaction.ChainEnum.Ropsten.value
}
override fun getBalance(): Amount? {
return if (!hasBalanceInfo()) {
null
} else {
convertToAmount(coinData.balanceInInternalUnits)
}
}
override fun getBalanceHTML(): String? {
return if (hasLinkedContract()) {
if (balance != null) {
balance!!.toDescriptionString(tokenDecimals)
} else {
""
}
} else {
"NO LINKED CONTRACT"
}
}
override fun getBalanceCurrency(): String? {
return ctx.card.getTokenSymbol()
}
override fun getAmountInputFilters(): Array<InputFilter>? {
return arrayOf(DecimalDigitsInputFilter(tokenDecimals))
}
override fun getFeeCurrency(): String? {
return balanceCurrency
}
override fun isBalanceNotZero(): Boolean {
if (coinData == null) return false
return if (coinData.balanceInInternalUnits == null) {
false
} else {
coinData.balanceInInternalUnits.notZero()
}
}
override fun getBalanceEquivalent(): String? {
return ""
}
override fun evaluateFeeEquivalent(fee: String?): String? {
return ""
}
override fun defineWallet() {
try {
if (hasLinkedContract()) {
ctx.coinData.wallet = TLVList.fromBytes(ctx.card.issuerData).getTLV(TLV.Tag.TAG_Token_Contract_Address).asString
} else {
ctx.coinData.wallet = calculateAddress(ctx.card.walletPublicKey)
}
} catch (e: Exception) {
ctx.coinData.wallet = "ERROR"
throw TangemException("Can't define wallet address")
}
}
override fun hasBalanceInfo(): Boolean {
return coinData.balanceInInternalUnits != null
}
override fun getWalletExplorerUri(): Uri {
return Uri.parse("https://ropsten.etherscan.io/token/" + getContractAddress(ctx.card) + "?a=" + ctx.coinData.wallet)
}
override fun isExtractPossible(): Boolean {
if (!hasBalanceInfo()) {
ctx.setMessage(R.string.loaded_wallet_error_obtaining_blockchain_data)
} else if (!isBalanceNotZero) {
ctx.setMessage(R.string.general_wallet_empty)
} else if (awaitingConfirmation()) {
ctx.setMessage(R.string.loaded_wallet_message_wait)
} else {
return true
}
return false
}
override fun checkNewTransactionAmountAndFee(amount: Amount, fee: Amount?, isFeeIncluded: Boolean): Boolean {
try {
if (isFeeIncluded && (amount > balance || amount < fee)) return false
if (!isFeeIncluded && amount.add(fee) > balance) return false
} catch (e: NumberFormatException) {
e.printStackTrace()
}
return true
}
override fun constructTransaction(amountValue: Amount, feeValue: Amount?, IncFee: Boolean, targetAddress: String?): SignTask.TransactionToSign? {
val functionBytes = "transfer".toByteArray()
val contractHex = coinData.wallet.substring(2)
val contractBytes = Util.hexToBytes(contractHex)
val recipientHex = targetAddress!!.substring(2)
val recipientBytes = Util.hexToBytes(recipientHex)
val amountBytes = convertToInternalAmount(amountValue).toBigInteger().toBytesPadded(32)
val feeLimitBytes = convertToInternalAmount(feeValue).toBigInteger().toBytesPadded(32)
val sequence = ctx.card.SignedHashes
val sequenceBytes = sequence.toBigInteger().toBytesPadded(4)
val hashToSign = Keccak256().digest(contractBytes + functionBytes + amountBytes + recipientBytes + feeLimitBytes + sequenceBytes)
return object : SignTask.TransactionToSign {
override fun isSigningMethodSupported(signingMethod: TangemCard.SigningMethod): Boolean {
return signingMethod == TangemCard.SigningMethod.Sign_Hash
}
override fun getHashesToSign(): Array<ByteArray> {
return arrayOf(hashToSign)
}
@Throws(java.lang.Exception::class)
override fun getRawDataToSign(): ByteArray {
throw java.lang.Exception("Signing of raw transaction not supported for " + this.javaClass.simpleName)
}
@Throws(java.lang.Exception::class)
override fun getHashAlgToSign(): String {
throw java.lang.Exception("Signing of raw transaction not supported for " + this.javaClass.simpleName)
}
@Throws(java.lang.Exception::class)
override fun getIssuerTransactionSignature(dataToSignByIssuer: ByteArray): ByteArray {
throw java.lang.Exception("Transaction validation by issuer not supported in this version")
}
@Throws(java.lang.Exception::class)
override fun onSignCompleted(signFromCard: ByteArray): ByteArray {
// val r = BigInteger(1, Arrays.copyOfRange(signFromCard, 0, 32))
// var s: BigInteger? = BigInteger(1, Arrays.copyOfRange(signFromCard, 32, 64))
// s = CryptoUtil.toCanonicalised(s)
//
// val publicKey = ctx.getCard().getWalletPublicKey()
//
// val verified = ECKey.verify(hashToSign, ECKey.ECDSASignature(r, s), publicKey)
// if (!verified) {
// Log.e(this.javaClass.simpleName + "-CHECK", "sign Failed.")
// }
//
// val v = BruteRecoveryID2(ECDSASignatureETH(r, s), hashToSign, publicKey)
// if (v != 27 && v != 28) {
// Log.e(TAG, "invalid v")
// throw java.lang.Exception("Error in " + this.javaClass.simpleName + " - invalid v")
// }
// Log.e(TAG, this.javaClass.simpleName + " V: " + v.toString())
//
// var rBytes = r.toByteArray()
// if (rBytes.size == 33) {
// rBytes = rBytes.copyOfRange(1,33)
// }
// val sBytes = s.toByteArray()
//
// val tokenEmvTransferBody = TokenEmvTransferBody(
// CID = Util.bytesToHex(ctx.card.cid),
// publicKey = Util.bytesToHex(ctx.card.walletPublicKey),
// contract = contractHex,
// amount = Util.byteArrayToHexString(amountBytes),
// recipient = recipientHex,
// feeLimit = Util.byteArrayToHexString(feeLimitBytes),
// sequence = sequence,
// r = Util.byteArrayToHexString(rBytes),
// s = Util.byteArrayToHexString(sBytes),
// v = v
// )
val tokenEmvTransferBody = TokenEmvTransferBody(
CID = Util.bytesToHex(ctx.card.cid),
publicKey = Util.bytesToHex(ctx.card.walletPublicKey),
amount = amountValue.toValueString(),
currency = amountValue.currency,
recipient = targetAddress,
feeLimit = feeValue!!.toValueString(),
sequence = sequence,
signature = Util.bytesToHex(signFromCard)
)
val jsonBody = Gson().toJson(tokenEmvTransferBody)
val serializedBody = SerializationUtils.serialize(jsonBody)
notifyOnNeedSendTransaction(serializedBody)
return serializedBody
}
}
}
override fun requestBalanceAndUnspentTransactions(blockchainRequestsCallbacks: BlockchainRequestsCallbacks) {
val serverApiInfura = ServerApiInfura(ctx.blockchain)
val responseListener: ServerApiInfura.ResponseListener = object : ServerApiInfura.ResponseListener {
override fun onSuccess(method: String, infuraResponse: InfuraResponse) {
try {
var balanceCap = infuraResponse.result
balanceCap = balanceCap!!.substring(2)
val l = BigInteger(balanceCap, 16)
coinData.balanceInInternalUnits = InternalAmount(l, ctx.card.tokenSymbol)
coinData.isBalanceReceived = true
// Log.i("$TAG eth_call", balanceCap)
} catch (e: java.lang.Exception) {
onFail(method, e.message ?: "invalid response")
}
blockchainRequestsCallbacks.onComplete(true)
}
override fun onFail(method: String, message: String) {
Log.e(TAG, "onFail: $method $message")
ctx.error = message
blockchainRequestsCallbacks.onComplete(false)
}
}
serverApiInfura.setResponseListener(responseListener)
if (validateAddress(getContractAddress(ctx.card))) {
serverApiInfura.requestData(ServerApiInfura.INFURA_ETH_CALL, 67, coinData.wallet, getContractAddress(ctx.card), "")
} else {
ctx.error = "Smart contract address not defined"
blockchainRequestsCallbacks.onComplete(false)
}
}
override fun requestFee(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, targetAddress: String?, amount: Amount) {
val tokenEmvTransferBody = TokenEmvGetTransferFeeBody(
CID = Util.bytesToHex(ctx.card.cid),
publicKey = Util.bytesToHex(ctx.card.walletPublicKey)
)
val observer = object : DisposableSingleObserver<TokenEmvGetTransferFeeAnswer>() {
override fun onError(e: Throwable) {
ctx.error = "Can't get transfer fee, ${e.message}"
blockchainRequestsCallbacks.onComplete(false)
}
override fun onSuccess(t: TokenEmvGetTransferFeeAnswer) {
if (t.success != null && t.success) {
ctx.error = null
coinData.minFee = Amount(t.fee, t.currency)
coinData.normalFee = Amount(t.fee, t.currency)
coinData.maxFee = Amount(t.fee, t.currency)
blockchainRequestsCallbacks.onComplete(true)
} else {
if (t.error != null) {
ctx.error = t.error
} else {
ctx.error = "Can't get transfer fee, code ${t.errorCode}"
}
blockchainRequestsCallbacks.onComplete(false)
}
}
}
ServerApiTokenEmv().getTransferFee(tokenEmvTransferBody, observer)
blockchainRequestsCallbacks.onComplete(true)
}
override fun requestSendTransaction(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, txForSend: ByteArray?) {
val jsonBody = SerializationUtils.deserialize<String>(txForSend)
Log.e(TAG, jsonBody)
val tokenEmvTransferBody = Gson().fromJson(jsonBody, TokenEmvTransferBody::class.java)
val observer = object : DisposableSingleObserver<TokenEmvTransferAnswer>() {
override fun onError(e: Throwable) {
ctx.error = "Can't send transfer, ${e.message}"
blockchainRequestsCallbacks.onComplete(false)
}
override fun onSuccess(t: TokenEmvTransferAnswer) {
if (t.success != null && t.success) {
ctx.error = null
blockchainRequestsCallbacks.onComplete(true)
} else {
if (t.error != null) {
ctx.error = t.error
} else {
ctx.error = "Can't send transfer, code ${t.errorCode}"
}
blockchainRequestsCallbacks.onComplete(false)
}
}
}
ServerApiTokenEmv().transfer(tokenEmvTransferBody, observer)
}
override fun allowSelectFeeLevel(): Boolean {
return false
}
override fun pendingTransactionTimeoutInSeconds(): Int {
return 60
}
}

View file

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

View file

@ -74,7 +74,10 @@ class CardSession(
}
runnable.run(this) { result ->
stop()
when (result) {
is CompletionResult.Success -> stop()
is CompletionResult.Failure -> stopWithError(result.error)
}
callback(result)
}
}
@ -99,7 +102,6 @@ class CardSession(
}
is CompletionResult.Success -> {
callback(this, null)
}
}
}

View file

@ -91,7 +91,7 @@ abstract class Command<T : CommandResponse> : CardSessionRunnable<T> {
}
}
is CompletionResult.Failure ->
if (result.error == SessionError.TagLost()) {
if (result.error is SessionError.TagLost) {
session.viewDelegate.onTagLost()
} else {
callback(CompletionResult.Failure(result.error))

View file

@ -14,67 +14,50 @@ import java.util.*
/**
* Determines which type of data is required for signing.
*/
data class SigningMethod(val rawValue: Int) {
data class SigningMethodMask(val rawValue: Int) {
fun contains(value: Int): Boolean {
fun contains(signingMethod: SigningMethod): Boolean {
return if (rawValue and 0x80 == 0) {
value == rawValue
signingMethod.code == rawValue
} else {
rawValue and (0x01 shl value) != 0
rawValue and (0x01 shl signingMethod.code) != 0
}
}
}
companion object {
const val signHash = 0
const val signRaw = 1
const val signHashValidatedByIssuer = 2
const val signRawValidatedByIssuer = 3
const val signHashValidatedByIssuerAndWriteIssuerData = 4
const val signRawValidatedByIssuerAndWriteIssuerData = 5
const val signPos = 6
enum class SigningMethod(val code: Int) {
SignHash(0),
SignRaw(1),
SignHashValidateByIssuer(2),
SignRawValidateByIssuer(3),
SignHashValidateByIssuerWriteIssuerData(4),
SignRawValidateByIssuerWriteIssuerData(5),
SignPos(6)
}
fun build(
signHash: Boolean = false,
signRaw: Boolean = false,
signHashValidatedByIssuer: Boolean = false,
signRawValidatedByIssuer: Boolean = false,
signHashValidatedByIssuerAndWriteIssuerData: Boolean = false,
signRawValidatedByIssuerAndWriteIssuerData: Boolean = false,
signPos: Boolean = false
class SigningMethodMaskBuilder() {
): SigningMethod {
fun Boolean.toInt() = if (this) 1 else 0
private val signingMethods = mutableSetOf<SigningMethod>()
val signingMethodsCount = 0 +
signHash.toInt() +
signRaw.toInt() +
signHashValidatedByIssuer.toInt() +
signRawValidatedByIssuer.toInt() +
signHashValidatedByIssuerAndWriteIssuerData.toInt() +
signRawValidatedByIssuerAndWriteIssuerData.toInt() +
signPos.toInt()
fun add(signingMethod: SigningMethod) {
signingMethods.add(signingMethod)
}
var signingMethod: Int = 0
if (signingMethodsCount == 1) {
if (signHash) signingMethod += SigningMethod.signHash
if (signRaw) signingMethod += SigningMethod.signRaw
if (signHashValidatedByIssuer) signingMethod += SigningMethod.signHashValidatedByIssuer
if (signRawValidatedByIssuer) signingMethod += SigningMethod.signRawValidatedByIssuer
if (signHashValidatedByIssuerAndWriteIssuerData) signingMethod += SigningMethod.signHashValidatedByIssuerAndWriteIssuerData
if (signRawValidatedByIssuerAndWriteIssuerData) signingMethod += SigningMethod.signRawValidatedByIssuerAndWriteIssuerData
if (signPos) signingMethod += SigningMethod.signPos
} else if (signingMethodsCount > 1) {
signingMethod = 0x80
if (signHash) signingMethod += 0x01
if (signRaw) signingMethod += 0x01 shl SigningMethod.signRaw
if (signHashValidatedByIssuer) signingMethod += 0x01 shl SigningMethod.signHashValidatedByIssuer
if (signRawValidatedByIssuer) signingMethod += 0x01 shl SigningMethod.signRawValidatedByIssuer
if (signHashValidatedByIssuerAndWriteIssuerData) signingMethod += 0x01 shl SigningMethod.signHashValidatedByIssuerAndWriteIssuerData
if (signRawValidatedByIssuerAndWriteIssuerData) signingMethod += 0x01 shl SigningMethod.signRawValidatedByIssuerAndWriteIssuerData
if (signPos) signingMethod += 0x01 shl SigningMethod.signPos
fun build(): SigningMethodMask {
val rawValue: Int = when {
signingMethods.count() == 0 -> {
0
}
signingMethods.count() == 1 -> {
signingMethods.iterator().next().code
}
else -> {
signingMethods.fold(
0x80, { acc, singingMethod -> acc + (0x01 shl singingMethod.code) }
)
}
return SigningMethod(signingMethod)
}
return SigningMethodMask(rawValue)
}
}
@ -113,22 +96,23 @@ enum class CardStatus(val code: Int) {
*/
data class ProductMask(val rawValue: Int) {
fun contains(value: Int): Boolean = (rawValue and value) != 0
fun contains(product: Product): Boolean = (rawValue and product.code) != 0
companion object {
const val note = 0x01
const val tag = 0x02
const val idCard = 0x04
const val idIssuer = 0x08
}
}
enum class Product(val code: Int) {
Note(0x01),
Tag(0x02),
IdCard(0x04),
IdIssuer(0x08)
}
class ProductMaskBuilder() {
private var productMaskValue = 0
fun add(productCode: Int) {
productMaskValue = productMaskValue or productCode
fun add(product: Product) {
productMaskValue = productMaskValue or product.code
}
fun build() = ProductMask(productMaskValue)
@ -299,7 +283,7 @@ class Card(
/**
* Defines what data should be submitted to SIGN command.
*/
val signingMethod: SigningMethod?,
val signingMethods: SigningMethodMask?,
/**
* Delay in seconds before COS executes commands protected by PIN2.
@ -415,7 +399,7 @@ class ReadCommand : Command<Card>() {
issuerPublicKey = decoder.decodeOptional(TlvTag.IssuerDataPublicKey),
curve = decoder.decodeOptional(TlvTag.CurveId),
maxSignatures = decoder.decodeOptional(TlvTag.MaxSignatures),
signingMethod = decoder.decodeOptional(TlvTag.SigningMethod),
signingMethods = decoder.decodeOptional(TlvTag.SigningMethod),
pauseBeforePin2 = decoder.decodeOptional(TlvTag.PauseBeforePin2),
walletPublicKey = decoder.decodeOptional(TlvTag.WalletPublicKey),
walletRemainingSignatures = decoder.decodeOptional(TlvTag.RemainingSignatures),

View file

@ -60,7 +60,7 @@ class PersonalizeCommand(
issuerPublicKey = decoder.decodeOptional(TlvTag.IssuerDataPublicKey),
curve = decoder.decodeOptional(TlvTag.CurveId),
maxSignatures = decoder.decodeOptional(TlvTag.MaxSignatures),
signingMethod = decoder.decodeOptional(TlvTag.SigningMethod),
signingMethods = decoder.decodeOptional(TlvTag.SigningMethod),
pauseBeforePin2 = decoder.decodeOptional(TlvTag.PauseBeforePin2),
walletPublicKey = decoder.decodeOptional(TlvTag.WalletPublicKey),
walletRemainingSignatures = decoder.decodeOptional(TlvTag.RemainingSignatures),
@ -105,7 +105,7 @@ class PersonalizeCommand(
tlvBuilder.append(TlvTag.CardId, cardId)
tlvBuilder.append(TlvTag.CurveId, config.curveID)
tlvBuilder.append(TlvTag.MaxSignatures, config.maxSignatures)
tlvBuilder.append(TlvTag.SigningMethod, config.signingMethod)
tlvBuilder.append(TlvTag.SigningMethod, config.signingMethods)
tlvBuilder.append(TlvTag.SettingsMask, config.createSettingsMask())
tlvBuilder.append(TlvTag.PauseBeforePin2, config.pauseBeforePin2 / 10)
tlvBuilder.append(TlvTag.Cvc, config.cvc.toByteArray())

View file

@ -2,7 +2,7 @@ package com.tangem.commands.personalization.entities
import com.tangem.commands.CardData
import com.tangem.commands.EllipticCurve
import com.tangem.commands.SigningMethod
import com.tangem.commands.SigningMethodMask
data class NdefRecord(
val type: Type,
@ -33,7 +33,7 @@ data class CardConfig(
val pauseBeforePin2: Int,
val smartSecurityDelay: Boolean,
val curveID: EllipticCurve,
val signingMethod: SigningMethod,
val signingMethods: SigningMethodMask,
val maxSignatures: Int,
val isReusable: Boolean,
val allowSwapPin: Boolean,

View file

@ -122,9 +122,9 @@ class TlvDecoder(val tlvList: List<Tlv>) {
}
}
TlvValueType.SigningMethod -> {
typeCheck<T, SigningMethod>(tag)
typeCheck<T, SigningMethodMask>(tag)
try {
SigningMethod(tlvValue.toInt()) as T
SigningMethodMask(tlvValue.toInt()) as T
} catch (exception: Exception) {
logException(tag, tlvValue.toInt().toString(), exception)
throw SessionError.DecodingFailed()

View file

@ -87,8 +87,8 @@ class TlvEncoder {
(value as CardStatus).code.toByteArray()
}
TlvValueType.SigningMethod -> {
typeCheck<T, SigningMethod>(tag)
byteArrayOf((value as SigningMethod).rawValue.toByte())
typeCheck<T, SigningMethodMask>(tag)
byteArrayOf((value as SigningMethodMask).rawValue.toByte())
}
TlvValueType.IssuerDataMode -> {
typeCheck<T, IssuerDataMode>(tag)

View file

@ -20,7 +20,7 @@ internal class ScanTask : CardSessionRunnable<Card> {
if (card == null) {
callback(CompletionResult.Failure(SessionError.MissingPreflightRead()))
} else if (card.cardData?.productMask?.contains(ProductMask.tag) != false) {
} else if (card.cardData?.productMask?.contains(Product.Tag) != false) {
callback(CompletionResult.Success(card))
} else if (card.status != CardStatus.Loaded) {

View file

@ -82,8 +82,8 @@ class TlvDecoderTest {
@Test
fun `map SigningMethods single value returns correct value`() {
val signingMethods: SigningMethod = tlvMapper.decode(TlvTag.SigningMethod)
assertThat(signingMethods.contains(SigningMethod.signHash))
val signingMethods: SigningMethodMask = tlvMapper.decode(TlvTag.SigningMethod)
assertThat(signingMethods.contains(SigningMethod.SignHash))
.isTrue()
}
@ -91,20 +91,20 @@ class TlvDecoderTest {
fun `map SigningMethods set of methods returns correct value`() {
val localMapper = TlvDecoder(Tlv.deserialize("070195".hexToBytes())!!)
val signingMethod: SigningMethod = localMapper.decode(TlvTag.SigningMethod)
assertThat(signingMethod.contains(SigningMethod.signHash))
val signingMethods: SigningMethodMask = localMapper.decode(TlvTag.SigningMethod)
assertThat(signingMethods.contains(SigningMethod.SignHash))
.isTrue()
assertThat(signingMethod.contains(SigningMethod.signHashValidatedByIssuer))
assertThat(signingMethods.contains(SigningMethod.SignHashValidateByIssuer))
.isTrue()
assertThat(signingMethod.contains(SigningMethod.signHashValidatedByIssuerAndWriteIssuerData))
assertThat(signingMethods.contains(SigningMethod.SignHashValidateByIssuerWriteIssuerData))
.isTrue()
assertThat(signingMethod.contains(SigningMethod.signRaw))
assertThat(signingMethods.contains(SigningMethod.SignRaw))
.isFalse()
assertThat(signingMethod.contains(SigningMethod.signRawValidatedByIssuer))
assertThat(signingMethods.contains(SigningMethod.SignRawValidateByIssuer))
.isFalse()
assertThat(signingMethod.contains(SigningMethod.signRawValidatedByIssuerAndWriteIssuerData))
assertThat(signingMethods.contains(SigningMethod.SignRawValidateByIssuerWriteIssuerData))
.isFalse()
assertThat(signingMethod.contains(SigningMethod.signPos))
assertThat(signingMethods.contains(SigningMethod.SignPos))
.isFalse()
}
@ -119,7 +119,7 @@ class TlvDecoderTest {
fun `map ProductMask with raw value 5 returns correct value`() {
val localMapper = TlvDecoder(listOf(Tlv(TlvTag.ProductMask, byteArrayOf(5))))
val productMask: ProductMask = localMapper.decode(TlvTag.ProductMask)
assertThat(productMask.contains(ProductMask.note) && productMask.contains(ProductMask.idCard))
assertThat(productMask.contains(Product.Note) && productMask.contains(Product.IdCard))
.isTrue()
}
@ -127,7 +127,7 @@ class TlvDecoderTest {
fun `map ProductMask with raw value 1 returns correct value`() {
val localMapper = TlvDecoder(listOf(Tlv(TlvTag.ProductMask, byteArrayOf(1))))
val productMask: ProductMask = localMapper.decode(TlvTag.ProductMask)
assertThat(productMask.contains(ProductMask.note))
assertThat(productMask.contains(Product.Note))
.isTrue()
}

View file

@ -11,24 +11,38 @@ fun CardConfig.Companion.create(application: Application): CardConfig {
val preferences = application.getSharedPreferences("prefs", Context.MODE_PRIVATE)
val signingMethod = SigningMethod.build(
signHash = preferences.getBoolean("personalization_SigningMethod_0", false),
signRaw = preferences.getBoolean("personalization_SigningMethod_1", false),
signHashValidatedByIssuer = preferences.getBoolean("personalization_SigningMethod_2", false),
signRawValidatedByIssuer = preferences.getBoolean("personalization_SigningMethod_3", false),
signHashValidatedByIssuerAndWriteIssuerData = preferences.getBoolean("personalization_SigningMethod_4", false),
signRawValidatedByIssuerAndWriteIssuerData = preferences.getBoolean("personalization_SigningMethod_5", false),
signPos = preferences.getBoolean("personalization_SigningMethod_6", false)
)
val signingMethodMaskBuilder = SigningMethodMaskBuilder()
if (preferences.getBoolean("personalization_SigningMethod_0", false)) {
signingMethodMaskBuilder.add(SigningMethod.SignHash)
}
if (preferences.getBoolean("personalization_SigningMethod_1", false)) {
signingMethodMaskBuilder.add(SigningMethod.SignRaw)
}
if (preferences.getBoolean("personalization_SigningMethod_2", false)) {
signingMethodMaskBuilder.add(SigningMethod.SignHashValidateByIssuer)
}
if (preferences.getBoolean("personalization_SigningMethod_3", false)) {
signingMethodMaskBuilder.add(SigningMethod.SignRawValidateByIssuer)
}
if (preferences.getBoolean("personalization_SigningMethod_4", false)) {
signingMethodMaskBuilder.add(SigningMethod.SignHashValidateByIssuerWriteIssuerData)
}
if (preferences.getBoolean("personalization_SigningMethod_5", false)) {
signingMethodMaskBuilder.add(SigningMethod.SignRawValidateByIssuerWriteIssuerData)
}
if (preferences.getBoolean("personalization_SigningMethod_6", false)) {
signingMethodMaskBuilder.add(SigningMethod.SignHash)
}
val signingMethod = signingMethodMaskBuilder.build()
val isNote = preferences.getBoolean("personalization_ProductMask_IsNote", true)
val isTag = preferences.getBoolean("personalization_ProductMask_IsTag", false)
val isIdCard = preferences.getBoolean("personalization_ProductMask_IsIDCard", false)
val productMaskBuilder = ProductMaskBuilder()
if (isNote) productMaskBuilder.add(ProductMask.note)
if (isTag) productMaskBuilder.add(ProductMask.tag)
if (isIdCard) productMaskBuilder.add(ProductMask.idCard)
if (isNote) productMaskBuilder.add(Product.Note)
if (isTag) productMaskBuilder.add(Product.Tag)
if (isIdCard) productMaskBuilder.add(Product.IdCard)
val productMask = productMaskBuilder.build()
var tokenSymbol: String? = null
@ -80,7 +94,7 @@ fun CardConfig.Companion.create(application: Application): CardConfig {
cardData = cardData,
curveID = EllipticCurve.byName(preferences.getString("personalization_CurveId", "secp256k1")!!)
?: EllipticCurve.Secp256k1,
signingMethod = signingMethod,
signingMethods = signingMethod,
createWallet = preferences.getBoolean("personalization_CreateWallet", true),
maxSignatures = preferences.getString("personalization_MaxSignatures", "1000")!!.toInt(),
isReusable = preferences.getBoolean("personalization_SettingsMask_IsReusable", true),

View file

@ -1,8 +1,6 @@
package com.tangem.tangemtest.ucase.variants.personalize.converter
import com.tangem.commands.CardData
import com.tangem.commands.EllipticCurve
import com.tangem.commands.ProductMaskBuilder
import com.tangem.commands.*
import com.tangem.commands.personalization.entities.CardConfig
import com.tangem.commands.personalization.entities.NdefRecord
import com.tangem.tangemtest.ucase.variants.personalize.dto.PersonalizationConfig
@ -12,15 +10,29 @@ import java.util.*
class PersonalizationConfigToCardConfig : Converter<PersonalizationConfig, CardConfig> {
override fun convert(from: PersonalizationConfig): CardConfig {
val signingMethod = com.tangem.commands.SigningMethod.build(
signHash = from.SigningMethod0,
signRaw = from.SigningMethod1,
signHashValidatedByIssuer = from.SigningMethod2,
signRawValidatedByIssuer = from.SigningMethod3,
signHashValidatedByIssuerAndWriteIssuerData = from.SigningMethod4,
signRawValidatedByIssuerAndWriteIssuerData = from.SigningMethod5,
signPos = from.SigningMethod6
)
val signingMethodMaskBuilder = SigningMethodMaskBuilder()
if (from.SigningMethod0) {
signingMethodMaskBuilder.add(SigningMethod.SignHash)
}
if (from.SigningMethod1) {
signingMethodMaskBuilder.add(SigningMethod.SignRaw)
}
if (from.SigningMethod2) {
signingMethodMaskBuilder.add(SigningMethod.SignHashValidateByIssuer)
}
if (from.SigningMethod3) {
signingMethodMaskBuilder.add(SigningMethod.SignRawValidateByIssuer)
}
if (from.SigningMethod4) {
signingMethodMaskBuilder.add(SigningMethod.SignHashValidateByIssuerWriteIssuerData)
}
if (from.SigningMethod5) {
signingMethodMaskBuilder.add(SigningMethod.SignRawValidateByIssuerWriteIssuerData)
}
if (from.SigningMethod6) {
signingMethodMaskBuilder.add(SigningMethod.SignHash)
}
val signingMethod = signingMethodMaskBuilder.build()
val isNote = from.cardData.product_note
val isTag = from.cardData.product_tag
@ -28,10 +40,10 @@ class PersonalizationConfigToCardConfig : Converter<PersonalizationConfig, CardC
val isIdIssuer = from.cardData.product_id_issuer
val productMaskBuilder = ProductMaskBuilder()
if (isNote) productMaskBuilder.add(com.tangem.commands.ProductMask.note)
if (isTag) productMaskBuilder.add(com.tangem.commands.ProductMask.tag)
if (isIdCard) productMaskBuilder.add(com.tangem.commands.ProductMask.idCard)
if (isIdIssuer) productMaskBuilder.add(com.tangem.commands.ProductMask.idIssuer)
if (isNote) productMaskBuilder.add(com.tangem.commands.Product.Note)
if (isTag) productMaskBuilder.add(com.tangem.commands.Product.Tag)
if (isIdCard) productMaskBuilder.add(com.tangem.commands.Product.IdCard)
if (isIdIssuer) productMaskBuilder.add(com.tangem.commands.Product.IdIssuer)
val productMask = productMaskBuilder.build()
var tokenSymbol: String? = null