diff --git a/.gitignore b/.gitignore index 0a91f19203..ceb1b04599 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ /build /captures .externalNativeBuild -app/libs/ .idea/vcs.xml .idea/caches .idea/dictionaries diff --git a/app/build.gradle b/app/build.gradle index ab63c2525b..f368ea6864 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,8 +15,8 @@ android { applicationId "com.tangem.wallet" minSdkVersion 21 targetSdkVersion 28 - versionCode 106 - versionName "0.903.2." + generateVersionName() + versionCode 107 + versionName "0.904.1." + generateVersionName() testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } androidExtensions { @@ -47,11 +47,9 @@ dependencies { implementation project(':card-common') implementation project(':card-android') implementation project(':server-android') - -// implementation 'com.github.TangemCash:card-common:0.1.2' -// implementation 'com.github.TangemCash:card-android:0.1.3' -// implementation 'com.github.TangemCash:server-android:0.1.4' - + // implementation 'com.github.TangemCash:card-common:0.1.2' + // implementation 'com.github.TangemCash:card-android:0.1.3' + // implementation 'com.github.TangemCash:server-android:0.1.4' implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' @@ -81,4 +79,12 @@ dependencies { implementation 'io.reactivex.rxjava2:rxjava:2.2.0' implementation 'io.reactivex.rxjava2:rxandroid:2.0.2' implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0' + + implementation files('libs/ripple-core-0.0.1.jar') + //4 dependencies for ripple-core TODO: move to module? + implementation 'net.i2p.crypto:eddsa:0.3.0' + implementation 'org.bouncycastle:bcprov-jdk15on:1.58' + //noinspection DuplicatePlatformClasses + implementation 'org.json:json:20180813' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' } \ No newline at end of file diff --git a/app/libs/ripple-core-0.0.1.jar b/app/libs/ripple-core-0.0.1.jar new file mode 100644 index 0000000000..617db52b2d Binary files /dev/null and b/app/libs/ripple-core-0.0.1.jar differ diff --git a/app/src/main/java/com/tangem/data/Blockchain.java b/app/src/main/java/com/tangem/data/Blockchain.java index e229b98330..c613c3e3d6 100644 --- a/app/src/main/java/com/tangem/data/Blockchain.java +++ b/app/src/main/java/com/tangem/data/Blockchain.java @@ -12,12 +12,13 @@ public enum Blockchain { Ethereum("ETH", "ETH", 1.0, R.drawable.ic_logo_ethereum, "Ethereum"), EthereumTestNet("ETH/test", "ETH", 1.0, R.drawable.ic_logo_ethereum_testnet, "Ethereum Testnet"), Token("Token", "ETH", 1.0, R.drawable.ic_logo_bat_token, "Ethereum"), - NftToken("NftToken", "", 1.0, R.drawable.ic_logo_bat_token, "Ethereum"), + NftToken("NftToken", "", 1.0, R.drawable.tangem2, "Ethereum"), BitcoinCash("BCH", "BCH", 100000000.0, R.drawable.ic_logo_bitcoin_cash, "Bitcoin Cash"), - Litecoin("LTC", "LTC", 100000000.0, R.drawable.ic_logo_bitcoin, "Litecoin"), - Rootstock("RSK", "RBTC", 1.0, R.drawable.ic_logo_bitcoin, "Rootstock"), - RootstockToken("Token", "RBTC", 1.0, R.drawable.ic_logo_bat_token, "Rootstock"), - Cardano("CARDANO", "ADA", 1000000.0,R.drawable.ic_logo_bitcoin, "Cardano"); + Litecoin("LTC", "LTC", 100000000.0, R.drawable.tangem2, "Litecoin"), + Rootstock("RSK", "RBTC", 1.0, R.drawable.tangem2, "Rootstock"), + RootstockToken("RskToken", "RBTC", 1.0, R.drawable.tangem2, "Rootstock"), + Cardano("CARDANO", "ADA", 1000000.0, R.drawable.tangem2, "Cardano"), + Ripple ("XRP", "XRP", 1000000.0, R.drawable.tangem2, "Ripple"); Blockchain(String ID, String currency, double multiplier, int imageResource, String officialName) { mID = ID; @@ -92,6 +93,9 @@ public enum Blockchain { case "BTC": return R.drawable.ic_logo_bitcoin; + case "BCH": + return R.drawable.ic_logo_bitcoin_cash; + case "Token": if (symbolName.equals("SEED")) return R.drawable.ic_logo_seed; diff --git a/app/src/main/java/com/tangem/data/network/RippleApi.java b/app/src/main/java/com/tangem/data/network/RippleApi.java new file mode 100644 index 0000000000..2b6ad9fe29 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/RippleApi.java @@ -0,0 +1,15 @@ +package com.tangem.data.network; + +import com.tangem.data.network.model.RippleBody; +import com.tangem.data.network.model.RippleResponse; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.Headers; +import retrofit2.http.POST; + +public interface RippleApi { + @Headers("Content-Type: application/json") + @POST("./") + Call ripple(@Body RippleBody body); +} diff --git a/app/src/main/java/com/tangem/data/network/Server.java b/app/src/main/java/com/tangem/data/network/Server.java index 9be4f58449..832e0495c2 100644 --- a/app/src/main/java/com/tangem/data/network/Server.java +++ b/app/src/main/java/com/tangem/data/network/Server.java @@ -32,6 +32,9 @@ public class Server { } } + /** + * https://public-node.rsk.co/ + */ public static class ApiRootstock { public static final String URL_ROOTSTOCK = ServerURL.API_ROOTSTOCK; diff --git a/app/src/main/java/com/tangem/data/network/ServerApiElectrum.java b/app/src/main/java/com/tangem/data/network/ServerApiElectrum.java index a087030180..9b920fcace 100644 --- a/app/src/main/java/com/tangem/data/network/ServerApiElectrum.java +++ b/app/src/main/java/com/tangem/data/network/ServerApiElectrum.java @@ -325,6 +325,7 @@ public class ServerApiElectrum { try { Log.i(TAG, host + " " + port); sslSocket = (SSLSocket) sf.createSocket(host, port); + sslSocket.setSoTimeout(3000); try { OutputStream os = sslSocket.getOutputStream(); OutputStreamWriter out = new OutputStreamWriter(os, "UTF-8"); diff --git a/app/src/main/java/com/tangem/data/network/ServerApiRipple.java b/app/src/main/java/com/tangem/data/network/ServerApiRipple.java new file mode 100644 index 0000000000..ddbe82e090 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/ServerApiRipple.java @@ -0,0 +1,117 @@ +package com.tangem.data.network; + +import android.util.Log; + +import com.tangem.data.network.model.RippleBody; +import com.tangem.data.network.model.RippleResponse; + +import java.util.HashMap; + +import androidx.annotation.NonNull; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class ServerApiRipple { + private static String TAG = ServerApiRipple.class.getSimpleName(); + + public static final String RIPPLE_ACCOUNT_INFO = "account_info"; + public static final String RIPPLE_ACCOUNT_UNCONFIRMED = "account_unconfirmed"; + public static final String RIPPLE_SUBMIT = "submit"; + public static final String RIPPLE_FEE = "fee"; + public static final String RIPPLE_SERVER_STATE = "server_state"; + + private int requestsCount = 0; + + public static String lastNode; + + public boolean isRequestsSequenceCompleted() { + Log.i(TAG, String.format("isRequestsSequenceCompleted: %s (%d requests left)", String.valueOf(requestsCount <= 0), requestsCount)); + return requestsCount <= 0; + } + + private ResponseListener responseListener; + + public interface ResponseListener { + void onSuccess(String method, RippleResponse rippleResponse); + + void onFail(String method, String message); + } + + public void setResponseListener(ResponseListener listener) { + responseListener = listener; + } + + public void requestData(String method, String wallet, String tx) { + requestsCount++; + String rippleURL = "https://s1.ripple.com:51234"; //TODO: make random selection + lastNode = rippleURL; //TODO: show node instead of URL + + Retrofit retrofitRipple = new Retrofit.Builder() + .baseUrl(rippleURL) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + + RippleApi rippleApi = retrofitRipple.create(RippleApi.class); + + RippleBody rippleBody; + HashMap paramsMap; + + switch (method) { + case RIPPLE_ACCOUNT_INFO: + paramsMap = new HashMap<>(); + paramsMap.put("account", wallet); + paramsMap.put("ledger_index", "validated"); + rippleBody = new RippleBody(method, paramsMap); + break; + + case RIPPLE_ACCOUNT_UNCONFIRMED: + paramsMap = new HashMap<>(); + paramsMap.put("account", wallet); + paramsMap.put("ledger_index", "current"); +// paramsMap.put("queue", "true"); TODO: make queue check if needed + rippleBody = new RippleBody(RIPPLE_ACCOUNT_INFO, paramsMap); + break; + + case RIPPLE_SERVER_STATE: + rippleBody = new RippleBody(method, new HashMap<>()); + break; + + case RIPPLE_FEE: + rippleBody = new RippleBody(method, new HashMap<>()); + break; + + case RIPPLE_SUBMIT: + paramsMap = new HashMap<>(); + paramsMap.put("tx_blob", tx); + rippleBody = new RippleBody(method, paramsMap); + break; + + default: + rippleBody = new RippleBody(); + } + + Call call = rippleApi.ripple(rippleBody); + call.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.code() == 200) { + requestsCount--; + responseListener.onSuccess(method, response.body()); + Log.i(TAG, "requestData " + method + " onResponse " + response.code()); + } else { + responseListener.onFail(method, String.valueOf(response.code())); + Log.e(TAG, "requestData " + method + " onResponse " + response.code()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + responseListener.onFail(method, String.valueOf(t.getMessage())); + Log.e(TAG, "requestData " + method + " onFailure " + t.getMessage()); + } + }); + } +} diff --git a/app/src/main/java/com/tangem/data/network/ServerApiRootstock.java b/app/src/main/java/com/tangem/data/network/ServerApiRootstock.java index 0bcf5b7b4f..198f5cc6b0 100644 --- a/app/src/main/java/com/tangem/data/network/ServerApiRootstock.java +++ b/app/src/main/java/com/tangem/data/network/ServerApiRootstock.java @@ -16,7 +16,7 @@ public class ServerApiRootstock { /** * HTTP - * Infura + * Rootstock *

* eth_getBalance * eth_getTransactionCount diff --git a/app/src/main/java/com/tangem/data/network/model/AdaliteResponse.kt b/app/src/main/java/com/tangem/data/network/model/AdaliteResponse.kt index 2c1602b513..d560d5fefc 100644 --- a/app/src/main/java/com/tangem/data/network/model/AdaliteResponse.kt +++ b/app/src/main/java/com/tangem/data/network/model/AdaliteResponse.kt @@ -4,15 +4,15 @@ import com.google.gson.annotations.SerializedName data class AdaliteResponse( @SerializedName("Right") - var right: AddressData? = null + var right: AdaliteAddressData? = null ) data class AdaliteResponseUtxo( @SerializedName("Right") - var right: List + var right: List ) -data class AddressData( +data class AdaliteAddressData( @SerializedName("caAddress") var caAddress: String? = null, @@ -20,7 +20,7 @@ data class AddressData( var caBalance: AdaliteCoins? = null, @SerializedName("caTxList") - var caTxList: List + var caTxList: List ) data class AdaliteCoins( @@ -28,7 +28,7 @@ data class AdaliteCoins( var getCoin: Long? = null ) -data class UtxoData( +data class AdaliteUtxoData( @SerializedName("cuId") var cuId: String? = null, @@ -39,7 +39,7 @@ data class UtxoData( var cuCoins: AdaliteCoins? = null ) -data class TxData( +data class AdaliteTxData( @SerializedName("ctbId") var ctbId: String? = null ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/data/network/model/RippleBody.java b/app/src/main/java/com/tangem/data/network/model/RippleBody.java new file mode 100644 index 0000000000..0423f357a1 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/RippleBody.java @@ -0,0 +1,24 @@ +package com.tangem.data.network.model; + +import java.util.ArrayList; +import java.util.HashMap; + +public class RippleBody { + private String method; + private ArrayList> params; + + public RippleBody() { + } + + //for RIPPLE_FEE + public RippleBody(String method) { + this.method = method; + } + + public RippleBody(String method, HashMap paramsMap) { + this.method = method; + ArrayList> paramsList = new ArrayList<>(); + paramsList.add(paramsMap); + this.params = paramsList; + } +} diff --git a/app/src/main/java/com/tangem/data/network/model/RippleResponse.kt b/app/src/main/java/com/tangem/data/network/model/RippleResponse.kt new file mode 100644 index 0000000000..4a865aca67 --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/RippleResponse.kt @@ -0,0 +1,78 @@ +package com.tangem.data.network.model + +import com.google.gson.annotations.SerializedName + +data class RippleResponse( + @SerializedName("result") + var result: RippleResult? = null +) + +data class RippleResult( + @SerializedName("account_data") + var account_data: RippleAccountData? = null, + + @SerializedName("validated") + var validated: Boolean? = null, + + //for RIPPLE_FEE + @SerializedName("drops") + var drops: RippleFeeDrops? = null, + + //for RIPPLE_SUBMIT + @SerializedName("engine_result_code") + var engine_result_code: Int? = null, + + //for RIPPLE_SUBMIT + @SerializedName ("engine_result_message") + var engine_result_message: String? = null, + + //for RIPPLE_SUBMIT + @SerializedName("error") + var error: String? = null, + + //for RIPPLE_SUBMIT + @SerializedName("error_exception") + var error_exception: String? = null, + + //for RIPPLE_SERVER_STATE + @SerializedName("state") + var state: RippleState? = null, + + //for "Account not found error" + @SerializedName("error_code") + var error_code: Int? = null +) + +data class RippleAccountData( + @SerializedName("Account") + var account: String? = null, + + @SerializedName("Balance") + var balance: String? = null, + + @SerializedName("Sequence") + var sequence: Long? = null +) + +data class RippleFeeDrops( + //enough to put tx to queue + @SerializedName("minimum_fee") + var minimum_fee: String? = null, + + //enough to put tx to current ledger + @SerializedName("open_ledger_fee") + var open_ledger_fee: String? = null, + + @SerializedName("median_fee") + var median_fee: String? = null +) + +data class RippleState( + @SerializedName("validated_ledger") + var validated_ledger: RippleLedger? = null +) + +data class RippleLedger( + @SerializedName("reserve_base") + var reserve_base: Long? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java b/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java index f539bb9443..5936584616 100644 --- a/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/CoinEngine.java @@ -216,7 +216,7 @@ public abstract class CoinEngine { public abstract boolean validateAddress(String address); - public abstract String calculateAddress(byte[] pkUncompressed) throws NoSuchProviderException, NoSuchAlgorithmException, CborException, IOException; + public abstract String calculateAddress(byte[] pkUncompressed) throws NoSuchProviderException, NoSuchAlgorithmException, CborException, IOException, Exception; public abstract Amount convertToAmount(InternalAmount internalAmount) throws Exception; diff --git a/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.kt b/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.kt index 00e26bc4cb..15c56cef45 100644 --- a/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.kt +++ b/app/src/main/java/com/tangem/domain/wallet/CoinEngineFactory.kt @@ -12,11 +12,13 @@ import com.tangem.domain.wallet.ltc.LtcEngine import com.tangem.domain.wallet.nftToken.NftTokenEngine import com.tangem.domain.wallet.rsk.RskEngine import com.tangem.domain.wallet.rsk.RskTokenEngine +import com.tangem.domain.wallet.xrp.XrpEngine /** * Factory for create specific engine * - * @param Blockchain + * @param + * Blockchain * @param TangemContext * */ @@ -35,6 +37,7 @@ object CoinEngineFactory { Blockchain.Rootstock -> RskEngine() Blockchain.RootstockToken -> RskTokenEngine() Blockchain.Cardano -> CardanoEngine() + Blockchain.Ripple -> XrpEngine() else -> null } } @@ -61,6 +64,8 @@ object CoinEngineFactory { RskTokenEngine(context) else if (Blockchain.Cardano == context.blockchain) CardanoEngine(context) + else if (Blockchain.Ripple == context.blockchain) + XrpEngine(context) else return null } catch (e: Exception) { diff --git a/app/src/main/java/com/tangem/domain/wallet/btc/BitcoinNode.kt b/app/src/main/java/com/tangem/domain/wallet/btc/BitcoinNode.kt index 31b995cd4a..7259769043 100644 --- a/app/src/main/java/com/tangem/domain/wallet/btc/BitcoinNode.kt +++ b/app/src/main/java/com/tangem/domain/wallet/btc/BitcoinNode.kt @@ -1,27 +1,31 @@ package com.tangem.domain.wallet.btc enum class BitcoinNode(val host: String, val port: Int, val proto: String) { - N_001("electrum.anduck.net", 50001, "tcp"), - N_002("electrum.qtornado.com", 50001, "tcp"), - N_003("e-x.not.fyi", 50001, "tcp"), - N_004("electrum.vom-stausee.de", 50001, "tcp"), - N_005("electrum2.eff.ro", 50001, "tcp"), - N_006("electrum.coinucopia.io", 50001, "tcp"), - N_007("electrum.coinop.cc", 50002, "ssl"), - N_008("electrum.vom-stausee.de", 50002, "ssl"), - N_009("dedi.jochen-hoenicke.de", 50002, "ssl"), - N_010("e-x.not.fyi", 50002, "ssl"), - N_011("electrum.villocq.com", 50002, "ssl"), - N_012("electrum.anduck.net", 50012, "ssl"), - N_013("technetium.network", 50002, "ssl"), - N_014("electrum.coinucopia.io", 50002, "ssl"), - N_015("dimon.trimon.de", 50002, "ssl"), - N_016("btc.gravitech.net", 50002, "ssl"), - N_017("fn.48.org", 50002, "ssl"), - N_018("vps.hsmiths.com", 50002, "ssl"), - N_019("electrum.qtornado.com", 50002, "ssl"), - N_020("electrum2.eff.ro", 50002, "ssl"), - N_021("electrum.hsmiths.com", 995, "ssl"), - N_022("electrum.hsmiths.com", 50002, "ssl"), - N_023("tardis.bauerj.eu", 50002, "ssl"), + N_001("electrum.be", 50001, "tcp"), + N_002("electrum-lightning.cappux.com", 50001, "tcp"), + N_003("185.64.116.15", 50001, "tcp"), + N_004("electrum.noinput.xyz", 50001, "tcp"), + N_005("bitcoin.grey.pw", 50001, "tcp"), + N_006("139.162.14.142", 50001, "tcp"), + N_007("13.80.67.162", 50001, "tcp"), + N_008("207.154.223.80", 50001, "tcp"), + N_009("e.keff.org", 50001, "tcp"), + N_010("electrum.coinop.cc", 50002, "ssl"), + N_011("dedi.jochen-hoenicke.de", 50002, "ssl"), + N_012("technetium.network", 50002, "ssl"), + N_013("btc.gravitech.net", 50002, "ssl"), + N_014("vps.hsmiths.com", 50002, "ssl"), + N_015("tomscryptos.com", 50002, "ssl"), + N_016("electrum.be", 50002, "ssl"), + N_017("95.216.28.117", 50002, "ssl"), + N_018("88.198.241.196", 50002, "ssl"), + N_019("electrum-lightning.cappux.com", 50002, "ssl"), + N_020("5.10.11.242", 50002, "ssl"), + N_021("84.195.61.95", 50002, "ssl"), + N_022("185.64.116.15", 50002, "ssl"), + N_023("electrum.mindspot.org", 50002, "ssl"), + N_024("172.103.153.90", 50002, "ssl"), + N_025("electrum.noinput.xyz", 50002, "ssl"), + N_026("bitcoin.grey.pw", 50002, "ssl"), + N_027("207.180.251.11", 50002, "ssl"), } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/domain/wallet/btc/BtcEngine.java b/app/src/main/java/com/tangem/domain/wallet/btc/BtcEngine.java index 824bee559a..3e248c2bef 100644 --- a/app/src/main/java/com/tangem/domain/wallet/btc/BtcEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/btc/BtcEngine.java @@ -768,30 +768,26 @@ public class BtcEngine extends CoinEngine { final ServerApiElectrum.ResponseListener electrumListener = new ServerApiElectrum.ResponseListener() { @Override public void onSuccess(ElectrumRequest electrumRequest) { - BigDecimal fee; + BigDecimal kbFee; if (electrumRequest.isMethod(ElectrumRequest.METHOD_GetFee)) { try { - fee = new BigDecimal(electrumRequest.getResultString()); //fee per KB + kbFee = new BigDecimal(electrumRequest.getResultString()); //fee per KB - if (fee.equals(BigDecimal.ZERO)) { + if (kbFee.equals(BigDecimal.ZERO)) { serverApiElectrum.requestData(ctx, ElectrumRequest.getFee()); } -// if (calcSize != 0) { - fee = fee.multiply(new BigDecimal(calcSize)).divide(new BigDecimal(1024)); // (per KB -> per byte)*size -// } else { -// serverApiElectrum.requestData(ctx, ElectrumRequest.getFee()); -// } + BigDecimal minByteFee = kbFee.divide(new BigDecimal(1024)); // per KB -> per byte + BigDecimal normalByteFee = minByteFee.add(new BigDecimal(0.00000010)); + BigDecimal maxByteFee = minByteFee.add(new BigDecimal(0.00000025)); - //compare fee to usual relay fee -// if (fee.compareTo(relayFee) < 0) { -// fee = relayFee; -// } - fee = fee.setScale(8, RoundingMode.DOWN); + BigDecimal minFee = minByteFee.multiply(new BigDecimal(calcSize)).setScale(8, RoundingMode.DOWN); + BigDecimal normalFee = normalByteFee.multiply(new BigDecimal(calcSize)).setScale(8, RoundingMode.DOWN); + BigDecimal maxFee = maxByteFee.multiply(new BigDecimal(calcSize)).setScale(8, RoundingMode.DOWN); - CoinEngine.Amount minAmount = new CoinEngine.Amount(fee, ctx.getBlockchain().getCurrency()); - CoinEngine.Amount normalAmount = new CoinEngine.Amount(fee.multiply(new BigDecimal(3)), ctx.getBlockchain().getCurrency()); - CoinEngine.Amount maxAmount = new CoinEngine.Amount(fee.multiply(new BigDecimal(6)), ctx.getBlockchain().getCurrency()); + CoinEngine.Amount minAmount = new CoinEngine.Amount(minFee, ctx.getBlockchain().getCurrency()); + CoinEngine.Amount normalAmount = new CoinEngine.Amount(normalFee, ctx.getBlockchain().getCurrency()); + CoinEngine.Amount maxAmount = new CoinEngine.Amount(maxFee, ctx.getBlockchain().getCurrency()); coinData.minFee = minAmount; coinData.normalFee = normalAmount; diff --git a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java index 089f8e9624..9a12b2ab4e 100644 --- a/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/cardano/CardanoEngine.java @@ -10,8 +10,8 @@ import com.tangem.data.local.PendingTransactionsStorage; import com.tangem.data.network.ServerApiAdalite; import com.tangem.data.network.model.AdaliteResponse; import com.tangem.data.network.model.AdaliteResponseUtxo; -import com.tangem.data.network.model.TxData; -import com.tangem.data.network.model.UtxoData; +import com.tangem.data.network.model.AdaliteTxData; +import com.tangem.data.network.model.AdaliteUtxoData; import com.tangem.domain.wallet.BTCUtils; import com.tangem.domain.wallet.BalanceValidator; import com.tangem.domain.wallet.Base58; @@ -509,7 +509,7 @@ public class CardanoEngine extends CoinEngine { } @Override - public byte[][] getHashesToSign() throws Exception { + public byte[][] getHashesToSign() { byte[][] dataForSign = new byte[1][]; dataForSign[0] = dataToSign; return dataForSign; @@ -517,17 +517,17 @@ public class CardanoEngine extends CoinEngine { @Override public byte[] getRawDataToSign() throws Exception { - throw new Exception("Signing Raw Data is not supported for Cardano"); + throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName()); } @Override - public String getHashAlgToSign() { - return "sha-256x2"; + public String getHashAlgToSign() throws Exception { + throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName()); } @Override public byte[] getIssuerTransactionSignature(byte[] dataToSignByIssuer) throws Exception { - throw new Exception("Issuer validation not supported!"); + throw new Exception("Transaction validation by issuer not supported in this version"); } @Override @@ -625,8 +625,7 @@ public class CardanoEngine extends CoinEngine { if (App.pendingTransactionsStorage.hasTransactions(ctx.getCard())) { for (PendingTransactionsStorage.TransactionInfo pendingTx : App.pendingTransactionsStorage.getTransactions(ctx.getCard()).getTransactions()) { String pendingId = CalculateTxHash(pendingTx.getTx()); - int x = 0; - for (TxData walletTx : adaliteResponse.getRight().getCaTxList()) { + for (AdaliteTxData walletTx : adaliteResponse.getRight().getCaTxList()) { if (walletTx.getCtbId().equals(pendingId)) { App.pendingTransactionsStorage.removeTransaction(ctx.getCard(), pendingTx.getTx()); } @@ -652,7 +651,7 @@ public class CardanoEngine extends CoinEngine { Log.i(TAG, "onSuccess: " + method); try { coinData.getUnspentOutputs().clear(); - for (UtxoData utxo : adaliteResponseUtxo.getRight()) { + for (AdaliteUtxoData utxo : adaliteResponseUtxo.getRight()) { CardanoData.UnspentOutput unspentOutput = new CardanoData.UnspentOutput(); unspentOutput.txID = utxo.getCuId(); unspentOutput.Amount = utxo.getCuCoins().getGetCoin(); @@ -668,7 +667,7 @@ public class CardanoEngine extends CoinEngine { blockchainRequestsCallbacks.onComplete(!ctx.hasError()); } else { blockchainRequestsCallbacks.onProgress(); - Log.e(TAG, "FAIL INSIGHT_UNSPENT_OUTPUTS Exception"); + Log.e(TAG, "FAIL ADALITE_UNSPENT_OUTPUTS Exception"); } } @@ -676,17 +675,14 @@ public class CardanoEngine extends CoinEngine { public void onSuccess(String method, List listResponse) { Log.e(TAG, "Wrong response type for requestBalanceAndUnspentTransactions"); ctx.setError("Wrong response type for requestBalanceAndUnspentTransactions"); + blockchainRequestsCallbacks.onComplete(false); } @Override public void onFail(String method, String message) { Log.i(TAG, "onFail: " + method + " " + message); ctx.setError(message); - if (serverApiAdalite.isRequestsSequenceCompleted()) { - blockchainRequestsCallbacks.onComplete(false); - } else { - blockchainRequestsCallbacks.onProgress(); - } + blockchainRequestsCallbacks.onComplete(false); } }; diff --git a/app/src/main/java/com/tangem/domain/wallet/ltc/LitecoinNode.kt b/app/src/main/java/com/tangem/domain/wallet/ltc/LitecoinNode.kt index 12d59de976..fce1899f72 100644 --- a/app/src/main/java/com/tangem/domain/wallet/ltc/LitecoinNode.kt +++ b/app/src/main/java/com/tangem/domain/wallet/ltc/LitecoinNode.kt @@ -4,7 +4,5 @@ enum class LitecoinNode(val host: String, val port: Int, val proto: String) { N_001("backup.electrum-ltc.org", 443, "ssl"), N_002("electrum-ltc.petrkr.net", 60002, "ssl"), N_003("electrum-ltc.bysh.me", 50002, "ssl"), - N_004("e-1.claudioboxx.com", 50004, "ssl"), - N_005("e-2.claudioboxx.com", 50004, "ssl"), - N_006("electrum.ltc.xurious.com", 50002, "ssl"), + N_004("electrum.ltc.xurious.com", 50002, "ssl"), } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/domain/wallet/rsk/RskEngine.java b/app/src/main/java/com/tangem/domain/wallet/rsk/RskEngine.java index fd67467442..a82a04e799 100644 --- a/app/src/main/java/com/tangem/domain/wallet/rsk/RskEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/rsk/RskEngine.java @@ -56,6 +56,11 @@ public class RskEngine extends EthEngine { return Uri.parse("https://explorer.rsk.co/address/" + ctx.getCoinData().getWallet()); } + @Override + public String evaluateFeeEquivalent(String fee) { + return ""; + } + @Override public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) { final ServerApiRootstock serverApiRootstock = new ServerApiRootstock(); diff --git a/app/src/main/java/com/tangem/domain/wallet/rsk/RskTokenEngine.java b/app/src/main/java/com/tangem/domain/wallet/rsk/RskTokenEngine.java index e23a529db2..c71992239d 100644 --- a/app/src/main/java/com/tangem/domain/wallet/rsk/RskTokenEngine.java +++ b/app/src/main/java/com/tangem/domain/wallet/rsk/RskTokenEngine.java @@ -40,7 +40,7 @@ public class RskTokenEngine extends TokenEngine { @Override public Uri getWalletExplorerUri() { return Uri.parse("https://explorer.rsk.co/address/" + ctx.getCoinData().getWallet() + "?__tab=tokens"); - } // Only RSK explorer for now + } @Override public Uri getShareWalletUri() { @@ -63,6 +63,11 @@ public class RskTokenEngine extends TokenEngine { return false; } + @Override + public String evaluateFeeEquivalent(String fee) { + return ""; + } + @Override public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) { final ServerApiRootstock serverApiRootstock = new ServerApiRootstock(); diff --git a/app/src/main/java/com/tangem/domain/wallet/xrp/XrpBase58.java b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpBase58.java new file mode 100644 index 0000000000..7a34b0d289 --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpBase58.java @@ -0,0 +1,115 @@ +package com.tangem.domain.wallet.xrp; + +import java.math.BigInteger; + +/** + * Created by Ilia on 15.02.2018. + */ + +public class XrpBase58 { + private static final char[] BASE58 = "rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz".toCharArray(); + + private static final int BASE58_CHUNK_DIGITS = 10;//how many base 58 digits fits in long + private static final BigInteger BASE58_CHUNK_MOD = BigInteger.valueOf(0x5fa8624c7fba400L); //58^BASE58_CHUNK_DIGITS + private static final byte[] BASE58_VALUES = new byte[]{-1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, -1, -1, -1, -1, -1, + -1, 9, 10, 11, 12, 13, 14, 15, 16, -1, 17, 18, 19, 20, 21, -1, + 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1, + -1, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, -1, 44, 45, 46, + 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; + + public static byte[] decodeBase58(String input) { + if (input == null) { + return null; + } + input = input.trim(); + if (input.length() == 0) { + return new byte[0]; + } + BigInteger resultNum = BigInteger.ZERO; + int nLeadingZeros = 0; + while (nLeadingZeros < input.length() && input.charAt(nLeadingZeros) == BASE58[0]) { + nLeadingZeros++; + } + long acc = 0; + int nDigits = 0; + int p = nLeadingZeros; + while (p < input.length()) { + int v = BASE58_VALUES[input.charAt(p) & 0xff]; + if (v >= 0) { + acc *= 58; + acc += v; + nDigits++; + if (nDigits == BASE58_CHUNK_DIGITS) { + resultNum = resultNum.multiply(BASE58_CHUNK_MOD).add(BigInteger.valueOf(acc)); + acc = 0; + nDigits = 0; + } + p++; + } else { + break; + } + } + if (nDigits > 0) { + long mul = 58; + while (--nDigits > 0) { + mul *= 58; + } + resultNum = resultNum.multiply(BigInteger.valueOf(mul)).add(BigInteger.valueOf(acc)); + } + final int BASE58_SPACE = -2; + while (p < input.length() && BASE58_VALUES[input.charAt(p) & 0xff] == BASE58_SPACE) { + p++; + } + if (p < input.length()) { + return null; + } + byte[] plainNumber = resultNum.toByteArray(); + int plainNumbersOffs = plainNumber[0] == 0 ? 1 : 0; + byte[] result = new byte[nLeadingZeros + plainNumber.length - plainNumbersOffs]; + System.arraycopy(plainNumber, plainNumbersOffs, result, nLeadingZeros, plainNumber.length - plainNumbersOffs); + return result; + } + + public static String encodeBase58(byte[] input) { + if (input == null) { + return null; + } + StringBuilder str = new StringBuilder((input.length * 350) / 256 + 1); + BigInteger bn = new BigInteger(1, input); + long rem; + while (true) { + BigInteger[] divideAndRemainder = bn.divideAndRemainder(BASE58_CHUNK_MOD); + bn = divideAndRemainder[0]; + rem = divideAndRemainder[1].longValue(); + if (bn.compareTo(BigInteger.ZERO) == 0) { + break; + } + for (int i = 0; i < BASE58_CHUNK_DIGITS; i++) { + str.append(BASE58[(int) (rem % 58)]); + rem /= 58; + } + } + while (rem != 0) { + str.append(BASE58[(int) (rem % 58)]); + rem /= 58; + } + str.reverse(); + int nLeadingZeros = 0; + while (nLeadingZeros < input.length && input[nLeadingZeros] == 0) { + str.insert(0, BASE58[0]); + nLeadingZeros++; + } + return str.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/domain/wallet/xrp/XrpData.java b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpData.java new file mode 100644 index 0000000000..a44db475f8 --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpData.java @@ -0,0 +1,114 @@ +package com.tangem.domain.wallet.xrp; + +import android.os.Bundle; +import android.util.Log; + +import com.tangem.domain.wallet.CoinData; +import com.tangem.domain.wallet.CoinEngine; + +import java.math.BigDecimal; + +public class XrpData extends CoinData { + public XrpData() { + super(); + } + + private Long balanceConfirmed, balanceUnconfirmed, sequence; + + private Long reserve = 20000000L; + + private Boolean accountNotFound = false; + + @Override + public void loadFromBundle(Bundle B) { + super.loadFromBundle(B); + + if (B.containsKey("BalanceConfirmed")) balanceConfirmed = B.getLong("BalanceConfirmed"); + else balanceConfirmed = null; + if (B.containsKey("BalanceUnconfirmed")) balanceUnconfirmed = B.getLong("BalanceUnconfirmed"); + else balanceUnconfirmed = null; + if (B.containsKey("Sequence")) sequence = B.getLong("Sequence"); + else sequence = null; + if (B.containsKey("Reserve")) reserve = B.getLong("Reserve"); + else reserve = 20000000L; + if (B.containsKey("AccoundNotFound")) accountNotFound = B.getBoolean("AccoundNotFound"); + else reserve = 20000000L; + } + + @Override + public void saveToBundle(Bundle B) { + super.saveToBundle(B); + try { + if (balanceConfirmed != null) B.putLong("BalanceConfirmed", balanceConfirmed); + if (balanceUnconfirmed != null) B.putLong("BalanceUnconfirmed", balanceUnconfirmed); + if (sequence != null) B.putLong("Sequence", sequence); + if (reserve != null) B.putLong("Reserve", reserve); + if (accountNotFound != null) B.putBoolean("AccoundNotFound", accountNotFound); + } catch (Exception e) { + Log.e("Can't save to bundle ", e.getMessage()); + } + } + + @Override + public void clearInfo() { + super.clearInfo(); + balanceConfirmed = null; + balanceUnconfirmed = null; + sequence = null; + reserve = 20000000L; + accountNotFound = false; + } + + // balanceUnconfirmed is just the latest balance, it equals balanceConfirmed if no unconfirmed transaction present + public CoinEngine.InternalAmount getBalanceInInternalUnits() { + if (balanceUnconfirmed != null) + return new CoinEngine.InternalAmount(BigDecimal.valueOf(balanceUnconfirmed).subtract(BigDecimal.valueOf(reserve)), "Drops"); + else + return new CoinEngine.InternalAmount(BigDecimal.valueOf(balanceConfirmed).subtract(BigDecimal.valueOf(reserve)), "Drops"); + } + +// public Long getBalanceUnconfirmed() { +// return balanceUnconfirmed; +// } + + public void setBalanceConfirmed(Long balance) { + this.balanceConfirmed = balance; + } + + public void setBalanceUnconfirmed(Long balance) { + this.balanceUnconfirmed = balance; + } + + public void setSequence(Long sequence) { + this.sequence = sequence; + } + + public Long getSequence() { + return sequence; + } + + public void setReserve(Long reserve) { + this.reserve = reserve; + } + + public CoinEngine.InternalAmount getReserveInInternalUnits() { + return new CoinEngine.InternalAmount(BigDecimal.valueOf(reserve), "Drops"); + } + + public boolean isAccountNotFound() { + return accountNotFound; + } + + public void setAccountNotFound(boolean accountFound) { + this.accountNotFound = accountFound; + } + + public boolean hasBalanceInfo() { + return balanceConfirmed != null || balanceUnconfirmed != null; + } + + public boolean hasUnconfirmed() { + return !balanceConfirmed.equals(balanceUnconfirmed); + } + +} diff --git a/app/src/main/java/com/tangem/domain/wallet/xrp/XrpEngine.java b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpEngine.java new file mode 100644 index 0000000000..d79b9ca4cd --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpEngine.java @@ -0,0 +1,614 @@ +package com.tangem.domain.wallet.xrp; + +import android.net.Uri; +import android.text.InputFilter; +import android.util.Log; + +import com.ripple.core.coretypes.AccountID; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.crypto.ecdsa.ECDSASignature; +import com.ripple.encodings.addresses.Addresses; +import com.ripple.utils.HashUtils; +import com.tangem.card_common.data.TangemCard; +import com.tangem.card_common.reader.CardProtocol; +import com.tangem.card_common.tasks.SignTask; +import com.tangem.card_common.util.Util; +import com.tangem.data.network.ServerApiRipple; +import com.tangem.data.network.model.RippleResponse; +import com.tangem.domain.wallet.BTCUtils; +import com.tangem.domain.wallet.BalanceValidator; +import com.tangem.domain.wallet.CoinData; +import com.tangem.domain.wallet.CoinEngine; +import com.tangem.domain.wallet.TangemContext; +import com.tangem.util.CryptoUtil; +import com.tangem.util.DecimalDigitsInputFilter; +import com.tangem.wallet.R; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Arrays; + +public class XrpEngine extends CoinEngine { + + private static final String TAG = XrpEngine.class.getSimpleName(); + + public XrpData coinData = null; + + public XrpEngine(TangemContext context) throws Exception { + super(context); + if (context.getCoinData() == null) { + coinData = new XrpData(); + context.setCoinData(coinData); + } else if (context.getCoinData() instanceof XrpData) { + coinData = (XrpData) context.getCoinData(); + } else { + throw new Exception("Invalid type of Blockchain data for XrpEngine"); + } + } + + public XrpEngine() { + super(); + } + + private static int getDecimals() { + return 6; + } + + private void checkBlockchainDataExists() throws Exception { + if (coinData == null) throw new Exception("No blockchain data"); + } + + @Override + public boolean awaitingConfirmation() { + if (coinData == null) return false; + return coinData.hasUnconfirmed(); + } + + @Override + public String getBalanceHTML() { + Amount balance = getBalance(); + if (balance != null) { + return " " + balance.toDescriptionString(getDecimals()) + "
+ " + convertToAmount(coinData.getReserveInInternalUnits()).toDescriptionString(getDecimals()) + " reserve"; + } else { + return ""; + } + } + + @Override + public String getBalanceCurrency() { + return "XRP"; + } + + @Override + public String getOfflineBalanceHTML() { + InternalAmount offlineInternalAmount = convertToInternalAmount(ctx.getCard().getOfflineBalance()); + Amount offlineAmount = convertToAmount(offlineInternalAmount); + return offlineAmount.toDescriptionString(getDecimals()); + } + + @Override + public boolean isBalanceNotZero() { + if (coinData == null) return false; + if (coinData.getBalanceInInternalUnits() == null) return false; + return coinData.getBalanceInInternalUnits().notZero(); + } + + @Override + public boolean hasBalanceInfo() { + if (coinData == null) return false; + return coinData.hasBalanceInfo(); + } + + public boolean isExtractPossible() { + if (!hasBalanceInfo()) { + ctx.setMessage(R.string.cannot_obtain_data_from_blockchain); + } else if (!isBalanceNotZero()) { + ctx.setMessage(R.string.wallet_empty); + } else if (awaitingConfirmation()) { + ctx.setMessage(R.string.please_wait_while_previous); + } else { + return true; + } + return false; + } + + @Override + public String getFeeCurrency() { + return "XRP"; + } + + @Override + public boolean validateAddress(String address) { + if (address == null || address.isEmpty()) { + return false; + } + + if (address.length() < 25) { + return false; + } + + if (address.length() > 35) { + return false; + } + + if (!address.startsWith("r")) { + return false; + } + try { + Addresses.decodeAccountID(address); + } catch (Exception e) { + return false; + } + + return true; + } + + @Override + public boolean isNeedCheckNode() { + return true; + } + + @Override + public Uri getWalletExplorerUri() { + return Uri.parse("https://xrpscan.com/account/" + ctx.getCoinData().getWallet()); + } + + public Uri getShareWalletUri() { + return Uri.parse("ripple:" + ctx.getCoinData().getWallet()); + } + + @Override + public InputFilter[] getAmountInputFilters() { + return new InputFilter[]{new DecimalDigitsInputFilter(getDecimals())}; + } + + @Override + public boolean checkNewTransactionAmount(Amount amount) { + if (coinData == null) return false; + if (amount.compareTo(convertToAmount(coinData.getBalanceInInternalUnits())) > 0) { + return false; + } + return true; + } + + @Override + public boolean checkNewTransactionAmountAndFee(Amount amountValue, Amount feeValue, Boolean isIncludeFee) { + InternalAmount fee; + InternalAmount amount; + + try { + checkBlockchainDataExists(); + amount = convertToInternalAmount(amountValue); + fee = convertToInternalAmount(feeValue); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + + if (fee == null || amount == null) + return false; + + if (fee.isZero() || amount.isZero()) + return false; + + if (isIncludeFee && (amount.compareTo(coinData.getBalanceInInternalUnits()) > 0 || amount.compareTo(fee) < 0)) + return false; + + if (!isIncludeFee && amount.add(fee).compareTo(coinData.getBalanceInInternalUnits()) > 0) + return false; + + return true; + } + + @Override + public boolean validateBalance(BalanceValidator balanceValidator) { + try { + if (coinData.isAccountNotFound()) { + balanceValidator.setScore(0); + balanceValidator.setFirstLine("Account not found"); + balanceValidator.setSecondLine("Load 20+ XRP to create account"); + return false; + } + + if (((ctx.getCard().getOfflineBalance() == null) && !ctx.getCoinData().isBalanceReceived()) || (!ctx.getCoinData().isBalanceReceived() && (ctx.getCard().getRemainingSignatures() != ctx.getCard().getMaxSignatures()))) { + balanceValidator.setScore(0); + balanceValidator.setFirstLine("Unknown balance"); + balanceValidator.setSecondLine("Balance cannot be verified. Swipe down to refresh."); + return false; + } + + if (coinData.hasUnconfirmed()) { + balanceValidator.setScore(0); + balanceValidator.setFirstLine("Transaction in progress"); + balanceValidator.setSecondLine("Wait for confirmation in blockchain"); + return false; + } + + if (coinData.isBalanceReceived()) { + balanceValidator.setScore(100); + balanceValidator.setFirstLine("Verified balance"); + balanceValidator.setSecondLine("Balance confirmed in blockchain"); + if (coinData.getBalanceInInternalUnits().isZero()) { + balanceValidator.setFirstLine("Empty wallet"); + balanceValidator.setSecondLine(""); + } + } + + if ((ctx.getCard().getOfflineBalance() != null) && !coinData.isBalanceReceived() && (ctx.getCard().getRemainingSignatures() == ctx.getCard().getMaxSignatures()) && coinData.getBalanceInInternalUnits().notZero()) { + balanceValidator.setScore(80); + balanceValidator.setFirstLine("Verified offline balance"); + balanceValidator.setSecondLine("Can't obtain balance from blockchain. Restore internet connection to be more confident. "); + } + + return true; + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + @Override + public Amount getBalance() { + if (!hasBalanceInfo()) return null; + return convertToAmount(coinData.getBalanceInInternalUnits()); + } + + @Override + public String evaluateFeeEquivalent(String fee) { + if (!coinData.getAmountEquivalentDescriptionAvailable()) return ""; + try { + Amount feeAmount = new Amount(fee, getFeeCurrency()); + return feeAmount.toEquivalentString(coinData.getRate()); + } catch (Exception e) { + return ""; + } + } + + @Override + public String getBalanceEquivalent() { + if (coinData == null || !coinData.getAmountEquivalentDescriptionAvailable()) return ""; + Amount balance = getBalance(); + if (balance == null) return ""; + return balance.toEquivalentString(coinData.getRate()); + } + + public String calculateAddress(byte[] pkCompressed) throws Exception { + byte[] canonisedPubKey = canonisePubKey(pkCompressed); + byte[] accountId = CryptoUtil.sha256ripemd160(canonisedPubKey); + + return Addresses.encodeAccountID(accountId); + } + + @Override + public Amount convertToAmount(InternalAmount internalAmount) { + BigDecimal d = internalAmount.divide(new BigDecimal("1000000")); + return new Amount(d, getBalanceCurrency()); + } + + @Override + public Amount convertToAmount(String strAmount, String currency) { + return new Amount(strAmount, currency); + } + + @Override + public InternalAmount convertToInternalAmount(Amount amount) { + BigDecimal d = amount.multiply(new BigDecimal("1000000")); + return new InternalAmount(d, "Drops"); + } + + @Override + public InternalAmount convertToInternalAmount(byte[] bytes) { + if (bytes == null) return null; + byte[] reversed = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) reversed[i] = bytes[bytes.length - i - 1]; + return new InternalAmount(Util.byteArrayToLong(reversed), "Drops"); + } + + @Override + public byte[] convertToByteArray(InternalAmount internalAmount) { + byte[] bytes = Util.longToByteArray(internalAmount.longValueExact()); + return bytes; + } + + @Override + public CoinData createCoinData() { + return new XrpData(); + } + + @Override + public String getUnspentInputsDescription() { + return ""; + } + + @Override + public void defineWallet() throws CardProtocol.TangemException { + try { + String wallet = calculateAddress(ctx.getCard().getWalletPublicKeyRar()); + ctx.getCoinData().setWallet(wallet); + } catch (Exception e) { + ctx.getCoinData().setWallet("ERROR"); + throw new CardProtocol.TangemException("Can't define wallet address"); + } + } + + public byte[] canonisePubKey(byte[] pkCompressed) throws Exception { + byte[] canonicalPubKey = new byte[33]; + + if (pkCompressed.length == 32) { + canonicalPubKey[0] = (byte) 0xED; + System.arraycopy(pkCompressed, 0, canonicalPubKey, 1, 32); + } else if (pkCompressed.length == 33) + canonicalPubKey = pkCompressed; + else + throw new Exception("Invalid pubkey length"); + return canonicalPubKey; + } + + @Override + public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception { + checkBlockchainDataExists(); + + String amount, fee; + + if (IncFee) { + amount = convertToInternalAmount(amountValue).subtract(convertToInternalAmount(feeValue)).setScale(0).toPlainString(); + } else { + amount = Long.toString(convertToInternalAmount(amountValue).longValueExact()); + } + + fee = Long.toString(convertToInternalAmount(feeValue).longValueExact()); + + XrpPayment payment = new XrpPayment(); + + // Put `as` AccountID field Account, `Object` o + payment.as(AccountID.Account, coinData.getWallet()); + payment.as(AccountID.Destination, targetAddress); + payment.as(com.ripple.core.coretypes.Amount.Amount, amount); + payment.as(UInt32.Sequence, coinData.getSequence()); + payment.as(com.ripple.core.coretypes.Amount.Fee, fee); + + XrpSignedTransaction signedTx = payment.prepare(canonisePubKey(ctx.getCard().getWalletPublicKeyRar())); + + return new SignTask.TransactionToSign() { + + @Override + public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) { + return signingMethod == TangemCard.SigningMethod.Sign_Hash; + } + + @Override + public byte[][] getHashesToSign() throws Exception { + byte[][] dataForSign = new byte[1][]; + if (ctx.getCard().getWalletPublicKeyRar().length == 33) + dataForSign[0] = HashUtils.halfSha512(signedTx.signingData); + else if (ctx.getCard().getWalletPublicKeyRar().length == 32) + dataForSign[0] = signedTx.signingData; + else + throw new Exception("Invalid pubkey length"); + return dataForSign; + } + + @Override + public byte[] getRawDataToSign() throws Exception { + throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName()); + } + + @Override + public String getHashAlgToSign() throws Exception { + throw new Exception("Signing of raw transaction not supported for " + this.getClass().getSimpleName()); + } + + @Override + public byte[] getIssuerTransactionSignature(byte[] dataToSignByIssuer) throws Exception { + throw new Exception("Transaction validation by issuer not supported in this version"); + } + + @Override + public byte[] onSignCompleted(byte[] signFromCard) throws Exception { + if (ctx.getCard().getWalletPublicKeyRar().length == 33) { + int size = signFromCard.length / 2; + BigInteger r = new BigInteger(1, Arrays.copyOfRange(signFromCard, 0, size)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(signFromCard, size, size * 2)); + s = CryptoUtil.toCanonicalised(s); + ECDSASignature sig = new ECDSASignature(r, s); + byte[] sigDer = sig.encodeToDER(); + if (!ECDSASignature.isStrictlyCanonical(sigDer)) { + throw new IllegalStateException("Signature is not strictly canonical"); + } + signedTx.addSign(sigDer); + } else if (ctx.getCard().getWalletPublicKeyRar().length == 32) + signedTx.addSign(signFromCard); + else + throw new Exception("Invalid pubkey length"); + byte[] txForSend = BTCUtils.fromHex(signedTx.tx_blob); + notifyOnNeedSendTransaction(txForSend); + return txForSend; + } + }; + } + + public void requestBalanceAndUnspentTransactions(BlockchainRequestsCallbacks blockchainRequestsCallbacks) { + final ServerApiRipple serverApiRipple = new ServerApiRipple(); + + ServerApiRipple.ResponseListener rippleListener = new ServerApiRipple.ResponseListener() { + @Override + public void onSuccess(String method, RippleResponse rippleResponse) { + Log.i(TAG, "onSuccess: " + method); + switch (method) { + case ServerApiRipple.RIPPLE_ACCOUNT_INFO: { + try { + if (rippleResponse.getResult().getAccount_data() != null) { + String walletAddress = rippleResponse.getResult().getAccount_data().getAccount(); + if (!walletAddress.equals(coinData.getWallet())) { + throw new Exception("Invalid wallet address in answer!"); + } + coinData.setBalanceConfirmed(Long.parseLong(rippleResponse.getResult().getAccount_data().getBalance())); + } else if (rippleResponse.getResult().getError_code().equals(19)) // "Account not found" + coinData.setAccountNotFound(true); + } catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, "FAIL RIPPLE_ACCOUNT_INFO Exception"); + } + } + break; + + case ServerApiRipple.RIPPLE_ACCOUNT_UNCONFIRMED: { + try { + String walletAddress = rippleResponse.getResult().getAccount_data().getAccount(); + if (!walletAddress.equals(coinData.getWallet())) { + throw new Exception("Invalid wallet address in answer!"); + } + + coinData.setBalanceReceived(true); + coinData.setBalanceUnconfirmed(Long.parseLong(rippleResponse.getResult().getAccount_data().getBalance())); + coinData.setSequence(rippleResponse.getResult().getAccount_data().getSequence()); + coinData.setValidationNodeDescription(ServerApiRipple.lastNode); + +// //check pending +// if (App.pendingTransactionsStorage.hasTransactions(ctx.getCard())) { +// for (PendingTransactionsStorage.TransactionInfo pendingTx : App.pendingTransactionsStorage.getTransactions(ctx.getCard()).getTransactions()) { +// String pendingId = CalculateTxHash(pendingTx.getTx()); +// int x = 0; +// for (AdaliteTxData walletTx : adaliteResponse.getRight().getCaTxList()) { +// if (walletTx.getCtbId().equals(pendingId)) { +// App.pendingTransactionsStorage.removeTransaction(ctx.getCard(), pendingTx.getTx()); +// } +// } +// } +// } + + } catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, "FAIL RIPPLE_ACCOUNT_INFO Exception"); + } + } + break; + + case ServerApiRipple.RIPPLE_SERVER_STATE: { + try { + coinData.setReserve(rippleResponse.getResult().getState().getValidated_ledger().getReserve_base()); + } catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, "FAIL RIPPLE_ACCOUNT_INFO Exception"); + } + } + break; + } + + if (serverApiRipple.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(!ctx.hasError()); + } else { + blockchainRequestsCallbacks.onProgress(); + } + } + + @Override + public void onFail(String method, String message) { + Log.i(TAG, "onFail: " + method + " " + message); + ctx.setError(message); + if (serverApiRipple.isRequestsSequenceCompleted()) { + blockchainRequestsCallbacks.onComplete(false); + } else { + blockchainRequestsCallbacks.onProgress(); + } + } + }; + + serverApiRipple.setResponseListener(rippleListener); + + serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, coinData.getWallet(), ""); + serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_UNCONFIRMED, coinData.getWallet(), ""); + serverApiRipple.requestData(ServerApiRipple.RIPPLE_SERVER_STATE, "", ""); + } + + @Override + public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) { + final ServerApiRipple serverApiRipple = new ServerApiRipple(); + + ServerApiRipple.ResponseListener rippleListener = new ServerApiRipple.ResponseListener() { + @Override + public void onSuccess(String method, RippleResponse rippleResponse) { + try { + InternalAmount minFee = new InternalAmount(Long.valueOf(rippleResponse.getResult().getDrops().getMinimum_fee()), "Drops"); + InternalAmount normalFee = new InternalAmount(Long.valueOf(rippleResponse.getResult().getDrops().getOpen_ledger_fee()), "Drops"); + InternalAmount maxFee = new InternalAmount(Long.valueOf(rippleResponse.getResult().getDrops().getMedian_fee()), "Drops"); + + coinData.minFee = convertToAmount(minFee); + coinData.normalFee = convertToAmount(normalFee); + coinData.maxFee = convertToAmount(maxFee); + + blockchainRequestsCallbacks.onComplete(true); + } catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, "FAIL RIPPLE_FEE Exception"); + } + } + + @Override + public void onFail(String method, String message) { + Log.i(TAG, "onFail: " + method + " " + message); + ctx.setError(message); + blockchainRequestsCallbacks.onComplete(false); + } + }; + + serverApiRipple.setResponseListener(rippleListener); + + serverApiRipple.requestData(ServerApiRipple.RIPPLE_FEE, "", ""); + } + + @Override + public void requestSendTransaction(BlockchainRequestsCallbacks blockchainRequestsCallbacks, byte[] txForSend) { + final String txStr = BTCUtils.toHex(txForSend); + + final ServerApiRipple serverApiRipple = new ServerApiRipple(); + + final ServerApiRipple.ResponseListener responseListener = new ServerApiRipple.ResponseListener() { + @Override + public void onSuccess(String method, RippleResponse rippleResponse) { + try { + if (rippleResponse.getResult().getEngine_result_code() != null) { + if (rippleResponse.getResult().getEngine_result_code() == 0) { + ctx.setError(null); + blockchainRequestsCallbacks.onComplete(true); + } else { + ctx.setError(rippleResponse.getResult().getEngine_result_message()); + blockchainRequestsCallbacks.onComplete(false); + } + } else { + ctx.setError(rippleResponse.getResult().getError() + " - " + rippleResponse.getResult().getError_exception()); + blockchainRequestsCallbacks.onComplete(false); + } + } catch (Exception e) { + if (e.getMessage() != null) { + ctx.setError(e.getMessage()); + blockchainRequestsCallbacks.onComplete(false); + } else { + ctx.setError(e.getClass().getName()); + blockchainRequestsCallbacks.onComplete(false); + Log.e(TAG, rippleResponse.toString()); + } + } + } + + @Override + public void onFail(String method, String message) { + ctx.setError(message); + blockchainRequestsCallbacks.onComplete(false); + } + }; + serverApiRipple.setResponseListener(responseListener); + + serverApiRipple.requestData(ServerApiRipple.RIPPLE_SUBMIT, "", txStr); + } + + @Override + public int pendingTransactionTimeoutInSeconds() { + return 10; + } + + public boolean needMultipleLinesForBalance() { + return true; + } + +} diff --git a/app/src/main/java/com/tangem/domain/wallet/xrp/XrpPayment.java b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpPayment.java new file mode 100644 index 0000000000..449d755e65 --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpPayment.java @@ -0,0 +1,18 @@ +package com.tangem.domain.wallet.xrp; + + +import com.ripple.core.types.known.tx.txns.Payment; + +public class XrpPayment extends Payment { + + public XrpPayment() { + super(); + } + + public XrpSignedTransaction prepare(byte[] pubKeyBytes) { + XrpSignedTransaction tx = XrpSignedTransaction.fromTx(this); + tx.prepare(pubKeyBytes); + return tx; + } + +} diff --git a/app/src/main/java/com/tangem/domain/wallet/xrp/XrpSignedTransaction.java b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpSignedTransaction.java new file mode 100644 index 0000000000..29babc095b --- /dev/null +++ b/app/src/main/java/com/tangem/domain/wallet/xrp/XrpSignedTransaction.java @@ -0,0 +1,81 @@ +package com.tangem.domain.wallet.xrp; + +import com.ripple.core.coretypes.Amount; +import com.ripple.core.coretypes.Blob; +import com.ripple.core.coretypes.STObject; +import com.ripple.core.coretypes.hash.HalfSha512; +import com.ripple.core.coretypes.hash.prefixes.HashPrefix; +import com.ripple.core.coretypes.uint.UInt32; +import com.ripple.core.serialized.BytesList; +import com.ripple.core.serialized.MultiSink; +import com.ripple.core.types.known.tx.Transaction; +import com.ripple.core.types.known.tx.signed.SignedTransaction; + +import java.util.Arrays; + +public class XrpSignedTransaction extends SignedTransaction { + private XrpSignedTransaction(Transaction of) { + txn = (Transaction) STObject.fromBytes(of.toBytes()); + } + + protected XrpSignedTransaction() { + } + + public static XrpSignedTransaction fromTx(Transaction tx) { + return new XrpSignedTransaction(tx); + } + + public void prepare(byte[] pubKeyBytes) { + prepare(pubKeyBytes, null, null, null); + } + + public void prepare(byte[] pubKeyBytes, + Amount fee, + UInt32 Sequence, + UInt32 lastLedgerSequence) { + + Blob pubKey = new Blob(pubKeyBytes); + + // This won't always be specified + if (lastLedgerSequence != null) { + txn.put(UInt32.LastLedgerSequence, lastLedgerSequence); + } + if (Sequence != null) { + txn.put(UInt32.Sequence, Sequence); + } + if (fee != null) { + txn.put(Amount.Fee, fee); + } + + txn.signingPubKey(pubKey); + + if (Transaction.CANONICAL_FLAG_DEPLOYED) { + txn.setCanonicalSignatureFlag(); + } + + txn.checkFormat(); + signingData = txn.signingData(); + if (previousSigningData != null && Arrays.equals(signingData, previousSigningData)) { + return; + } + } + + public void addSign(byte[] signature) { + try { + txn.txnSignature(new Blob(signature)); + + BytesList blob = new BytesList(); + HalfSha512 id = HalfSha512.prefixed256(HashPrefix.transactionID); + + txn.toBytesSink(new MultiSink(blob, id)); + tx_blob = blob.bytesHex(); + hash = id.finish(); + } catch (Exception e) { + // electric paranoia + previousSigningData = null; + throw new RuntimeException(e); + } /*else {*/ + previousSigningData = signingData; + // } + } +} diff --git a/app/src/main/java/com/tangem/ui/fragment/LoadedWallet.kt b/app/src/main/java/com/tangem/ui/fragment/LoadedWallet.kt index 7695c2027f..1a71db7376 100644 --- a/app/src/main/java/com/tangem/ui/fragment/LoadedWallet.kt +++ b/app/src/main/java/com/tangem/ui/fragment/LoadedWallet.kt @@ -143,7 +143,7 @@ class LoadedWallet : androidx.fragment.app.Fragment(), NfcAdapter.ReaderCallback tvWallet.setOnClickListener { doShareWallet(false) } - btnExplore.setOnClickListener { startActivity(Intent(Intent.ACTION_VIEW, engine?.walletExplorerUri)) } + btnExplore.setOnClickListener { startActivity(Intent(Intent.ACTION_VIEW, engine.walletExplorerUri)) } btnCopy.setOnClickListener { doShareWallet(false) } @@ -594,7 +594,7 @@ class LoadedWallet : androidx.fragment.app.Fragment(), NfcAdapter.ReaderCallback tvBalance.text = html } - else -> tvBalance.text = getString(R.string.no_data_string) + else -> tvBalance.text = "" } tvWallet.text = ctx.coinData!!.wallet @@ -726,6 +726,7 @@ class LoadedWallet : androidx.fragment.app.Fragment(), NfcAdapter.ReaderCallback Blockchain.Rootstock -> "bitcoin" Blockchain.RootstockToken -> "bitcoin" Blockchain.Cardano -> "cardano" + Blockchain.Ripple -> "ripple" else -> { throw Exception("Can''t get rate for blockchain " + ctx.blockchainName) } diff --git a/app/src/main/res/layout/activity_confirm_transaction.xml b/app/src/main/res/layout/activity_confirm_transaction.xml index 6e10638831..5a48672aa3 100644 --- a/app/src/main/res/layout/activity_confirm_transaction.xml +++ b/app/src/main/res/layout/activity_confirm_transaction.xml @@ -139,7 +139,7 @@ android:layout_marginBottom="1dp" android:fontFamily="@font/maax" android:textColor="@color/primary" - android:textSize="14sp" + android:textSize="14dp" app:layout_constraintBottom_toBottomOf="@+id/textView3" app:layout_constraintLeft_toRightOf="@+id/textView3" tools:text="(including fee)" /> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index cc0bdcca10..48a209311f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -30,7 +30,7 @@ android:layout_marginTop="12dp" android:fontFamily="@font/maax" android:gravity="center" - android:lineSpacingExtra="8sp" + android:lineSpacingExtra="8dp" android:minHeight="200dp" android:paddingLeft="20dp" android:paddingTop="20dp" diff --git a/app/src/main/res/layout/activity_pin_request.xml b/app/src/main/res/layout/activity_pin_request.xml index 3bdeda2f16..d0a3f210a5 100644 --- a/app/src/main/res/layout/activity_pin_request.xml +++ b/app/src/main/res/layout/activity_pin_request.xml @@ -15,7 +15,7 @@ android:paddingBottom="16dp" android:singleLine="false" android:textAlignment="center" - android:textSize="36sp" + android:textSize="36dp" tools:ignore="TextViewEdits" tools:text="1234" /> diff --git a/app/src/main/res/layout/activity_pin_save.xml b/app/src/main/res/layout/activity_pin_save.xml index 7475cab91a..c9c3d906bc 100644 --- a/app/src/main/res/layout/activity_pin_save.xml +++ b/app/src/main/res/layout/activity_pin_save.xml @@ -15,7 +15,7 @@ android:paddingBottom="8dp" android:singleLine="false" android:textAlignment="center" - android:textSize="36sp" + android:textSize="36dp" tools:ignore="TextViewEdits" /> diff --git a/app/src/main/res/layout/layout_tangem_card.xml b/app/src/main/res/layout/layout_tangem_card.xml index e7bc7d65d1..9b4136a6df 100644 --- a/app/src/main/res/layout/layout_tangem_card.xml +++ b/app/src/main/res/layout/layout_tangem_card.xml @@ -42,7 +42,7 @@ android:fontFamily="@font/maaxmedium" android:text="@string/verifying" android:textColor="@color/colorPrimary" - android:textSize="12sp" + android:textSize="12dp" android:textStyle="bold" /> 140dp 16dp - 11sp - 13sp - 15sp - 19sp - 23sp - 26sp - 34sp + 11dp + 13dp + 15dp + 19dp + 23dp + 26dp + 34dp 180dp 30dp diff --git a/card-common/src/main/java/com/tangem/card_common/tasks/SignTask.java b/card-common/src/main/java/com/tangem/card_common/tasks/SignTask.java index 7d4a0d5d8e..d3eebd6bda 100644 --- a/card-common/src/main/java/com/tangem/card_common/tasks/SignTask.java +++ b/card-common/src/main/java/com/tangem/card_common/tasks/SignTask.java @@ -85,6 +85,7 @@ public class SignTask extends CustomReadCardTask { } transactionToSign.onSignCompleted(signResult.getTLV(TLV.Tag.TAG_Signature).Value); + //TODO: maybe we should move notifyOnNeedSendTransaction(txForSend) here? mNotifications.onReadProgress(protocol, 100); } diff --git a/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt b/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt index eeed2c053e..f7a1d91c9a 100644 --- a/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt +++ b/server-android/src/main/java/com/tangem/server_android/data/LocalStorage.kt @@ -49,7 +49,7 @@ class LocalStorage artworks = HashMap() } - if (artworks.count() < 21) { + if (artworks.count() < 23) { // forceSave=true only on the last one putResourceArtworkToCatalog(R.drawable.card_default, false) putResourceArtworkToCatalog(R.drawable.card_default_nft, false) @@ -71,6 +71,8 @@ class LocalStorage putResourceArtworkToCatalog(R.drawable.card_ru030, false) putResourceArtworkToCatalog(R.drawable.card_ru031, false) putResourceArtworkToCatalog(R.drawable.card_ru032, false) + putResourceArtworkToCatalog(R.drawable.card_ru037, false) + putResourceArtworkToCatalog(R.drawable.card_ru039, false) putResourceArtworkToCatalog(R.drawable.card_ff32, true) } if (batchesFile.exists()) { @@ -244,6 +246,8 @@ class LocalStorage card.batch == "0020" -> R.drawable.card_ru030 card.batch == "0021" -> R.drawable.card_ru031 card.batch == "0022" -> R.drawable.card_ru032 + card.batch == "0025" -> R.drawable.card_ru037 + card.batch == "0026" -> R.drawable.card_ru039 card.batch == "FF32" -> R.drawable.card_ff32 else -> null diff --git a/server-android/src/main/res/drawable/card_ru037.png b/server-android/src/main/res/drawable/card_ru037.png new file mode 100644 index 0000000000..362fbca5b9 Binary files /dev/null and b/server-android/src/main/res/drawable/card_ru037.png differ diff --git a/server-android/src/main/res/drawable/card_ru039.png b/server-android/src/main/res/drawable/card_ru039.png new file mode 100644 index 0000000000..a508efef6c Binary files /dev/null and b/server-android/src/main/res/drawable/card_ru039.png differ