Updated on 2026-08-14
This commit is contained in:
parent
b476f20da5
commit
6e5b272987
12 changed files with 198 additions and 141 deletions
17
app/src/main/java/com/tangem/data/network/PayIdApi.java
Normal file
17
app/src/main/java/com/tangem/data/network/PayIdApi.java
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package com.tangem.data.network;
|
||||
|
||||
import com.tangem.data.network.model.PayIdResponse;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Headers;
|
||||
import retrofit2.http.Path;
|
||||
|
||||
public interface PayIdApi {
|
||||
@Headers({
|
||||
"Accept: application/xrpl-mainnet+json",
|
||||
"PayID-Version: 1.0"
|
||||
})
|
||||
@GET("{user}")
|
||||
Single<PayIdResponse> getAddress(@Path("user") String user);
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.data.network;
|
||||
|
||||
import com.jakewharton.retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
|
||||
import com.tangem.App;
|
||||
import com.tangem.data.network.model.PayIdResponse;
|
||||
import com.tangem.tangem_card.util.Log;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.SingleObserver;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
public class ServerApiPayId {
|
||||
private static String TAG = ServerApiPayId.class.getSimpleName();
|
||||
|
||||
private int requestsCount = 0;
|
||||
|
||||
public boolean isRequestsSequenceCompleted() {
|
||||
Log.i(TAG, String.format("isRequestsSequenceCompleted: %s (%d requests left)", String.valueOf(requestsCount <= 0), requestsCount));
|
||||
return requestsCount <= 0;
|
||||
}
|
||||
|
||||
public void getAddress(String payID, SingleObserver<PayIdResponse> addressObserver) {
|
||||
requestsCount++;
|
||||
Log.i(TAG, "new getAddress request");
|
||||
|
||||
String[] addressParts = payID.split("\\$");
|
||||
String user = addressParts[0];
|
||||
String domain = addressParts[1];
|
||||
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl("https://" + domain + "/")
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||
.build();
|
||||
|
||||
PayIdApi api = retrofit.create(PayIdApi.class);
|
||||
|
||||
Single<PayIdResponse> addressSingle = api.getAddress(user)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnEvent((object, throwable) -> requestsCount--);
|
||||
|
||||
addressSingle.subscribe(addressObserver);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.data.network.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class PayIdResponse(
|
||||
@SerializedName("addresses")
|
||||
var addresses: List<PayIdAddress>? = null
|
||||
)
|
||||
|
||||
data class PayIdAddress(
|
||||
@SerializedName("addressDetails")
|
||||
var addressDetails: PayIdAddressDetails? = null
|
||||
)
|
||||
|
||||
data class PayIdAddressDetails(
|
||||
@SerializedName("address")
|
||||
var address: String? = null
|
||||
)
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
package com.tangem.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();
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,8 @@ public class XrpData extends CoinData {
|
|||
|
||||
private Boolean accountNotFound, targetAccountCreated = false;
|
||||
|
||||
private String resolvedPayIdAddress = null;
|
||||
|
||||
@Override
|
||||
public void loadFromBundle(Bundle B) {
|
||||
super.loadFromBundle(B);
|
||||
|
|
@ -35,6 +37,8 @@ public class XrpData extends CoinData {
|
|||
else accountNotFound = false;
|
||||
if (B.containsKey("TargetAccountCreated")) targetAccountCreated = B.getBoolean("TargetAccountCreated");
|
||||
else targetAccountCreated = false;
|
||||
if (B.containsKey("ResolvedPayIdAddress")) resolvedPayIdAddress = B.getString("ResolvedPayIdAddress");
|
||||
else resolvedPayIdAddress = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -47,6 +51,7 @@ public class XrpData extends CoinData {
|
|||
if (reserve != null) B.putLong("Reserve", reserve);
|
||||
if (accountNotFound != null) B.putBoolean("AccoundNotFound", accountNotFound);
|
||||
if (targetAccountCreated != null) B.putBoolean("TargetAccountCreated", targetAccountCreated);
|
||||
if (resolvedPayIdAddress != null) B.putString("ResolvedPayIdAddress", resolvedPayIdAddress);
|
||||
} catch (Exception e) {
|
||||
Log.e("Can't save to bundle ", e.getMessage());
|
||||
}
|
||||
|
|
@ -61,6 +66,7 @@ public class XrpData extends CoinData {
|
|||
reserve = 20000000L;
|
||||
accountNotFound = false;
|
||||
targetAccountCreated = false;
|
||||
resolvedPayIdAddress = null;
|
||||
}
|
||||
|
||||
// balanceUnconfirmed is just the latest balance, it equals balanceConfirmed if no unconfirmed transaction present
|
||||
|
|
@ -124,4 +130,12 @@ public class XrpData extends CoinData {
|
|||
public boolean hasUnconfirmed() {
|
||||
return hasBalanceInfo() && !balanceConfirmed.equals(balanceUnconfirmed);
|
||||
}
|
||||
|
||||
public String getResolvedPayIdAddress() {
|
||||
return resolvedPayIdAddress;
|
||||
}
|
||||
|
||||
public void setResolvedPayIdAddress(String resolvedPayIdAddress) {
|
||||
this.resolvedPayIdAddress = resolvedPayIdAddress;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import com.ripple.crypto.ecdsa.ECDSASignature;
|
|||
import com.ripple.encodings.addresses.Addresses;
|
||||
import com.ripple.utils.HashUtils;
|
||||
import com.tangem.App;
|
||||
import com.tangem.data.network.ServerApiPayId;
|
||||
import com.tangem.data.network.ServerApiRipple;
|
||||
import com.tangem.data.network.model.PayIdResponse;
|
||||
import com.tangem.data.network.model.RippleResponse;
|
||||
import com.tangem.tangem_card.data.TangemCard;
|
||||
import com.tangem.tangem_card.reader.CardProtocol;
|
||||
|
|
@ -27,8 +29,14 @@ import com.tangem.wallet.TangemContext;
|
|||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
|
||||
import io.reactivex.SingleObserver;
|
||||
import io.reactivex.observers.DisposableSingleObserver;
|
||||
import io.xpring.xrpl.ClassicAddress;
|
||||
import io.xpring.xrpl.Utils;
|
||||
|
||||
public class XrpEngine extends CoinEngine {
|
||||
|
||||
private static final String TAG = XrpEngine.class.getSimpleName();
|
||||
|
|
@ -116,25 +124,31 @@ public class XrpEngine extends CoinEngine {
|
|||
if (address == null || address.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (address.contains("$")) { // PayID
|
||||
String[] addressParts = address.split("\\$");
|
||||
|
||||
if (address.length() < 25) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (address.length() > 35) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!address.startsWith("r")) {
|
||||
return false;
|
||||
if (addressParts.length != 2) {
|
||||
return false;
|
||||
}
|
||||
String addressURL = "https://" + addressParts[1] + "/" + addressParts[0];
|
||||
try {
|
||||
new URL(addressURL).toURI();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
Addresses.decodeAccountID(address);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
// X-address
|
||||
ClassicAddress classicAddress = Utils.decodeXAddress(address);
|
||||
if (classicAddress == null) {
|
||||
return false;
|
||||
}
|
||||
return !classicAddress.isTest();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -342,7 +356,22 @@ public class XrpEngine extends CoinEngine {
|
|||
public SignTask.TransactionToSign constructTransaction(Amount amountValue, Amount feeValue, boolean IncFee, String targetAddress) throws Exception {
|
||||
checkBlockchainDataExists();
|
||||
|
||||
String amount, fee;
|
||||
String amount, fee, destination;
|
||||
Integer destinationTag = null;
|
||||
|
||||
//PayID
|
||||
if (coinData.getResolvedPayIdAddress() != null) {
|
||||
destination = coinData.getResolvedPayIdAddress();
|
||||
} else {
|
||||
destination = targetAddress;
|
||||
}
|
||||
|
||||
//X-address
|
||||
ClassicAddress classicAddress = Utils.decodeXAddress(destination);
|
||||
if (classicAddress != null) {
|
||||
destination = classicAddress.address();
|
||||
destinationTag = classicAddress.tag().get();
|
||||
}
|
||||
|
||||
if (IncFee) {
|
||||
amount = convertToInternalAmount(amountValue).subtract(convertToInternalAmount(feeValue)).setScale(0).toPlainString();
|
||||
|
|
@ -363,10 +392,13 @@ public class XrpEngine extends CoinEngine {
|
|||
|
||||
// Put `as` AccountID field Account, `Object` o
|
||||
payment.as(AccountID.Account, coinData.getWallet());
|
||||
payment.as(AccountID.Destination, targetAddress);
|
||||
payment.as(AccountID.Destination, destination);
|
||||
payment.as(com.ripple.core.coretypes.Amount.Amount, amount);
|
||||
payment.as(UInt32.Sequence, coinData.getSequence());
|
||||
payment.as(com.ripple.core.coretypes.Amount.Fee, fee);
|
||||
if (destinationTag != null) {
|
||||
payment.as(UInt32.DestinationTag, destinationTag);
|
||||
}
|
||||
|
||||
XrpSignedTransaction signedTx = payment.prepare(canonisePubKey(ctx.getCard().getWalletPublicKeyRar()));
|
||||
|
||||
|
|
@ -525,6 +557,7 @@ public class XrpEngine extends CoinEngine {
|
|||
@Override
|
||||
public void requestFee(BlockchainRequestsCallbacks blockchainRequestsCallbacks, String targetAddress, Amount amount) {
|
||||
final ServerApiRipple serverApiRipple = new ServerApiRipple();
|
||||
final ServerApiPayId serverApiPayId = new ServerApiPayId();
|
||||
|
||||
ServerApiRipple.ResponseListener rippleListener = new ServerApiRipple.ResponseListener() {
|
||||
@Override
|
||||
|
|
@ -542,7 +575,7 @@ public class XrpEngine extends CoinEngine {
|
|||
coinData.setTargetAccountCreated(true); //expected behaviour, if account exists, there should be no error code -> null pointer
|
||||
}
|
||||
|
||||
if (serverApiRipple.isRequestsSequenceCompleted()) {
|
||||
if (serverApiRipple.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) {
|
||||
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
|
||||
} else {
|
||||
blockchainRequestsCallbacks.onProgress();
|
||||
|
|
@ -566,7 +599,7 @@ public class XrpEngine extends CoinEngine {
|
|||
ctx.setError(e.getMessage());
|
||||
}
|
||||
|
||||
if (serverApiRipple.isRequestsSequenceCompleted()) {
|
||||
if (serverApiRipple.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) {
|
||||
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
|
||||
} else {
|
||||
blockchainRequestsCallbacks.onProgress();
|
||||
|
|
@ -585,9 +618,46 @@ public class XrpEngine extends CoinEngine {
|
|||
};
|
||||
|
||||
serverApiRipple.setResponseListener(rippleListener);
|
||||
|
||||
serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, targetAddress, "");
|
||||
serverApiRipple.requestData(ServerApiRipple.RIPPLE_FEE, "", "");
|
||||
|
||||
if (targetAddress.contains("$")) { // PayID
|
||||
|
||||
SingleObserver<PayIdResponse> observer = new DisposableSingleObserver<PayIdResponse>() {
|
||||
@Override
|
||||
public void onSuccess(PayIdResponse payIdResponse) {
|
||||
try {
|
||||
String resolvedAddress = payIdResponse.getAddresses().get(0).getAddressDetails().getAddress();
|
||||
|
||||
if (validateAddress(resolvedAddress)) {
|
||||
coinData.setResolvedPayIdAddress(resolvedAddress);
|
||||
//check if target account is created
|
||||
serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, resolvedAddress, ""); //TODO: maybe just assume PayID account is created?
|
||||
} else {
|
||||
ctx.setError("Unknown address format in PayID response");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ctx.setError("FAIL payID Exception");
|
||||
}
|
||||
if (serverApiRipple.isRequestsSequenceCompleted() && serverApiPayId.isRequestsSequenceCompleted()) {
|
||||
blockchainRequestsCallbacks.onComplete(!ctx.hasError());
|
||||
} else {
|
||||
blockchainRequestsCallbacks.onProgress();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
Log.i(TAG, "onFail: " + "payID" + " " + e.getMessage());
|
||||
ctx.setError(e.getMessage());
|
||||
blockchainRequestsCallbacks.onComplete(false);
|
||||
}
|
||||
};
|
||||
|
||||
serverApiPayId.getAddress(targetAddress, observer);
|
||||
} else {
|
||||
//check if target account is created
|
||||
serverApiRipple.requestData(ServerApiRipple.RIPPLE_ACCOUNT_INFO, targetAddress, "");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue