Updated on 2026-08-14
This commit is contained in:
commit
6048def307
39 changed files with 1302 additions and 98 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -8,7 +8,6 @@
|
|||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
app/libs/
|
||||
.idea/vcs.xml
|
||||
.idea/caches
|
||||
.idea/dictionaries
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
BIN
app/libs/ripple-core-0.0.1.jar
Normal file
BIN
app/libs/ripple-core-0.0.1.jar
Normal file
Binary file not shown.
|
|
@ -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;
|
||||
|
|
|
|||
15
app/src/main/java/com/tangem/data/network/RippleApi.java
Normal file
15
app/src/main/java/com/tangem/data/network/RippleApi.java
Normal file
|
|
@ -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<RippleResponse> ripple(@Body RippleBody body);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
117
app/src/main/java/com/tangem/data/network/ServerApiRipple.java
Normal file
117
app/src/main/java/com/tangem/data/network/ServerApiRipple.java
Normal file
|
|
@ -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<String, String> 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<RippleResponse> call = rippleApi.ripple(rippleBody);
|
||||
call.enqueue(new Callback<RippleResponse>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<RippleResponse> call, @NonNull Response<RippleResponse> 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<RippleResponse> call, @NonNull Throwable t) {
|
||||
responseListener.onFail(method, String.valueOf(t.getMessage()));
|
||||
Log.e(TAG, "requestData " + method + " onFailure " + t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ public class ServerApiRootstock {
|
|||
|
||||
/**
|
||||
* HTTP
|
||||
* Infura
|
||||
* Rootstock
|
||||
* <p>
|
||||
* eth_getBalance
|
||||
* eth_getTransactionCount
|
||||
|
|
|
|||
|
|
@ -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<UtxoData>
|
||||
var right: List<AdaliteUtxoData>
|
||||
)
|
||||
|
||||
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<TxData>
|
||||
var caTxList: List<AdaliteTxData>
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
|
@ -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<HashMap<String,String>> params;
|
||||
|
||||
public RippleBody() {
|
||||
}
|
||||
|
||||
//for RIPPLE_FEE
|
||||
public RippleBody(String method) {
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public RippleBody(String method, HashMap<String,String> paramsMap) {
|
||||
this.method = method;
|
||||
ArrayList<HashMap<String, String>> paramsList = new ArrayList<>();
|
||||
paramsList.add(paramsMap);
|
||||
this.params = paramsList;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
115
app/src/main/java/com/tangem/domain/wallet/xrp/XrpBase58.java
Normal file
115
app/src/main/java/com/tangem/domain/wallet/xrp/XrpBase58.java
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
114
app/src/main/java/com/tangem/domain/wallet/xrp/XrpData.java
Normal file
114
app/src/main/java/com/tangem/domain/wallet/xrp/XrpData.java
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
614
app/src/main/java/com/tangem/domain/wallet/xrp/XrpEngine.java
Normal file
614
app/src/main/java/com/tangem/domain/wallet/xrp/XrpEngine.java
Normal file
|
|
@ -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()) + " <br><small><small>+ " + convertToAmount(coinData.getReserveInInternalUnits()).toDescriptionString(getDecimals()) + " reserve</small></small>";
|
||||
} 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
android:paddingBottom="8dp"
|
||||
android:singleLine="false"
|
||||
android:textAlignment="center"
|
||||
android:textSize="36sp"
|
||||
android:textSize="36dp"
|
||||
tools:ignore="TextViewEdits" />
|
||||
|
||||
<TextView
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@
|
|||
android:fontFamily="@font/maaxmedium"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/primary_dark"
|
||||
android:textSize="18sp"
|
||||
android:textSize="18dp"
|
||||
android:textStyle="normal"
|
||||
tools:text="Bitcoin" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
||||
<ProgressBar
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@
|
|||
android:alpha="1"
|
||||
android:text="@string/reading"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:textSize="12sp"
|
||||
android:textSize="12dp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<ProgressBar
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -17,13 +17,13 @@
|
|||
<dimen name="empty_wallet_left_col_width">140dp</dimen>
|
||||
<dimen name="empty_wallet_left_col_margin">16dp</dimen>
|
||||
|
||||
<dimen name="text_size_2_small">11sp</dimen>
|
||||
<dimen name="text_size_small">13sp</dimen>
|
||||
<dimen name="text_size_1_small">15sp</dimen>
|
||||
<dimen name="text_size_medium">19sp</dimen>
|
||||
<dimen name="text_size_1_large">23sp</dimen>
|
||||
<dimen name="text_size_large">26sp</dimen>
|
||||
<dimen name="text_size_pin">34sp</dimen>
|
||||
<dimen name="text_size_2_small">11dp</dimen>
|
||||
<dimen name="text_size_small">13dp</dimen>
|
||||
<dimen name="text_size_1_small">15dp</dimen>
|
||||
<dimen name="text_size_medium">19dp</dimen>
|
||||
<dimen name="text_size_1_large">23dp</dimen>
|
||||
<dimen name="text_size_large">26dp</dimen>
|
||||
<dimen name="text_size_pin">34dp</dimen>
|
||||
|
||||
<dimen name="size_qr_image">180dp</dimen>
|
||||
<dimen name="size_explore_image">30dp</dimen>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
BIN
server-android/src/main/res/drawable/card_ru037.png
Normal file
BIN
server-android/src/main/res/drawable/card_ru037.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
BIN
server-android/src/main/res/drawable/card_ru039.png
Normal file
BIN
server-android/src/main/res/drawable/card_ru039.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
Loading…
Add table
Add a link
Reference in a new issue