Updated on 2026-08-14
This commit is contained in:
parent
c77a38ace4
commit
2adf3e50a6
7 changed files with 218 additions and 52 deletions
|
|
@ -10,6 +10,6 @@ import retrofit2.http.POST;
|
|||
|
||||
public interface RippleApi {
|
||||
@Headers("Content-Type: application/json")
|
||||
@POST
|
||||
@POST("./")
|
||||
Call<RippleResponse> ripple(@Body RippleBody body);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public class ServerApiRipple {
|
|||
private ResponseListener responseListener;
|
||||
|
||||
public interface ResponseListener {
|
||||
void onSuccess(String method, RippleResponse infuraResponse);
|
||||
void onSuccess(String method, RippleResponse rippleResponse);
|
||||
|
||||
void onFail(String method, String message);
|
||||
}
|
||||
|
|
@ -45,9 +45,9 @@ public class ServerApiRipple {
|
|||
responseListener = listener;
|
||||
}
|
||||
|
||||
public void requestData(String method, int id, String wallet, String tx) {
|
||||
public void requestData(String method, String wallet, String tx) {
|
||||
requestsCount++;
|
||||
String rippleURL = "http://s1.ripple.com:51234/"; //TODO: make random selection
|
||||
String rippleURL = "https://s1.ripple.com:51234"; //TODO: make random selection
|
||||
lastNode = rippleURL; //TODO: show node instead of URL
|
||||
|
||||
Retrofit retrofitRipple = new Retrofit.Builder()
|
||||
|
|
|
|||
|
|
@ -19,12 +19,21 @@ data class RippleResult(
|
|||
var drops: FeeDrops? = null,
|
||||
|
||||
//for RIPPLE_SUBMIT
|
||||
@SerializedName("engine_result")
|
||||
var engine_result: String? = null,
|
||||
@SerializedName("engine_result_code")
|
||||
var engine_result_code: Int? = null,
|
||||
|
||||
//for RIPPLE_SUBMIT
|
||||
@SerializedName ("engine_result_message")
|
||||
var engine_result_message: String? = null
|
||||
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
|
||||
|
||||
)
|
||||
|
||||
data class RippleAccountData(
|
||||
|
|
@ -35,7 +44,7 @@ data class RippleAccountData(
|
|||
var balance: String? = null,
|
||||
|
||||
@SerializedName("Sequence")
|
||||
var sequence: Int? = null
|
||||
var sequence: Long? = null
|
||||
)
|
||||
|
||||
data class FeeDrops(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ public class XrpData extends CoinData {
|
|||
this.sequence = sequence;
|
||||
}
|
||||
|
||||
public Long getSequence() {
|
||||
return sequence;
|
||||
}
|
||||
|
||||
public boolean hasBalanceInfo() {
|
||||
return balanceConfirmed != null || balanceUnconfirmed != null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,19 @@ 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.core.types.known.tx.signed.SignedTransaction;
|
||||
import com.ripple.core.types.known.tx.txns.Payment;
|
||||
import com.ripple.encodings.addresses.Addresses;
|
||||
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;
|
||||
|
|
@ -23,6 +27,7 @@ import com.tangem.wallet.R;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.util.Arrays;
|
||||
|
|
@ -133,23 +138,9 @@ public class XrpEngine extends CoinEngine {
|
|||
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)) {
|
||||
try {
|
||||
Addresses.decodeAccountID(address);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -166,6 +157,10 @@ public class XrpEngine extends CoinEngine {
|
|||
return Uri.parse("https://xrpscan.com/account/" + ctx.getCoinData().getWallet());
|
||||
}
|
||||
|
||||
public Uri getShareWalletUri() {
|
||||
return Uri.parse(ctx.getCoinData().getWallet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputFilter[] getAmountInputFilters() {
|
||||
return new InputFilter[]{new DecimalDigitsInputFilter(getDecimals())};
|
||||
|
|
@ -274,18 +269,14 @@ public class XrpEngine extends CoinEngine {
|
|||
return balance.toEquivalentString(coinData.getRate());
|
||||
}
|
||||
|
||||
public String calculateAddress(byte[] pkCompressed) throws NoSuchAlgorithmException, NoSuchProviderException, IOException {
|
||||
byte[] pkForHash = canonicalPubBytes(pkCompressed);
|
||||
public String calculateAddress(byte[] pkCompressed) {
|
||||
byte[] canonisedPubKey = canonisePubKey(pkCompressed);
|
||||
byte[] accountId = CryptoUtil.sha256ripemd160(canonisedPubKey);
|
||||
String address = Addresses.encodeAccountID(accountId);
|
||||
|
||||
ByteArrayOutputStream address = new ByteArrayOutputStream();
|
||||
address.write((byte) 0x00);
|
||||
byte[] accountId = CryptoUtil.sha256ripemd160(pkForHash);
|
||||
address.write(accountId);
|
||||
boolean valid = validateAddress(address);
|
||||
|
||||
byte [] doubleSha256 = CryptoUtil.doubleSha256(address.toByteArray());
|
||||
address.write(Arrays.copyOfRange(doubleSha256,0,4));
|
||||
|
||||
return BTCUtils.toHex(address.toByteArray());
|
||||
return address;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -337,25 +328,23 @@ public class XrpEngine extends CoinEngine {
|
|||
try {
|
||||
String wallet = calculateAddress(ctx.getCard().getWalletPublicKeyRar());
|
||||
ctx.getCoinData().setWallet(wallet);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
} 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];
|
||||
public byte[] canonisePubKey(byte[] pkCompressed) {
|
||||
byte[] canonicalPubKey = new byte[33];
|
||||
|
||||
if (pkCompressed.length == 32) {
|
||||
pkForHash[0] = (byte) 0xED;
|
||||
System.arraycopy(pkCompressed, 0, pkForHash, 1,32);
|
||||
canonicalPubKey[0] = (byte) 0xED;
|
||||
System.arraycopy(pkCompressed, 0, canonicalPubKey, 1, 32);
|
||||
} else {
|
||||
pkForHash = pkCompressed;
|
||||
canonicalPubKey = pkCompressed;
|
||||
}
|
||||
return canonicalPubKey;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception {
|
||||
|
|
@ -364,13 +353,13 @@ public class XrpEngine extends CoinEngine {
|
|||
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");
|
||||
payment.as(AccountID.Account, coinData.getWallet());
|
||||
payment.as(AccountID.Destination, targetAddress);
|
||||
payment.as(com.ripple.core.coretypes.Amount.Amount, amountValue);
|
||||
payment.as(UInt32.Sequence, coinData.getSequence());
|
||||
payment.as(com.ripple.core.coretypes.Amount.Fee, feeValue);
|
||||
|
||||
SignedTransaction signedTx = payment.prepare(canonicalPubBytes(ctx.getCard().getWalletPublicKeyRar()));
|
||||
SignedTransaction signedTx = payment.prepare(canonisePubKey(ctx.getCard().getWalletPublicKeyRar()));
|
||||
|
||||
return new SignTask.TransactionToSign() {
|
||||
|
||||
|
|
@ -408,4 +397,162 @@ public class XrpEngine extends CoinEngine {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
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 {
|
||||
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()));
|
||||
} 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 (TxData 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;
|
||||
}
|
||||
|
||||
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(), "");
|
||||
}
|
||||
|
||||
@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) {
|
||||
BigDecimal minFee = new BigDecimal(rippleResponse.getResult().getDrops().getMinimum_fee()).divide(new BigDecimal(getDecimals()));
|
||||
BigDecimal normalFee = new BigDecimal(rippleResponse.getResult().getDrops().getOpen_ledger_fee()).divide(new BigDecimal(getDecimals()));
|
||||
BigDecimal maxFee = new BigDecimal(rippleResponse.getResult().getDrops().getMedian_fee()).divide(new BigDecimal(getDecimals()));
|
||||
|
||||
coinData.minFee = new Amount(minFee.setScale(getDecimals(), RoundingMode.UP), getFeeCurrency());
|
||||
coinData.normalFee = new Amount(normalFee.setScale(getDecimals(), RoundingMode.UP), getFeeCurrency());
|
||||
coinData.maxFee = new Amount(maxFee.setScale(getDecimals(), RoundingMode.UP), getFeeCurrency());
|
||||
|
||||
blockchainRequestsCallbacks.onComplete(true);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
@ -730,6 +730,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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue