diff --git a/app/build.gradle b/app/build.gradle index af2ce223fa..027978da9f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -54,18 +54,19 @@ android { } } compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } packagingOptions { pickFirst('META-INF/proguard/okhttp3.pro') - // for one.block:eosiojava:0.1.0 +// for one.block:eosiojava:0.1.0 exclude 'lib/x86_64/darwin/libscrypt.dylib' exclude 'lib/x86_64/freebsd/libscrypt.so' exclude 'lib/x86_64/linux/libscrypt.so' exclude 'org.slf4j:slf4j-jdk14:1.7.25' } - buildToolsVersion '28.0.3' + buildToolsVersion '29.0.3' } repositories { @@ -81,6 +82,8 @@ dependencies { implementation project(':tangem-sdk-old') implementation project(':server-android') + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.5' + implementation 'androidx.navigation:navigation-fragment:2.2.2' implementation 'androidx.navigation:navigation-ui-ktx:2.2.2' implementation 'androidx.legacy:legacy-support-v4:1.0.0' @@ -132,7 +135,9 @@ dependencies { implementation 'com.google.firebase:firebase-crashlytics:17.0.0-beta04' implementation 'com.google.firebase:firebase-perf:19.0.6' + //dependencies for XRP implementation files('libs/ripple-core-0.0.1.jar') + implementation 'io.xpring:xpring4j:2.2.0' //4 dependencies for ripple-core TODO: move to module? implementation 'net.i2p.crypto:eddsa:0.3.0' implementation 'org.bouncycastle:bcprov-jdk15on:1.61' diff --git a/app/src/main/java/com/tangem/data/network/PayIdApi.java b/app/src/main/java/com/tangem/data/network/PayIdApi.java new file mode 100644 index 0000000000..032968baaa --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/PayIdApi.java @@ -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 getAddress(@Path("user") String user); +} diff --git a/app/src/main/java/com/tangem/data/network/ServerApiPayId.java b/app/src/main/java/com/tangem/data/network/ServerApiPayId.java new file mode 100644 index 0000000000..237fcb279d --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/ServerApiPayId.java @@ -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 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 addressSingle = api.getAddress(user) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnEvent((object, throwable) -> requestsCount--); + + addressSingle.subscribe(addressObserver); + } +} diff --git a/app/src/main/java/com/tangem/data/network/model/PayIdResponse.kt b/app/src/main/java/com/tangem/data/network/model/PayIdResponse.kt new file mode 100644 index 0000000000..5d383b7b0c --- /dev/null +++ b/app/src/main/java/com/tangem/data/network/model/PayIdResponse.kt @@ -0,0 +1,18 @@ +package com.tangem.data.network.model + +import com.google.gson.annotations.SerializedName + +data class PayIdResponse( + @SerializedName("addresses") + var addresses: List? = null +) + +data class PayIdAddress( + @SerializedName("addressDetails") + var addressDetails: PayIdAddressDetails? = null +) + +data class PayIdAddressDetails( + @SerializedName("address") + var address: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/xrp/XrpBase58.java b/app/src/main/java/com/tangem/wallet/xrp/XrpBase58.java deleted file mode 100644 index c149776f2c..0000000000 --- a/app/src/main/java/com/tangem/wallet/xrp/XrpBase58.java +++ /dev/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(); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/wallet/xrp/XrpData.java b/app/src/main/java/com/tangem/wallet/xrp/XrpData.java index ca79936381..09bc6276fa 100644 --- a/app/src/main/java/com/tangem/wallet/xrp/XrpData.java +++ b/app/src/main/java/com/tangem/wallet/xrp/XrpData.java @@ -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; + } } diff --git a/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java b/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java index ff06160ba2..a667537b8f 100644 --- a/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java +++ b/app/src/main/java/com/tangem/wallet/xrp/XrpEngine.java @@ -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 observer = new DisposableSingleObserver() { + @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 diff --git a/build.gradle b/build.gradle index 511fa6ac99..40bc31028b 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { maven { url 'https://maven.fabric.io/public' } } dependencies { - classpath "com.android.tools.build:gradle:$versions.build_gradle" + classpath "com.android.tools.build:gradle:${versions.build_gradle}" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$versions.kotlin" classpath "com.github.dcendents:android-maven-gradle-plugin:2.1" classpath 'com.google.gms:google-services:4.3.3' diff --git a/dependencies.gradle b/dependencies.gradle index 3c7aaf12d8..f61f047e5a 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,4 +1,4 @@ ext.versions = [ kotlin : '1.3.72', - build_gradle: '3.6.3', + build_gradle: '4.0.0', ] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3cbbb9a496..01e71a44df 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Mon Mar 30 09:15:33 MSK 2020 +#Thu Jun 11 11:33:17 MSK 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/tangem-sdk-old/build.gradle b/tangem-sdk-old/build.gradle index e7ef5a0015..189bc3b334 100644 --- a/tangem-sdk-old/build.gradle +++ b/tangem-sdk-old/build.gradle @@ -24,7 +24,7 @@ android { versionNameSuffix "-beta" } } - buildToolsVersion '28.0.3' + buildToolsVersion '29.0.2' compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 diff --git a/tangem-sdk/build.gradle b/tangem-sdk/build.gradle index de05139eb5..0664ab0e25 100644 --- a/tangem-sdk/build.gradle +++ b/tangem-sdk/build.gradle @@ -10,7 +10,7 @@ version "$jitpackSdk.version" android { compileSdkVersion 29 - buildToolsVersion "29.0.2" + buildToolsVersion "29.0.3" defaultConfig {