diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index 92db5c5d43..f51ab87524 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -23,11 +23,12 @@ public enum Blockchain { BinanceTestNet("BINANCE/test", "BNB", 100000000.0, R.drawable.tangem2, "Binance Testnet"), Matic("MATIC", "MTX", 1.0, R.drawable.tangem2, "Matic"), MaticTestNet("MATIC/test", "MTX", 1.0, R.drawable.tangem2, "Matic Testnet"), - Stellar("XLM", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar"), - StellarTestNet("XLM/test", "XLM", 1000000.0, R.drawable.ic_logo_stellar, "Stellar Testnet"), + Stellar("XLM", "XLM", 10000000.0, R.drawable.ic_logo_stellar, "Stellar"), + StellarTestNet("XLM/test", "XLM", 10000000.0, R.drawable.ic_logo_stellar, "Stellar Testnet"), StellarAsset("Asset", "XLM", 10000000.0, R.drawable.ic_logo_stellar, "Stellar"), Eos("EOS", "EOS", 10000.0, R.drawable.tangem2, "EOS"), - Ducatus("DUC", "DUC", 100000000.0, R.drawable.tangem2, "Ducatus"); + Ducatus("DUC", "DUC", 100000000.0, R.drawable.tangem2, "Ducatus"), + Tezos("TEZOS", "XTZ", 10000000.0, R.drawable.tangem2, "Tezos"); Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) { mID = ID; diff --git a/app/src/main/java/com/tangem/data/network/ServerApiTezos.java b/app/src/main/java/com/tangem/data/network/ServerApiTezos.java new file mode 100644 index 0000000000..b5300912ca --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/ServerApiTezos.java @@ -0,0 +1,139 @@ +package com.tangem.data.network; + +import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; +import com.tangem.data.network.model.TezosAccountResponse; +import com.tangem.data.network.model.TezosForgeBody; +import com.tangem.data.network.model.TezosHeaderResponse; +import com.tangem.data.network.model.TezosPreapplyBody; +import com.tangem.tangem_card.util.Log; + +import java.util.ArrayList; +import java.util.List; + +import io.reactivex.Single; +import io.reactivex.SingleObserver; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.converter.scalars.ScalarsConverterFactory; + +public class ServerApiTezos { + private static String TAG = ServerApiTezos.class.getSimpleName(); + + private final String letzbakeURI = "https://teznode.letzbake.com"; + private final String tezrpcURI = "https://mainnet.tezrpc.me"; + + static final String TEZOS_ADDRESS = "chains/main/blocks/head/context/contracts/{address}"; + static final String TEZOS_HEADER = "chains/main/blocks/head/header"; + static final String TEZOS_MANAGER_KEY = "chains/main/blocks/head/context/contracts/{address}/manager_key"; + static final String TEZOS_FORGE_OPERATIONS = "chains/main/blocks/head/helpers/forge/operations"; + static final String TEZOS_PREAPPLY_OPERATIONS = "chains/main/blocks/head/helpers/preapply/operations"; + static final String TEZOS_RUN_OPERATION = "chains/main/blocks/head/helpers/scripts/run_operation"; + static final String TEZOS_INJECT_OPERATIONS = "injection/operation"; + + private Retrofit retrofitTezos = new Retrofit.Builder() + .baseUrl(letzbakeURI) + .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(ScalarsConverterFactory.create()) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + //logging for testing + .client(new OkHttpClient.Builder().addInterceptor( + new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY) + ).build()) + + .build(); + + private TezosApi tezosApi = retrofitTezos.create(TezosApi.class); + + private int requestsCount = 0; + + public boolean isRequestsSequenceCompleted() { + Log.i(TAG, String.format("isRequestsSequenceCompleted: %s (%d requests left)", String.valueOf(requestsCount <= 0), requestsCount)); + return requestsCount <= 0; + } + + public void getAddress(String wallet, SingleObserver accountObserver) { + requestsCount++; + Log.i(TAG, "new getAddress request"); + + Single accountSingle = tezosApi.getAccount(wallet) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnEvent((object, throwable) -> requestsCount--); + + accountSingle.subscribe(accountObserver); + } + + public void getMangerKey(String wallet, SingleObserver accountObserver) { + requestsCount++; + Log.i(TAG, "new getManagerKey request"); + + Single managerKeySingle = tezosApi.getManagerKey(wallet) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnEvent((object, throwable) -> requestsCount--); + + managerKeySingle.subscribe(accountObserver); + } + + public TezosHeaderResponse getHeader() throws Exception { // TODO? not async + requestsCount++; + Log.i(TAG, "new getHeader request"); + + Response headerResponse = tezosApi.getHeader().execute(); + + requestsCount--; + if (headerResponse.code() == 200) { + return headerResponse.body(); + } else { + throw new Exception("Wrong header response, code: " + headerResponse.code()); + } + } + + public String forgeOperations(TezosForgeBody tezosForgeBody) throws Exception { // TODO? not async + requestsCount++; + Log.i(TAG, "new forgeOperations request"); + + Response forgeResponse = tezosApi.forgeOperations(tezosForgeBody).execute(); + + requestsCount--; + if (forgeResponse.code() == 200) { + return forgeResponse.body(); + } else { + throw new Exception("Wrong forge response, code: " + forgeResponse.code()); + } + } + + public void peapplyOperations(TezosPreapplyBody tezosPreapplyBody) throws Exception { + Log.i(TAG, "new peapplyOperations request"); + + List tezosPreapplyBodyList = new ArrayList<>(); + tezosPreapplyBodyList.add(tezosPreapplyBody); + Response preapplyResponse = tezosApi.preapplyOperations(tezosPreapplyBodyList).execute(); + + if (preapplyResponse.code() != 200) { + String error = "Preapply error: unknown error"; + if (preapplyResponse.errorBody() != null) { + error = "Preapply error: " + preapplyResponse.errorBody().string(); + } + Log.e(TAG, error); + throw new Exception(error); + } + } + + public void injectOperations(String txForSend, SingleObserver injectObserver) { + requestsCount++; + Log.i(TAG, "new injectOperations request"); + + Single injectSingle = tezosApi.injectOperations(txForSend) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnEvent((object, throwable) -> requestsCount--); + + injectSingle.subscribe(injectObserver); + } +} diff --git a/app/src/main/java/com/tangem/data/network/TezosApi.java b/app/src/main/java/com/tangem/data/network/TezosApi.java new file mode 100644 index 0000000000..e61809b3d7 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/TezosApi.java @@ -0,0 +1,35 @@ +package com.tangem.data.network; + +import com.tangem.data.network.model.TezosAccountResponse; +import com.tangem.data.network.model.TezosForgeBody; +import com.tangem.data.network.model.TezosHeaderResponse; +import com.tangem.data.network.model.TezosPreapplyBody; + +import java.util.List; + +import io.reactivex.Single; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Path; + +public interface TezosApi { + @GET(ServerApiTezos.TEZOS_ADDRESS) + Single getAccount(@Path("address") String address); + + @GET(ServerApiTezos.TEZOS_HEADER) + Call getHeader(); + + @GET(ServerApiTezos.TEZOS_MANAGER_KEY) + Single getManagerKey(@Path("address") String address); + + @POST(ServerApiTezos.TEZOS_FORGE_OPERATIONS) + Call forgeOperations(@Body TezosForgeBody tezosForgeBody); + + @POST(ServerApiTezos.TEZOS_PREAPPLY_OPERATIONS) + Call preapplyOperations(@Body List tezosPreapplyBodyList); + + @POST(ServerApiTezos.TEZOS_INJECT_OPERATIONS) + Single injectOperations(@Body String txForSend); +} diff --git a/app/src/main/java/com/tangem/data/network/model/TezosBody.kt b/app/src/main/java/com/tangem/data/network/model/TezosBody.kt new file mode 100644 index 0000000000..16bf34b145 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/TezosBody.kt @@ -0,0 +1,25 @@ +package com.tangem.data.network.model + +data class TezosForgeBody( + val branch: String, + val contents: List +) + +data class TezosOperationContent( + val kind: String, + val source: String, + val fee: String, + val counter: String, + val gas_limit: String, + val storage_limit: String, + val public_key: String? = null, + val destination: String? = null, + val amount: String? = null +) + +data class TezosPreapplyBody( + val protocol: String, + val branch: String, + val contents: List, + val signature: String +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/model/TezosResponse.kt b/app/src/main/java/com/tangem/data/network/model/TezosResponse.kt new file mode 100644 index 0000000000..db3bece398 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/TezosResponse.kt @@ -0,0 +1,19 @@ +package com.tangem.data.network.model + +import com.google.gson.annotations.SerializedName + +data class TezosAccountResponse( + @SerializedName("balance") + var balance: Long? = null, + + @SerializedName("counter") + var counter: Long? = null +) + +data class TezosHeaderResponse( + @SerializedName("protocol") + var protocol: String? = null, + + @SerializedName("hash") + var hash: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt index 7a39a24de2..fb372a74b7 100644 --- a/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt +++ b/app/src/main/java/com/tangem/wallet/CoinEngineFactory.kt @@ -17,6 +17,7 @@ import com.tangem.wallet.matic.MaticTokenEngine import com.tangem.wallet.nftToken.NftTokenEngine import com.tangem.wallet.rsk.RskEngine import com.tangem.wallet.rsk.RskTokenEngine +import com.tangem.wallet.tezos.TezosEngine import com.tangem.wallet.xlm.XlmAssetEngine import com.tangem.wallet.xlm.XlmEngine import com.tangem.wallet.xrp.XrpEngine @@ -51,6 +52,7 @@ object CoinEngineFactory { Blockchain.StellarAsset -> XlmAssetEngine() Blockchain.Eos -> EosEngine() Blockchain.Ducatus -> DucatusEngine() + Blockchain.Tezos -> TezosEngine() else -> null } } @@ -90,6 +92,8 @@ object CoinEngineFactory { EosEngine(context) else if (Blockchain.Ducatus == context.blockchain) DucatusEngine(context) + else if (Blockchain.Tezos == context.blockchain) + TezosEngine(context) else return null } catch (e: Exception) { diff --git a/app/src/main/java/com/tangem/wallet/cardano/CardanoEngine.java b/app/src/main/java/com/tangem/wallet/cardano/CardanoEngine.java index c9a700254f..3b7792df59 100644 --- a/app/src/main/java/com/tangem/wallet/cardano/CardanoEngine.java +++ b/app/src/main/java/com/tangem/wallet/cardano/CardanoEngine.java @@ -503,7 +503,7 @@ public class CardanoEngine extends CoinEngine { @Override public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) { - return signingMethod == TangemCard.SigningMethod.Sign_Hash || signingMethod == TangemCard.SigningMethod.Sign_Raw; + return signingMethod == TangemCard.SigningMethod.Sign_Hash; } @Override diff --git a/app/src/main/java/com/tangem/wallet/tezos/TezosData.kt b/app/src/main/java/com/tangem/wallet/tezos/TezosData.kt new file mode 100644 index 0000000000..0fe80b1c4a --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/tezos/TezosData.kt @@ -0,0 +1,49 @@ +package com.tangem.wallet.tezos + +import android.os.Bundle +import android.util.Log +import com.tangem.wallet.CoinData +import java.lang.Exception + +class TezosData : CoinData() { + var balance: Long? = null + var counter: Long? = null + var publicKeyReavealed: Boolean? = null + var tezosPublicKey: String? = null + + override fun clearInfo() { + super.clearInfo() + balance = null + counter = null + publicKeyReavealed = null + tezosPublicKey = null + } + + override fun loadFromBundle(B: Bundle) { + super.loadFromBundle(B) + + if (B.containsKey("Balance")) balance = B.getLong("Balance") + + if (B.containsKey("Counter")) counter = B.getLong("Counter") + + if (B.containsKey("PublicKeyReavealed")) publicKeyReavealed = B.getBoolean("PublicKeyReavealed") + + if (B.containsKey("TezosPublicKey")) tezosPublicKey = B.getString("TezosPublicKey") + } + + override fun saveToBundle(B: Bundle) { + super.saveToBundle(B) + try { + if (balance != null) B.putLong("Balance", balance!!) + + if (counter != null) B.putLong("Counter", counter!!) + + if (publicKeyReavealed != null) B.putBoolean("PublicKeyReavealed", publicKeyReavealed!!) + + if (tezosPublicKey != null) B.putString("TezosPublicKey", tezosPublicKey!!) + + } catch (e: Exception) { + Log.e("Can't save to bundle ", e.message) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/tezos/TezosEngine.kt b/app/src/main/java/com/tangem/wallet/tezos/TezosEngine.kt new file mode 100644 index 0000000000..5ad3c85cf2 --- /dev/null +++ b/app/src/main/java/com/tangem/wallet/tezos/TezosEngine.kt @@ -0,0 +1,507 @@ +package com.tangem.wallet.tezos + +import android.net.Uri +import android.os.StrictMode +import android.text.InputFilter +import android.util.Log +import com.tangem.App +import com.tangem.data.network.ServerApiTezos +import com.tangem.data.network.model.TezosAccountResponse +import com.tangem.data.network.model.TezosForgeBody +import com.tangem.data.network.model.TezosOperationContent +import com.tangem.data.network.model.TezosPreapplyBody +import com.tangem.tangem_card.data.TangemCard +import com.tangem.tangem_card.tasks.SignTask +import com.tangem.tangem_card.util.Util +import com.tangem.util.CryptoUtil +import com.tangem.util.DecimalDigitsInputFilter +import com.tangem.wallet.* +import io.reactivex.observers.DisposableSingleObserver +import org.spongycastle.jcajce.provider.digest.Blake2b +import java.math.BigDecimal + +class TezosEngine : CoinEngine { + constructor() + + constructor(context: TangemContext) : super(context) { + if (context.coinData == null) { + coinData = TezosData() + context.coinData = coinData + } else if (context.coinData is TezosData) { + coinData = context.coinData as TezosData + } else { + throw Exception("Invalid type of Blockchain data for XlmEngine") + } + } + + private val TAG = TezosEngine::class.java.simpleName + + var coinData: TezosData? = null + + private fun getDecimals() = 6 + + @Throws(Exception::class) + private fun checkBlockchainDataExists() { + if (coinData == null) throw Exception("No blockchain data") + } + + override fun awaitingConfirmation(): Boolean { + return App.pendingTransactionsStorage.hasTransactions(ctx.card) + } + + override fun getBalanceHTML(): String? { + val balance = balance + return if (balance != null) { + balance.toDescriptionString(getDecimals()) + } else { + "" + } + } + + override fun getBalanceCurrency(): String? { + return "XTZ" + } + + override fun isBalanceNotZero(): Boolean { + if (coinData == null) return false + return if (balance == null) false else balance!!.notZero() + } + + override fun hasBalanceInfo(): Boolean { + return if (coinData == null) false else coinData!!.balance != null + } + + override fun isExtractPossible(): Boolean { + if (!hasBalanceInfo()) { + ctx.setMessage(R.string.loaded_wallet_error_obtaining_blockchain_data) + } else if (!isBalanceNotZero) { + ctx.setMessage(R.string.general_wallet_empty) + } else if (awaitingConfirmation()) { + ctx.setMessage(R.string.loaded_wallet_message_wait) + } else { + return true + } + return false + } + + override fun getFeeCurrency(): String? { + return "XTZ" + } + + override fun validateAddress(address: String?): Boolean { + val prefixedHashWithChecksum = Base58.decodeBase58(address) + + if (prefixedHashWithChecksum == null || prefixedHashWithChecksum.size != 27) return false + + val prefixedHash = prefixedHashWithChecksum.copyOf(23) + val checksum = prefixedHashWithChecksum.copyOfRange(23, 27) + + val calcChecksum = CryptoUtil.doubleSha256(prefixedHash).copyOfRange(0, 4) + + return calcChecksum.contentEquals(checksum) + } + + override fun isNeedCheckNode(): Boolean { + return false + } + + override fun getWalletExplorerUri(): Uri? { + return Uri.parse("https://tezblock.io/account/" + ctx.coinData.wallet) + } + + override fun getShareWalletUri(): Uri? { + return Uri.parse(ctx.coinData.wallet) + } + + override fun getAmountInputFilters(): Array? { + return arrayOf(DecimalDigitsInputFilter(getDecimals())) + } + + override fun checkNewTransactionAmount(amount: Amount): Boolean { + if (coinData == null) return false + return amount <= balance + } + + override fun checkNewTransactionAmountAndFee(amountValue: Amount?, + feeValue: Amount?, + isIncludeFee: Boolean + ): Boolean { + try { + checkBlockchainDataExists() + } catch (e: java.lang.Exception) { + e.printStackTrace() + return false + } + if (feeValue == null || amountValue == null) return false + if (feeValue.isZero || amountValue.isZero) return false + if (isIncludeFee && (amountValue > balance || amountValue < feeValue)) return false + if (!isIncludeFee && amountValue.add(feeValue) > balance) return false + + return true + } + + override fun validateBalance(balanceValidator: BalanceValidator): Boolean { + try { + if (ctx.card.offlineBalance == null && + !ctx.coinData.isBalanceReceived || + !ctx.coinData.isBalanceReceived && + ctx.card.remainingSignatures != ctx.card.maxSignatures + ) { + balanceValidator.setScore(0) + balanceValidator.firstLine = R.string.balance_validator_first_line_unknown_balance + balanceValidator.setSecondLine(R.string.balance_validator_second_line_unverified_balance) + return false + } + if (coinData!!.isBalanceReceived) { + balanceValidator.setScore(100) + balanceValidator.firstLine = R.string.balance_validator_first_line_verified_balance + balanceValidator.setSecondLine(R.string.balance_validator_second_line_confirmed_in_blockchain) + if (balance!!.isZero) { + balanceValidator.firstLine = R.string.balance_validator_first_line_empty_wallet + balanceValidator.setSecondLine(R.string.empty_string) + } + } + if (ctx.card.offlineBalance != null && + !coinData!!.isBalanceReceived && + ctx.card.remainingSignatures == + ctx.card.maxSignatures && + balance!!.notZero() + ) { + balanceValidator.setScore(80) + balanceValidator.firstLine = R.string.balance_validator_first_line_verified_offline + balanceValidator.setSecondLine( + R.string.balance_validator_second_line_internet_to_get_balance + ) + } + return true + } catch (e: java.lang.Exception) { + e.printStackTrace() + return false + } + } + + override fun getBalance(): Amount? { + var balanceAmount: Amount? = null + + if (hasBalanceInfo()) { + val xtzBalance = BigDecimal + .valueOf(coinData!!.balance!!).movePointLeft(getDecimals()) + balanceAmount = Amount(xtzBalance, balanceCurrency) + } + + return balanceAmount + } + + override fun evaluateFeeEquivalent(fee: String?): String? { + return if (!coinData!!.amountEquivalentDescriptionAvailable) "" else try { + val feeAmount = Amount(fee, feeCurrency) + feeAmount.toEquivalentString(coinData!!.rate.toDouble()) + } catch (e: java.lang.Exception) { + "" + } + } + + override fun getBalanceEquivalent(): String? { + if (coinData == null || !coinData!!.amountEquivalentDescriptionAvailable) return "" + val balance = balance ?: return "" + return balance.toEquivalentString(coinData!!.rate.toDouble()) + } + + override fun calculateAddress(pkUncompressed: ByteArray): String? { + val publicKeyHash = Blake2b.Blake2b160().digest(pkUncompressed) + + val tz1Prefix = Util.hexToBytes("06A19F") + val prefixedHash = tz1Prefix + publicKeyHash + + val checksum = CryptoUtil.doubleSha256(prefixedHash).copyOfRange(0, 4) + val prefixedHashWithChecksum = prefixedHash + checksum + + return Base58.encodeBase58(prefixedHashWithChecksum) + } + + fun calculateTezosPublicKey(pkUncompressed: ByteArray): String { + val edpkPrefix = Util.hexToBytes("0D0F25D9") + val prefixedPubKey = edpkPrefix + pkUncompressed + + val checksum = CryptoUtil.doubleSha256(prefixedPubKey).copyOfRange(0, 4) + val prefixedHashWithChecksum = prefixedPubKey + checksum + + return Base58.encodeBase58(prefixedHashWithChecksum) + } + + override fun convertToAmount(internalAmount: InternalAmount): Amount { + return Amount(internalAmount.movePointLeft(getDecimals()), balanceCurrency) + } + + override fun convertToAmount(strAmount: String, currency: String): Amount { + return Amount(strAmount, currency) + } + + override fun convertToInternalAmount(amount: Amount): InternalAmount { + return InternalAmount(amount.movePointRight(getDecimals()), "mutez") + } + + override fun convertToInternalAmount(bytes: ByteArray?): InternalAmount? { + if (bytes == null) return null + val reversed = ByteArray(bytes.size) + for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1] + return InternalAmount(Util.byteArrayToLong(reversed), "mutez") + } + + override fun convertToByteArray(internalAmount: InternalAmount): ByteArray? { + val bytes = Util.longToByteArray(internalAmount.longValueExact()) + val reversed = ByteArray(bytes.size) + for (i in bytes.indices) reversed[i] = bytes[bytes.size - i - 1] + return reversed + } + + override fun createCoinData(): CoinData { + return TezosData() + } + + override fun getUnspentInputsDescription() = "" + + override fun constructTransaction( + amountValue: Amount, + feeValue: Amount, + IncFee: Boolean, + targetAddress: String + ): SignTask.TransactionToSign { + + checkBlockchainDataExists() + val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() + StrictMode.setThreadPolicy(policy) + + val finalAmount = if (IncFee) { + amountValue.minus(feeValue) + } else { + amountValue + } + + val serverApiTezos = ServerApiTezos() + val headerResponse = serverApiTezos.header + + val contents = arrayListOf() + + var counter = coinData!!.counter!! + + if (!coinData!!.publicKeyReavealed!!) { + counter++ + val revealOp = TezosOperationContent( + kind = "reveal", + source = coinData!!.wallet, + fee = "1300", + counter = counter.toString(), + gas_limit = "10000", + storage_limit = "0", + public_key = coinData!!.tezosPublicKey!! + ) + + contents.add(revealOp) + } + + counter++ + val transactionOp = TezosOperationContent( + kind = "transaction", + source = coinData!!.wallet, + fee = "1350", + counter = counter.toString(), + gas_limit = "10600", + storage_limit = "277", + destination = targetAddress, + amount = finalAmount.movePointRight(getDecimals()).toBigInteger().toString() + ) + + contents.add(transactionOp) + + val tezosForgeBody = TezosForgeBody(headerResponse.hash!!, contents) + val forgeResponse = serverApiTezos.forgeOperations(tezosForgeBody) + val watermark = "03" + val forgedBytes = Util.hexToBytes(watermark + forgeResponse) + + return object : SignTask.TransactionToSign { + override fun isSigningMethodSupported(signingMethod: TangemCard.SigningMethod): Boolean { + return signingMethod == TangemCard.SigningMethod.Sign_Hash + } + + @Throws(java.lang.Exception::class) + override fun getHashesToSign(): Array { + val dataForSign = arrayOfNulls(1) + dataForSign[0] = Blake2b.Blake2b256().digest(forgedBytes) + return dataForSign + } + + @Throws(java.lang.Exception::class) + override fun getRawDataToSign(): ByteArray? { + throw java.lang.Exception("Signing of raw transaction not supported for " + this.javaClass.simpleName) + } + + @Throws(java.lang.Exception::class) + override fun getHashAlgToSign(): String? { + throw java.lang.Exception("Signing of raw transaction not supported for " + this.javaClass.simpleName) + } + + @Throws(java.lang.Exception::class) + override fun getIssuerTransactionSignature(dataToSignByIssuer: ByteArray?): ByteArray? { + throw java.lang.Exception("Transaction validation by issuer not supported in this version") + } + + @Throws(java.lang.Exception::class) + override fun onSignCompleted(signFromCard: ByteArray): ByteArray? { + val edsigPrefix = Util.hexToBytes("09F5CD8612") + val prefixedSignature = edsigPrefix + signFromCard + val checksum = CryptoUtil.doubleSha256(prefixedSignature).copyOfRange(0, 4) + val prefixedSignatureWithChecksum = prefixedSignature + checksum + + val preapplyBody = TezosPreapplyBody( + protocol = headerResponse.protocol!!, + branch = headerResponse.hash!!, + contents = contents, + signature = Base58.encodeBase58(prefixedSignatureWithChecksum) + ) + + try { + serverApiTezos.peapplyOperations(preapplyBody) + } catch (e: java.lang.Exception) { + ctx.error = e.message + return null + } + + val txForSend = Util.hexToBytes(forgeResponse) + signFromCard + notifyOnNeedSendTransaction(txForSend) + return txForSend + } + } + } + + override fun requestBalanceAndUnspentTransactions( + blockchainRequestsCallbacks: BlockchainRequestsCallbacks + ) { + coinData!!.tezosPublicKey = calculateTezosPublicKey(ctx.card.walletPublicKey) + + val serverApiTezos = ServerApiTezos() + + val accountObserver = object : DisposableSingleObserver() { + override fun onSuccess(response: TezosAccountResponse) { + coinData!!.balance = response.balance + coinData!!.isBalanceReceived = true + coinData!!.counter = response.counter + + if (serverApiTezos.isRequestsSequenceCompleted) { + blockchainRequestsCallbacks.onComplete(ctx.hasError()) + } else { + blockchainRequestsCallbacks.onProgress() + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "requestBalanceAndUnspentTransactions error " + e.message) + ctx.error = e.message + e.printStackTrace() + + if (serverApiTezos.isRequestsSequenceCompleted) { + blockchainRequestsCallbacks.onComplete(false) + } else { + blockchainRequestsCallbacks.onProgress() + } + } + } + + val managerKeyObserver = object : DisposableSingleObserver() { + override fun onSuccess(response: String) { + coinData!!.publicKeyReavealed = true + + if (serverApiTezos.isRequestsSequenceCompleted) { + blockchainRequestsCallbacks.onComplete(ctx.hasError()) + } else { + blockchainRequestsCallbacks.onProgress() + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "requestFee error " + e.message) + e.printStackTrace() + + coinData!!.publicKeyReavealed = false // error expected when key is not revealed + + if (serverApiTezos.isRequestsSequenceCompleted) { + blockchainRequestsCallbacks.onComplete(ctx.hasError()) + } else { + blockchainRequestsCallbacks.onProgress() + } + } + } + + serverApiTezos.getAddress(coinData!!.wallet, accountObserver) + serverApiTezos.getMangerKey(coinData!!.wallet, managerKeyObserver) + + } + + override fun requestFee( + blockchainRequestsCallbacks: BlockchainRequestsCallbacks, + targetAddress: String?, + amount: Amount? + ) { + var fee: BigDecimal = BigDecimal.valueOf(0.00135) + + if (!coinData!!.publicKeyReavealed!!) { + fee += BigDecimal.valueOf(0.0013) + } + + val serverApiTezos = ServerApiTezos() + + val accountObserver = object : DisposableSingleObserver() { + override fun onSuccess(response: TezosAccountResponse) { + if (response.balance == 0L) { + fee += BigDecimal.valueOf(0.257) + } + val feeAmount = Amount(fee, feeCurrency) + coinData!!.minFee = feeAmount + coinData!!.normalFee = feeAmount + coinData!!.maxFee = feeAmount + + blockchainRequestsCallbacks.onComplete(true) + } + + override fun onError(e: Throwable) { + Log.e(TAG, "requestFee error " + e.message) + ctx.error = e.message + e.printStackTrace() + + blockchainRequestsCallbacks.onComplete(false) + } + } + + serverApiTezos.getAddress(targetAddress, accountObserver) + } + + override fun requestSendTransaction( + blockchainRequestsCallbacks: BlockchainRequestsCallbacks, + txForSend: ByteArray? + ) { + + if (txForSend == null) { + blockchainRequestsCallbacks.onComplete(false) + } else { + val injectObserver = object : DisposableSingleObserver() { + override fun onSuccess(response: Any) { + blockchainRequestsCallbacks.onComplete(true) + } + + override fun onError(e: Throwable) { + blockchainRequestsCallbacks.onComplete(false) + } + } + + ServerApiTezos().injectOperations(Util.bytesToHex(txForSend), injectObserver) + } + } + + override fun allowSelectFeeLevel(): Boolean { + return false + } + + override fun pendingTransactionTimeoutInSeconds(): Int { + return 60 + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index e01893d33f..c4edca4c38 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { maven { url 'https://maven.fabric.io/public' } } dependencies { - classpath 'com.android.tools.build:gradle:3.5.2' + classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' classpath 'io.fabric.tools:gradle:1.31.0'