Updated on 2026-08-14

This commit is contained in:
Tangem 2020-04-09 14:53:21 +03:00
parent 9082083c95
commit bf1c374582
9 changed files with 412 additions and 7 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

@ -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;

View file

@ -0,0 +1,43 @@
package com.tangem.data.network
import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import com.tangem.data.network.model.TezosAccountResponse
import com.tangem.data.network.model.TokenEmvTransferBody
import com.tangem.tangem_card.util.Log
import io.reactivex.CompletableObserver
import io.reactivex.Single
import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.BiConsumer
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
class ServerApiTokenEmv {
private val TAG = ServerApiTokenEmv::class.java.simpleName
private val tangemServer = ""
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: CompletableObserver) {
Log.i(TAG, "new transfer request")
tokenEmvApi.transfer(tokenEmvTransferBody)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(transferObserver)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.data.network
import com.tangem.data.network.model.TokenEmvTransferBody
import io.reactivex.Completable
import retrofit2.http.Body
import retrofit2.http.POST
interface TokenEmvApi {
@POST("./")
fun transfer(@Body tokenEmvTransferBody: TokenEmvTransferBody): Completable
}

View file

@ -0,0 +1,15 @@
package com.tangem.data.network.model
import com.google.gson.annotations.SerializedName
data class TokenEmvTransferBody(
val contract: String,
val amount: String,
val recipient: String,
@SerializedName("fee_limit")
val feeLimit: String,
val sequence: Int,
val r: String,
val s: String,
val v: Int
)

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,30 +8,45 @@ 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;
}
}
@ -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,310 @@
package com.tangem.wallet.tokenEmv
import android.text.InputFilter
import android.util.Log
import com.google.gson.Gson
import com.tangem.data.Blockchain
import com.tangem.data.network.ServerApiTokenEmv
import com.tangem.data.network.model.TokenEmvTransferBody
import com.tangem.tangem_card.data.TangemCard
import com.tangem.tangem_card.reader.CardProtocol.TangemException
import com.tangem.tangem_card.tasks.SignTask
import com.tangem.tangem_card.util.Util
import com.tangem.util.CryptoUtil
import com.tangem.util.DecimalDigitsInputFilter
import com.tangem.wallet.*
import com.tangem.wallet.EthTransaction.BruteRecoveryID2
import com.tangem.wallet.token.TokenEngine
import io.reactivex.observers.DisposableCompletableObserver
import org.apache.commons.lang3.SerializationUtils
import org.bitcoinj.core.ECKey
import org.kethereum.extensions.toBytesPadded
import org.kethereum.extensions.toFixedLengthByteArray
import java.math.BigInteger
import java.util.*
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 == 20
}
override fun getBlockchain(): Blockchain {
return Blockchain.TokenEmv
}
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 = String.format("0x%s", BTCUtils.toHex(ctx.card.issuerData))
} 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 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 sequenceBytes = coinData.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(
contract = contractHex,
amount = Util.byteArrayToHexString(amountBytes),
recipient = recipientHex,
feeLimit = Util.byteArrayToHexString(feeLimitBytes),
sequence = coinData.sequence,
r = Util.byteArrayToHexString(rBytes),
s = Util.byteArrayToHexString(sBytes),
v = v
)
val jsonBody = Gson().toJson(tokenEmvTransferBody)
val serializedBody = SerializationUtils.serialize(jsonBody)
notifyOnNeedSendTransaction(serializedBody)
return serializedBody
}
}
}
override fun requestSendTransaction(blockchainRequestsCallbacks: BlockchainRequestsCallbacks, txForSend: ByteArray?) {
val jsonBody = SerializationUtils.deserialize<String>(txForSend)
val tokenEmvTransferBody = Gson().fromJson(jsonBody, TokenEmvTransferBody::class.java)
val transferObserver = object : DisposableCompletableObserver() {
override fun onComplete() {
blockchainRequestsCallbacks.onComplete(true)
}
override fun onError(e: Throwable) {
blockchainRequestsCallbacks.onComplete(false)
}
}
ServerApiTokenEmv().transfer(tokenEmvTransferBody, transferObserver)
}
// TODO: move all below to the server
fun constructTransfer(tokenEmvTransferBody: TokenEmvTransferBody, feeValue: Amount?): SignTask.TransactionToSign {
val contractAddress = tokenEmvTransferBody.contract
val nonceValue = coinData.confirmedTXCount
val weiFee: BigInteger = convertToInternalAmount(feeValue).toBigIntegerExact() //TODO: get fee as usual but set multiplier m = BigInteger.valueOf(100000), use normal?
var gasLimitInt = 100000
val gasPrice = weiFee.divide(BigInteger.valueOf(gasLimitInt.toLong()))
val gasLimit = BigInteger.valueOf(gasLimitInt.toLong())
val chainId = this.chainIdNum
val amountZero = BigInteger.ZERO
val transferSignature = "transfer(uint256,address,uint8,bytes32,bytes32,uint256,uint256,uint32)".toByteArray()
val selector = Keccak256().digest(transferSignature).copyOf(4)
val tokenAmount = Util.hexToBytes(tokenEmvTransferBody.amount).toFixedLengthByteArray(32)
val recipient = Util.hexToBytes(tokenEmvTransferBody.recipient).toFixedLengthByteArray(32)
val sigV = BigInteger.valueOf(tokenEmvTransferBody.v.toLong()).toBytesPadded(32)
val sigR = Util.hexToBytes(tokenEmvTransferBody.r).toFixedLengthByteArray(32)
val sigS = Util.hexToBytes(tokenEmvTransferBody.s).toFixedLengthByteArray(32)
val tokenFee = BigInteger.valueOf(1).toBytesPadded(32) //TODO: calculate using equivalent data
val tokenFeeLimit = Util.hexToBytes(tokenEmvTransferBody.feeLimit).toFixedLengthByteArray(32)
val sequence = BigInteger.valueOf(tokenEmvTransferBody.sequence.toLong()).toBytesPadded(32)
val data = selector + tokenAmount + recipient + sigV + sigR + sigS + tokenFee + tokenFeeLimit + sequence
val tx = EthTransaction.create(contractAddress, amountZero, nonceValue, gasPrice, gasLimit, chainId, data)
return object : SignTask.TransactionToSign {
override fun isSigningMethodSupported(signingMethod: TangemCard.SigningMethod): Boolean {
return signingMethod == TangemCard.SigningMethod.Sign_Hash
}
override fun getHashesToSign(): Array<ByteArray> {
return arrayOf(tx.rawHash)
}
@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 publicKey = ctx.getCard().getWalletPublicKey()
val for_hash = tx.rawHash
val r = BigInteger(1, Arrays.copyOfRange(signFromCard, 0, 32))
var s: BigInteger? = BigInteger(1, Arrays.copyOfRange(signFromCard, 32, 64))
s = CryptoUtil.toCanonicalised(s)
val f = ECKey.verify(for_hash, ECKey.ECDSASignature(r, s), publicKey)
if (!f) {
Log.e(this.javaClass.simpleName + "-CHECK", "sign Failed.")
}
tx.signature = ECDSASignatureETH(r, s)
val v = BruteRecoveryID2(tx.signature, for_hash, publicKey)
if (v != 27 && v != 28) {
Log.e(TAG, "invalid v")
throw java.lang.Exception("Error in " + this.javaClass.simpleName + " - invalid v")
}
tx.signature.v = v.toByte()
Log.e(TAG, this.javaClass.simpleName + " V: " + v.toString())
val txForSend = tx.encoded
notifyOnNeedSendTransaction(txForSend)
return txForSend
}
}
}
}