Updated on 2026-08-14

This commit is contained in:
Tangem 2019-04-08 14:34:37 +03:00
commit 6048def307
39 changed files with 1302 additions and 98 deletions

View file

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

View 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);
}

View file

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

View file

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

View 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());
}
});
}
}

View file

@ -16,7 +16,7 @@ public class ServerApiRootstock {
/**
* HTTP
* Infura
* Rootstock
* <p>
* eth_getBalance
* eth_getTransactionCount

View file

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

View file

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

View file

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

View file

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

View file

@ -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) {

View file

@ -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"),
}

View file

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

View file

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

View file

@ -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"),
}

View file

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

View file

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

View 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();
}
}

View 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);
}
}

View 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;
}
}

View file

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

View file

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

View file

@ -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)
}