Updated on 2026-08-14
This commit is contained in:
parent
ecc6243780
commit
241b29c5df
170 changed files with 13844 additions and 6 deletions
|
|
@ -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("Token", "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);
|
||||
}
|
||||
114
app/src/main/java/com/tangem/data/network/ServerApiRipple.java
Normal file
114
app/src/main/java/com/tangem/data/network/ServerApiRipple.java
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package com.tangem.data.network;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.tangem.App;
|
||||
import com.tangem.data.network.model.RippleBody;
|
||||
import com.tangem.data.network.model.RippleResponse;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
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";
|
||||
|
||||
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 infuraResponse);
|
||||
|
||||
void onFail(String method, String message);
|
||||
}
|
||||
|
||||
public void setResponseListener(ResponseListener listener) {
|
||||
responseListener = listener;
|
||||
}
|
||||
|
||||
public void requestData(String method, int id, String wallet, String tx) {
|
||||
requestsCount++;
|
||||
String rippleURL = "http://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_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());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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,52 @@
|
|||
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: FeeDrops? = null,
|
||||
|
||||
//for RIPPLE_SUBMIT
|
||||
@SerializedName("engine_result")
|
||||
var engine_result: String? = null,
|
||||
|
||||
//for RIPPLE_SUBMIT
|
||||
@SerializedName ("engine_result_message")
|
||||
var engine_result_message: String? = null
|
||||
)
|
||||
|
||||
data class RippleAccountData(
|
||||
@SerializedName("Account")
|
||||
var account: String? = null,
|
||||
|
||||
@SerializedName("Balance")
|
||||
var balance: String? = null,
|
||||
|
||||
@SerializedName("Sequence")
|
||||
var sequence: Int? = null
|
||||
)
|
||||
|
||||
data class FeeDrops(
|
||||
//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
|
||||
)
|
||||
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();
|
||||
}
|
||||
}
|
||||
78
app/src/main/java/com/tangem/domain/wallet/xrp/XrpData.java
Normal file
78
app/src/main/java/com/tangem/domain/wallet/xrp/XrpData.java
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
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;
|
||||
|
||||
@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");
|
||||
}
|
||||
|
||||
@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);
|
||||
} catch (Exception e) {
|
||||
Log.e("Can't save to bundle ", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearInfo() {
|
||||
super.clearInfo();
|
||||
balanceConfirmed = null;
|
||||
balanceUnconfirmed = null;
|
||||
sequence = null;
|
||||
}
|
||||
|
||||
// balanceUnconfirmed is just the latest balance, it equals balanceConfirmed if no unconfirmed transaction present
|
||||
public CoinEngine.InternalAmount getBalanceInInternalUnits() {
|
||||
return new CoinEngine.InternalAmount(BigDecimal.valueOf(balanceUnconfirmed), "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 boolean hasBalanceInfo() {
|
||||
return balanceConfirmed != null || balanceUnconfirmed != null;
|
||||
}
|
||||
|
||||
public boolean hasUnconfirmed() {
|
||||
return !balanceConfirmed.equals(balanceUnconfirmed);
|
||||
}
|
||||
|
||||
}
|
||||
411
app/src/main/java/com/tangem/domain/wallet/xrp/XrpEngine.java
Normal file
411
app/src/main/java/com/tangem/domain/wallet/xrp/XrpEngine.java
Normal file
|
|
@ -0,0 +1,411 @@
|
|||
package com.tangem.domain.wallet.xrp;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.InputFilter;
|
||||
|
||||
import com.ripple.core.coretypes.AccountID;
|
||||
import com.ripple.core.coretypes.uint.UInt32;
|
||||
import com.ripple.core.types.known.tx.signed.SignedTransaction;
|
||||
import com.ripple.core.types.known.tx.txns.Payment;
|
||||
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.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.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
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());
|
||||
} 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;
|
||||
}
|
||||
|
||||
byte[] decAddress = XrpBase58.decodeBase58(address);
|
||||
|
||||
if (decAddress == null || decAddress.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] payload = new byte[21];
|
||||
System.arraycopy(decAddress, 0, payload, 0, 21);
|
||||
|
||||
byte[] checksum = new byte[4];
|
||||
System.arraycopy(decAddress, 21, checksum, 0, 4);
|
||||
|
||||
byte[] calcChecksum = new byte[4];
|
||||
System.arraycopy(CryptoUtil.doubleSha256(payload),0,calcChecksum,0,4);
|
||||
|
||||
if (!Arrays.equals(checksum, calcChecksum)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNeedCheckNode() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWalletExplorerUri() {
|
||||
return Uri.parse("https://xrpscan.com/account/" + 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 (((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()) {// && coinData.isBalanceEqual()) { TODO:check
|
||||
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 NoSuchAlgorithmException, NoSuchProviderException, IOException {
|
||||
byte[] pkForHash = canonicalPubBytes(pkCompressed);
|
||||
|
||||
ByteArrayOutputStream address = new ByteArrayOutputStream();
|
||||
address.write((byte) 0x00);
|
||||
byte[] accountId = CryptoUtil.sha256ripemd160(pkForHash);
|
||||
address.write(accountId);
|
||||
|
||||
byte [] doubleSha256 = CryptoUtil.doubleSha256(address.toByteArray());
|
||||
address.write(Arrays.copyOfRange(doubleSha256,0,4));
|
||||
|
||||
return BTCUtils.toHex(address.toByteArray());
|
||||
}
|
||||
|
||||
@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());
|
||||
// byte[] reversed = new byte[bytes.length]; TODO: check if needed
|
||||
// for (int i = 0; i < bytes.length; i++) reversed[i] = bytes[bytes.length - i - 1];
|
||||
// return reversed;
|
||||
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[] canonicalPubBytes(byte[] pkCompressed) {
|
||||
byte[] pkForHash = new byte[33];
|
||||
|
||||
if (pkCompressed.length == 32) {
|
||||
pkForHash[0] = (byte) 0xED;
|
||||
System.arraycopy(pkCompressed, 0, pkForHash, 1,32);
|
||||
} else {
|
||||
pkForHash = pkCompressed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception {
|
||||
checkBlockchainDataExists();
|
||||
|
||||
Payment payment = new Payment();
|
||||
|
||||
// Put `as` AccountID field Account, `Object` o
|
||||
payment.as(AccountID.Account, "rGZG674DSZJfoY8abMPSgChxZTJZEhyMRm");
|
||||
payment.as(AccountID.Destination, "rPMh7Pi9ct699iZUTWaytJUoHcJ7cgyziK");
|
||||
payment.as(com.ripple.core.coretypes.Amount.Amount, "1000000000");
|
||||
payment.as(UInt32.Sequence, 10);
|
||||
payment.as(com.ripple.core.coretypes.Amount.Fee, "10000");
|
||||
|
||||
SignedTransaction signedTx = payment.prepare(canonicalPubBytes(ctx.getCard().getWalletPublicKeyRar()));
|
||||
|
||||
return new SignTask.TransactionToSign() {
|
||||
|
||||
@Override
|
||||
public boolean isSigningMethodSupported(TangemCard.SigningMethod signingMethod) {
|
||||
return signingMethod == TangemCard.SigningMethod.Sign_Hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[][] getHashesToSign() {
|
||||
byte[][] dataForSign = new byte[1][];
|
||||
dataForSign[0] = signedTx.signingData;
|
||||
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) {
|
||||
signedTx.addSign(signFromCard);
|
||||
return BTCUtils.fromHex(signedTx.tx_blob);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue