Updated on 2026-08-14

This commit is contained in:
Tangem 2020-06-11 12:53:11 +03:00
parent b476f20da5
commit 6e5b272987
12 changed files with 198 additions and 141 deletions

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

View file

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

View file

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

View file

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

View file

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

View file

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