1
mirror of https://github.com/m2049r/xmrwallet synced 2025-09-10 22:10:49 +02:00

Compare commits

...

5 Commits

Author SHA1 Message Date
m2049r
1209295a8c v1.12.6 2019-11-18 12:01:49 +01:00
m2049r
037b019d4d xmrto payment through subaddress (#639)
* use subaddress for xmrto only

* fix exchange rate
2019-11-18 12:00:39 +01:00
m2049r
7a1d788f2a UI tweaks (#638)
* fix amount entry field

* password entry on own line

* remove errors when typing; field spacing
2019-11-17 10:31:19 +01:00
m2049r
87d9a8cd95 use kraken for EURXMR exchange rate (#637)
combine with ECB rates for other fiat conversions
2019-11-17 00:42:57 +01:00
m2049r
f637d7f617 update block heights (#636) 2019-11-10 23:54:40 +01:00
30 changed files with 1309 additions and 731 deletions

View File

@@ -7,8 +7,8 @@ android {
applicationId "com.m2049r.xmrwallet" applicationId "com.m2049r.xmrwallet"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 28 targetSdkVersion 28
versionCode 193 versionCode 196
versionName "1.12.3 'Caerbannog'" versionName "1.12.6 'Caerbannog'"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild { externalNativeBuild {

View File

@@ -79,6 +79,23 @@ public class GenerateFragment extends Fragment {
private String type = null; private String type = null;
private void clearErrorOnTextEntry(final TextInputLayout textInputLayout) {
textInputLayout.getEditText().addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable editable) {
textInputLayout.setError(null);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
});
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@@ -110,6 +127,23 @@ public class GenerateFragment extends Fragment {
} }
} }
}); });
clearErrorOnTextEntry(etWalletName);
etWalletPassword.getEditText().addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable editable) {
checkPassword();
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
});
etWalletMnemonic.getEditText().setOnFocusChangeListener(new View.OnFocusChangeListener() { etWalletMnemonic.getEditText().setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override @Override
public void onFocusChange(View v, boolean hasFocus) { public void onFocusChange(View v, boolean hasFocus) {
@@ -118,6 +152,8 @@ public class GenerateFragment extends Fragment {
} }
} }
}); });
clearErrorOnTextEntry(etWalletMnemonic);
etWalletAddress.getEditText().setOnFocusChangeListener(new View.OnFocusChangeListener() { etWalletAddress.getEditText().setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override @Override
public void onFocusChange(View v, boolean hasFocus) { public void onFocusChange(View v, boolean hasFocus) {
@@ -126,6 +162,8 @@ public class GenerateFragment extends Fragment {
} }
} }
}); });
clearErrorOnTextEntry(etWalletAddress);
etWalletViewKey.getEditText().setOnFocusChangeListener(new View.OnFocusChangeListener() { etWalletViewKey.getEditText().setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override @Override
public void onFocusChange(View v, boolean hasFocus) { public void onFocusChange(View v, boolean hasFocus) {
@@ -134,6 +172,8 @@ public class GenerateFragment extends Fragment {
} }
} }
}); });
clearErrorOnTextEntry(etWalletViewKey);
etWalletSpendKey.getEditText().setOnFocusChangeListener(new View.OnFocusChangeListener() { etWalletSpendKey.getEditText().setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override @Override
public void onFocusChange(View v, boolean hasFocus) { public void onFocusChange(View v, boolean hasFocus) {
@@ -142,6 +182,7 @@ public class GenerateFragment extends Fragment {
} }
} }
}); });
clearErrorOnTextEntry(etWalletSpendKey);
Helper.showKeyboard(getActivity()); Helper.showKeyboard(getActivity());
@@ -310,21 +351,6 @@ public class GenerateFragment extends Fragment {
} }
}); });
etWalletPassword.getEditText().addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable editable) {
checkPassword();
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
});
etWalletName.requestFocus(); etWalletName.requestFocus();
initZxcvbn(); initZxcvbn();

View File

@@ -50,9 +50,7 @@ import com.m2049r.xmrwallet.widget.Toolbar;
import java.text.NumberFormat; import java.text.NumberFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import timber.log.Timber; import timber.log.Timber;
@@ -220,7 +218,7 @@ public class WalletFragment extends Fragment
// at this point selection is XMR in case of error // at this point selection is XMR in case of error
String displayB; String displayB;
double amountA = Helper.getDecimalAmount(unlockedBalance).doubleValue(); double amountA = Helper.getDecimalAmount(unlockedBalance).doubleValue();
if (!Helper.CRYPTO.equals(balanceCurrency)) { // not XMR if (!Helper.BASE_CRYPTO.equals(balanceCurrency)) { // not XMR
double amountB = amountA * balanceRate; double amountB = amountA * balanceRate;
displayB = Helper.getFormattedAmount(amountB, false); displayB = Helper.getFormattedAmount(amountB, false);
} else { // XMR } else { // XMR
@@ -229,7 +227,7 @@ public class WalletFragment extends Fragment
showBalance(displayB); showBalance(displayB);
} }
String balanceCurrency = Helper.CRYPTO; String balanceCurrency = Helper.BASE_CRYPTO;
double balanceRate = 1.0; double balanceRate = 1.0;
private final ExchangeApi exchangeApi = Helper.getExchangeApi(); private final ExchangeApi exchangeApi = Helper.getExchangeApi();
@@ -245,7 +243,7 @@ public class WalletFragment extends Fragment
Timber.d(currency); Timber.d(currency);
if (!currency.equals(balanceCurrency) || (balanceRate <= 0)) { if (!currency.equals(balanceCurrency) || (balanceRate <= 0)) {
showExchanging(); showExchanging();
exchangeApi.queryExchangeRate(Helper.CRYPTO, currency, exchangeApi.queryExchangeRate(Helper.BASE_CRYPTO, currency,
new ExchangeCallback() { new ExchangeCallback() {
@Override @Override
public void onSuccess(final ExchangeRate exchangeRate) { public void onSuccess(final ExchangeRate exchangeRate) {
@@ -301,10 +299,10 @@ public class WalletFragment extends Fragment
public void exchange(final ExchangeRate exchangeRate) { public void exchange(final ExchangeRate exchangeRate) {
hideExchanging(); hideExchanging();
if (!Helper.CRYPTO.equals(exchangeRate.getBaseCurrency())) { if (!Helper.BASE_CRYPTO.equals(exchangeRate.getBaseCurrency())) {
Timber.e("Not XMR"); Timber.e("Not XMR");
sCurrency.setSelection(0, true); sCurrency.setSelection(0, true);
balanceCurrency = Helper.CRYPTO; balanceCurrency = Helper.BASE_CRYPTO;
balanceRate = 1.0; balanceRate = 1.0;
} else { } else {
int spinnerPosition = ((ArrayAdapter) sCurrency.getAdapter()).getPosition(exchangeRate.getQuoteCurrency()); int spinnerPosition = ((ArrayAdapter) sCurrency.getAdapter()).getPosition(exchangeRate.getQuoteCurrency());

View File

@@ -116,12 +116,12 @@ public class SendAmountWizardFragment extends SendWizardFragment {
sendListener.getTxData().setAmount(Wallet.SWEEP_ALL); sendListener.getTxData().setAmount(Wallet.SWEEP_ALL);
} }
} else { } else {
if (!etAmount.validate(maxFunds)) { if (!etAmount.validate(maxFunds, 0)) {
return false; return false;
} }
if (sendListener != null) { if (sendListener != null) {
String xmr = etAmount.getAmount(); String xmr = etAmount.getNativeAmount();
if (xmr != null) { if (xmr != null) {
sendListener.getTxData().setAmount(Wallet.getAmountFromString(xmr)); sendListener.getTxData().setAmount(Wallet.getAmountFromString(xmr));
} else { } else {
@@ -148,8 +148,8 @@ public class SendAmountWizardFragment extends SendWizardFragment {
tvFunds.setText(getString(R.string.send_available, tvFunds.setText(getString(R.string.send_available,
getString(R.string.unknown_amount))); getString(R.string.unknown_amount)));
} }
// getAmount is null if exchange is in progress // getNativeAmount is null if exchange is in progress
if ((etAmount.getAmount() != null) && etAmount.getAmount().isEmpty()) { if ((etAmount.getNativeAmount() != null) && etAmount.getNativeAmount().isEmpty()) {
final BarcodeData data = sendListener.popBarcodeData(); final BarcodeData data = sendListener.popBarcodeData();
if ((data != null) && (data.amount != null)) { if ((data != null) && (data.amount != null)) {
etAmount.setAmount(data.amount); etAmount.setAmount(data.amount);

View File

@@ -31,7 +31,8 @@ import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper; import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.OkHttpHelper; import com.m2049r.xmrwallet.util.OkHttpHelper;
import com.m2049r.xmrwallet.widget.ExchangeBtcEditText; import com.m2049r.xmrwallet.widget.ExchangeEditText;
import com.m2049r.xmrwallet.widget.ExchangeOtherEditText;
import com.m2049r.xmrwallet.widget.SendProgressView; import com.m2049r.xmrwallet.widget.SendProgressView;
import com.m2049r.xmrwallet.xmrto.XmrToError; import com.m2049r.xmrwallet.xmrto.XmrToError;
import com.m2049r.xmrwallet.xmrto.XmrToException; import com.m2049r.xmrwallet.xmrto.XmrToException;
@@ -61,7 +62,7 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
} }
private TextView tvFunds; private TextView tvFunds;
private ExchangeBtcEditText etAmount; private ExchangeOtherEditText etAmount;
private TextView tvXmrToParms; private TextView tvXmrToParms;
private SendProgressView evParams; private SendProgressView evParams;
@@ -97,7 +98,7 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
} }
if (sendListener != null) { if (sendListener != null) {
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData(); TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
String btcString = etAmount.getAmount(); String btcString = etAmount.getNativeAmount();
if (btcString != null) { if (btcString != null) {
try { try {
double btc = Double.parseDouble(btcString); double btc = Double.parseDouble(btcString);
@@ -166,7 +167,7 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
getView().post(new Runnable() { getView().post(new Runnable() {
@Override @Override
public void run() { public void run() {
etAmount.setRate(1.0d / orderParameters.getPrice()); etAmount.setExchangeRate(1.0d / orderParameters.getPrice());
NumberFormat df = NumberFormat.getInstance(Locale.US); NumberFormat df = NumberFormat.getInstance(Locale.US);
df.setMaximumFractionDigits(6); df.setMaximumFractionDigits(6);
String min = df.format(orderParameters.getLowerLimit()); String min = df.format(orderParameters.getLowerLimit());
@@ -206,7 +207,7 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
} }
private void processOrderParmsError(final Exception ex) { private void processOrderParmsError(final Exception ex) {
etAmount.setRate(0); etAmount.setExchangeRate(0);
orderParameters = null; orderParameters = null;
maxBtc = 0; maxBtc = 0;
minBtc = 0; minBtc = 0;

View File

@@ -457,8 +457,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
} }
showProgress(3, getString(R.string.label_send_progress_create_tx)); showProgress(3, getString(R.string.label_send_progress_create_tx));
TxData txData = sendListener.getTxData(); TxData txData = sendListener.getTxData();
txData.setDestinationAddress(xmrtoStatus.getXmrReceivingAddress()); txData.setDestinationAddress(xmrtoStatus.getXmrReceivingSubaddress());
txData.setPaymentId(xmrtoStatus.getXmrRequiredPaymentIdShort());
txData.setAmount(Wallet.getAmountFromDouble(xmrtoStatus.getXmrAmountTotal())); txData.setAmount(Wallet.getAmountFromDouble(xmrtoStatus.getXmrAmountTotal()));
getActivityCallback().onPrepareSend(xmrtoStatus.getUuid(), txData); getActivityCallback().onPrepareSend(xmrtoStatus.getUuid(), txData);
} }

View File

@@ -0,0 +1,215 @@
/*
* Copyright (c) 2019 m2049r@monerujo.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// https://developer.android.com/training/basics/network-ops/xml
package com.m2049r.xmrwallet.service.exchange.ecb;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeException;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import timber.log.Timber;
public class ExchangeApiImpl implements ExchangeApi {
@NonNull
private final OkHttpClient okHttpClient;
@NonNull
private final HttpUrl baseUrl;
//so we can inject the mockserver url
@VisibleForTesting
public ExchangeApiImpl(@NonNull final OkHttpClient okHttpClient, @NonNull final HttpUrl baseUrl) {
this.okHttpClient = okHttpClient;
this.baseUrl = baseUrl;
}
public ExchangeApiImpl(@NonNull final OkHttpClient okHttpClient) {
this(okHttpClient, HttpUrl.parse("https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"));
// data is daily and is refreshed around 16:00 CET every working day
}
public static boolean isSameDay(Calendar calendar, Calendar anotherCalendar) {
return (calendar.get(Calendar.YEAR) == anotherCalendar.get(Calendar.YEAR)) &&
(calendar.get(Calendar.DAY_OF_YEAR) == anotherCalendar.get(Calendar.DAY_OF_YEAR));
}
@Override
public void queryExchangeRate(@NonNull final String baseCurrency, @NonNull final String quoteCurrency,
@NonNull final ExchangeCallback callback) {
if (!baseCurrency.equals("EUR")) {
callback.onError(new IllegalArgumentException("Only EUR supported as base"));
return;
}
if (baseCurrency.equals(quoteCurrency)) {
callback.onSuccess(new ExchangeRateImpl(quoteCurrency, 1.0, new Date()));
return;
}
if (fetchDate != null) { // we have data
boolean useCache = false;
// figure out if we can use the cached values
// data is daily and is refreshed around 16:00 CET every working day
Calendar now = Calendar.getInstance(TimeZone.getTimeZone("CET"));
int fetchWeekday = fetchDate.get(Calendar.DAY_OF_WEEK);
int fetchDay = fetchDate.get(Calendar.DAY_OF_YEAR);
int fetchHour = fetchDate.get(Calendar.HOUR_OF_DAY);
int today = now.get(Calendar.DAY_OF_YEAR);
int nowHour = now.get(Calendar.HOUR_OF_DAY);
if (
// was it fetched today before 16:00? assume no new data iff now < 16:00 as well
((today == fetchDay) && (fetchHour < 16) && (nowHour < 16))
// was it fetched after, 17:00? we can assume there is no newer data
|| ((today == fetchDay) && (fetchHour > 17))
|| ((today == fetchDay + 1) && (fetchHour > 17) && (nowHour < 16))
// is the data itself from today? there can be no newer data
|| (fxDate.get(Calendar.DAY_OF_YEAR) == today)
// was it fetched Sat/Sun? we can assume there is no newer data
|| ((fetchWeekday == Calendar.SATURDAY) || (fetchWeekday == Calendar.SUNDAY))
) { // return cached rate
try {
callback.onSuccess(getRate(quoteCurrency));
} catch (ExchangeException ex) {
callback.onError(ex);
}
return;
}
}
final Request httpRequest = createHttpRequest(baseUrl);
okHttpClient.newCall(httpRequest).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(final Call call, final IOException ex) {
callback.onError(ex);
}
@Override
public void onResponse(final Call call, final Response response) throws IOException {
if (response.isSuccessful()) {
try {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(response.body().byteStream());
doc.getDocumentElement().normalize();
parse(doc);
try {
callback.onSuccess(getRate(quoteCurrency));
} catch (ExchangeException ex) {
callback.onError(ex);
}
} catch (ParserConfigurationException | SAXException ex) {
Timber.w(ex);
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
}
} else {
callback.onError(new ExchangeException(response.code(), response.message()));
}
}
});
}
private Request createHttpRequest(final HttpUrl url) {
return new Request.Builder()
.url(url)
.get()
.build();
}
final private Map<String, Double> fxEntries = new HashMap<>();
private Calendar fxDate = null;
private Calendar fetchDate = null;
synchronized private ExchangeRate getRate(String currency) throws ExchangeException {
Timber.e("Getting %s", currency);
final Double rate = fxEntries.get(currency);
if (rate == null) throw new ExchangeException(404, "Currency not supported: " + currency);
return new ExchangeRateImpl(currency, rate, fxDate.getTime());
}
private final static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
{
DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private void parse(final Document xmlRootDoc) {
final Map<String, Double> entries = new HashMap<>();
Calendar date = Calendar.getInstance(TimeZone.getTimeZone("CET"));
try {
NodeList cubes = xmlRootDoc.getElementsByTagName("Cube");
for (int i = 0; i < cubes.getLength(); i++) {
Node node = cubes.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element cube = (Element) node;
if (cube.hasAttribute("time")) { // a time Cube
final Date time = DATE_FORMAT.parse(cube.getAttribute("time"));
date.setTime(time);
} else if (cube.hasAttribute("currency")
&& cube.hasAttribute("rate")) { // a rate Cube
String currency = cube.getAttribute("currency");
double rate = Double.valueOf(cube.getAttribute("rate"));
entries.put(currency, rate);
} // else an empty Cube - ignore
}
}
} catch (ParseException ex) {
Timber.d(ex);
}
synchronized (this) {
if (date != null) {
fetchDate = Calendar.getInstance(TimeZone.getTimeZone("CET"));
fxDate = date;
fxEntries.clear();
fxEntries.putAll(entries);
}
// else don't change what we have
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (c) 2019 m2049r et al.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.m2049r.xmrwallet.service.exchange.ecb;
import android.support.annotation.NonNull;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
import java.util.Date;
class ExchangeRateImpl implements ExchangeRate {
private final Date date;
private final String baseCurrency = "EUR";
private final String quoteCurrency;
private final double rate;
@Override
public String getServiceName() {
return "ecb.europa.eu";
}
@Override
public String getBaseCurrency() {
return baseCurrency;
}
@Override
public String getQuoteCurrency() {
return quoteCurrency;
}
@Override
public double getRate() {
return rate;
}
ExchangeRateImpl(@NonNull final String quoteCurrency, double rate, @NonNull final Date date) {
super();
this.quoteCurrency = quoteCurrency;
this.rate = rate;
this.date = date;
}
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2017-2018 m2049r et al. * Copyright (c) 2017-2019 m2049r et al.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.m2049r.xmrwallet.service.exchange.coinmarketcap; package com.m2049r.xmrwallet.service.exchange.kraken;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting; import android.support.annotation.VisibleForTesting;
@@ -36,9 +36,9 @@ import okhttp3.HttpUrl;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import timber.log.Timber;
public class ExchangeApiImpl implements ExchangeApi { public class ExchangeApiImpl implements ExchangeApi {
static final String CRYPTO_ID = "328";
@NonNull @NonNull
private final OkHttpClient okHttpClient; private final OkHttpClient okHttpClient;
@@ -47,14 +47,13 @@ public class ExchangeApiImpl implements ExchangeApi {
//so we can inject the mockserver url //so we can inject the mockserver url
@VisibleForTesting @VisibleForTesting
ExchangeApiImpl(@NonNull final OkHttpClient okHttpClient, final HttpUrl baseUrl) { public ExchangeApiImpl(@NonNull final OkHttpClient okHttpClient, final HttpUrl baseUrl) {
this.okHttpClient = okHttpClient; this.okHttpClient = okHttpClient;
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
} }
public ExchangeApiImpl(@NonNull final OkHttpClient okHttpClient) { public ExchangeApiImpl(@NonNull final OkHttpClient okHttpClient) {
this(okHttpClient, HttpUrl.parse("https://api.coinmarketcap.com/v2/ticker/")); this(okHttpClient, HttpUrl.parse("https://api.kraken.com/0/public/Ticker"));
} }
@Override @Override
@@ -66,29 +65,25 @@ public class ExchangeApiImpl implements ExchangeApi {
return; return;
} }
boolean inverse = false; boolean invertQuery;
String fiat = null;
if (baseCurrency.equals(Helper.CRYPTO)) {
fiat = quoteCurrency;
inverse = false;
}
if (quoteCurrency.equals(Helper.CRYPTO)) { if (Helper.BASE_CRYPTO.equals(baseCurrency)) {
fiat = baseCurrency; invertQuery = false;
inverse = true; } else if (Helper.BASE_CRYPTO.equals(quoteCurrency)) {
} invertQuery = true;
} else {
if (fiat == null) { callback.onError(new IllegalArgumentException("no crypto specified"));
callback.onError(new IllegalArgumentException("no fiat specified"));
return; return;
} }
final boolean swapAssets = inverse; Timber.d("queryExchangeRate: i %b, b %s, q %s", invertQuery, baseCurrency, quoteCurrency);
final boolean invert = invertQuery;
final String base = invert ? quoteCurrency : baseCurrency;
final String quote = invert ? baseCurrency : quoteCurrency;
final HttpUrl url = baseUrl.newBuilder() final HttpUrl url = baseUrl.newBuilder()
.addEncodedPathSegments(CRYPTO_ID + "/") .addQueryParameter("pair", base + (quote.equals("BTC") ? "XBT" : quote))
.addQueryParameter("convert", fiat)
.build(); .build();
final Request httpRequest = createHttpRequest(url); final Request httpRequest = createHttpRequest(url);
@@ -104,13 +99,13 @@ public class ExchangeApiImpl implements ExchangeApi {
if (response.isSuccessful()) { if (response.isSuccessful()) {
try { try {
final JSONObject json = new JSONObject(response.body().string()); final JSONObject json = new JSONObject(response.body().string());
final JSONObject metadata = json.getJSONObject("metadata"); final JSONArray jsonError = json.getJSONArray("error");
if (!metadata.isNull("error")) { if (jsonError.length() > 0) {
final String errorMsg = metadata.getString("error"); final String errorMsg = jsonError.getString(0);
callback.onError(new ExchangeException(response.code(), errorMsg)); callback.onError(new ExchangeException(response.code(), errorMsg));
} else { } else {
final JSONObject jsonResult = json.getJSONObject("data"); final JSONObject jsonResult = json.getJSONObject("result");
reportSuccess(jsonResult, swapAssets, callback); reportSuccess(jsonResult, invert, callback);
} }
} catch (JSONException ex) { } catch (JSONException ex) {
callback.onError(new ExchangeException(ex.getLocalizedMessage())); callback.onError(new ExchangeException(ex.getLocalizedMessage()));

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2017-2018 m2049r et al. * Copyright (c) 2017 m2049r et al.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.m2049r.xmrwallet.service.exchange.coinmarketcap; package com.m2049r.xmrwallet.service.exchange.kraken;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
@@ -25,7 +25,6 @@ import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.util.Iterator;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -38,7 +37,7 @@ class ExchangeRateImpl implements ExchangeRate {
@Override @Override
public String getServiceName() { public String getServiceName() {
return "coinmarketcap.com"; return "kraken.com";
} }
@Override @Override
@@ -65,21 +64,29 @@ class ExchangeRateImpl implements ExchangeRate {
ExchangeRateImpl(final JSONObject jsonObject, final boolean swapAssets) throws JSONException, ExchangeException { ExchangeRateImpl(final JSONObject jsonObject, final boolean swapAssets) throws JSONException, ExchangeException {
try { try {
final String baseC = jsonObject.getString("symbol"); final String key = jsonObject.keys().next(); // we expect only one
final JSONObject quotes = jsonObject.getJSONObject("quotes"); Pattern pattern = Pattern.compile("^X(.*?)Z(.*?)$");
final Iterator<String> keys = quotes.keys(); Matcher matcher = pattern.matcher(key);
String key = null; if (matcher.find()) {
// get key which is not USD unless it is the only one baseCurrency = swapAssets ? matcher.group(2) : matcher.group(1);
while (keys.hasNext()) { quoteCurrency = swapAssets ? matcher.group(1) : matcher.group(2);
key = keys.next(); } else {
if (!key.equals("USD")) break; throw new ExchangeException("no pair returned!");
}
JSONObject pair = jsonObject.getJSONObject(key);
JSONArray close = pair.getJSONArray("c");
String closePrice = close.getString(0);
if (closePrice != null) {
try {
double rate = Double.parseDouble(closePrice);
this.rate = swapAssets ? (1 / rate) : rate;
} catch (NumberFormatException ex) {
throw new ExchangeException(ex.getLocalizedMessage());
}
} else {
throw new ExchangeException("no close price returned!");
} }
final String quoteC = key;
baseCurrency = swapAssets ? quoteC : baseC;
quoteCurrency = swapAssets ? baseC : quoteC;
JSONObject quote = quotes.getJSONObject(key);
double price = quote.getDouble("price");
this.rate = swapAssets ? (1d / price) : price;
} catch (NoSuchElementException ex) { } catch (NoSuchElementException ex) {
throw new ExchangeException(ex.getLocalizedMessage()); throw new ExchangeException(ex.getLocalizedMessage());
} }

View File

@@ -0,0 +1,99 @@
/*
* Copyright (c) 2019 m2049r@monerujo.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// https://developer.android.com/training/basics/network-ops/xml
package com.m2049r.xmrwallet.service.exchange.krakenEcb;
import android.support.annotation.NonNull;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
import com.m2049r.xmrwallet.util.Helper;
import okhttp3.OkHttpClient;
import timber.log.Timber;
/*
Gets the XMR/EUR rate from kraken and then gets the EUR/fiat rate from the ECB
*/
public class ExchangeApiImpl implements ExchangeApi {
static public final String BASE_FIAT = "EUR";
@NonNull
private final OkHttpClient okHttpClient;
public ExchangeApiImpl(@NonNull final OkHttpClient okHttpClient) {
this.okHttpClient = okHttpClient;
}
@Override
public void queryExchangeRate(@NonNull final String baseCurrency, @NonNull final String quoteCurrency,
@NonNull final ExchangeCallback callback) {
Timber.d("B=%s Q=%s", baseCurrency, quoteCurrency);
if (baseCurrency.equals(quoteCurrency)) {
Timber.d("BASE=QUOTE=1");
callback.onSuccess(new ExchangeRateImpl(baseCurrency, quoteCurrency, 1.0));
return;
}
if (!Helper.BASE_CRYPTO.equals(baseCurrency)
&& !Helper.BASE_CRYPTO.equals(quoteCurrency)) {
callback.onError(new IllegalArgumentException("no " + Helper.BASE_CRYPTO + " specified"));
return;
}
final String quote = Helper.BASE_CRYPTO.equals(baseCurrency) ? quoteCurrency : baseCurrency;
final ExchangeApi krakenApi =
new com.m2049r.xmrwallet.service.exchange.kraken.ExchangeApiImpl(okHttpClient);
krakenApi.queryExchangeRate(Helper.BASE_CRYPTO, BASE_FIAT, new ExchangeCallback() {
@Override
public void onSuccess(final ExchangeRate krakenRate) {
Timber.d("kraken = %f", krakenRate.getRate());
final ExchangeApi ecbApi =
new com.m2049r.xmrwallet.service.exchange.ecb.ExchangeApiImpl(okHttpClient);
ecbApi.queryExchangeRate(BASE_FIAT, quote, new ExchangeCallback() {
@Override
public void onSuccess(final ExchangeRate ecbRate) {
Timber.d("ECB = %f", ecbRate.getRate());
double rate = ecbRate.getRate() * krakenRate.getRate();
Timber.d("Q=%s QC=%s", quote, quoteCurrency);
if (!quote.equals(quoteCurrency)) rate = 1.0d / rate;
Timber.d("rate = %f", rate);
final ExchangeRate exchangeRate =
new ExchangeRateImpl(baseCurrency, quoteCurrency, rate);
callback.onSuccess(exchangeRate);
}
@Override
public void onError(Exception ex) {
Timber.d(ex);
callback.onError(ex);
}
});
}
@Override
public void onError(Exception ex) {
Timber.d(ex);
callback.onError(ex);
}
});
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright (c) 2019 m2049r et al.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.m2049r.xmrwallet.service.exchange.krakenEcb;
import android.support.annotation.NonNull;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
import java.util.Date;
class ExchangeRateImpl implements ExchangeRate {
private final String baseCurrency;
private final String quoteCurrency;
private final double rate;
@Override
public String getServiceName() {
return "kraken+ecb";
}
@Override
public String getBaseCurrency() {
return baseCurrency;
}
@Override
public String getQuoteCurrency() {
return quoteCurrency;
}
@Override
public double getRate() {
return rate;
}
ExchangeRateImpl(@NonNull final String baseCurrency, @NonNull final String quoteCurrency, double rate) {
super();
this.baseCurrency = baseCurrency;
this.quoteCurrency = quoteCurrency;
this.rate = rate;
}
}

View File

@@ -58,7 +58,6 @@ import android.widget.TextView;
import com.m2049r.xmrwallet.BuildConfig; import com.m2049r.xmrwallet.BuildConfig;
import com.m2049r.xmrwallet.R; import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.model.NetworkType; import com.m2049r.xmrwallet.model.NetworkType;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi; import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
@@ -85,7 +84,7 @@ public class Helper {
static public final String NOCRAZYPASS_FLAGFILE = ".nocrazypass"; static public final String NOCRAZYPASS_FLAGFILE = ".nocrazypass";
static public final String CRYPTO = "XMR"; static public final String BASE_CRYPTO = "XMR";
static private final String WALLET_DIR = "monerujo" + FLAVOR_SUFFIX; static private final String WALLET_DIR = "monerujo" + FLAVOR_SUFFIX;
static private final String HOME_DIR = "monero" + FLAVOR_SUFFIX; static private final String HOME_DIR = "monero" + FLAVOR_SUFFIX;
@@ -207,22 +206,32 @@ public class Helper {
return d.toPlainString(); return d.toPlainString();
} }
static public String getFormattedAmount(double amount, boolean isXmr) { static public String getFormattedAmount(double amount, boolean isCrypto) {
// at this point selection is XMR in case of error // at this point selection is XMR in case of error
String displayB; String displayB;
if (isXmr) { // XMR if (isCrypto) {
long xmr = Wallet.getAmountFromDouble(amount); if ((amount >= 0) || (amount == 0)) {
if ((xmr > 0) || (amount == 0)) {
displayB = String.format(Locale.US, "%,.5f", amount); displayB = String.format(Locale.US, "%,.5f", amount);
} else { } else {
displayB = null; displayB = null;
} }
} else { // not XMR } else { // not crypto
displayB = String.format(Locale.US, "%,.2f", amount); displayB = String.format(Locale.US, "%,.2f", amount);
} }
return displayB; return displayB;
} }
// min 2 significant digits after decimal point
static public String getFormattedAmount(double amount) {
if ((amount >= 1.0d) || (amount == 0))
return String.format(Locale.US, "%,.2f", amount);
else { // amount < 1
int decimals = 1 - (int) Math.floor(Math.log10(amount));
if (decimals > 12) decimals = 12;
return String.format(Locale.US, "%,." + decimals + "f", amount);
}
}
static public Bitmap getBitmap(Context context, int drawableId) { static public Bitmap getBitmap(Context context, int drawableId) {
Drawable drawable = ContextCompat.getDrawable(context, drawableId); Drawable drawable = ContextCompat.getDrawable(context, drawableId);
if (drawable instanceof BitmapDrawable) { if (drawable instanceof BitmapDrawable) {
@@ -624,7 +633,7 @@ public class Helper {
} }
static public ExchangeApi getExchangeApi() { static public ExchangeApi getExchangeApi() {
return new com.m2049r.xmrwallet.service.exchange.coinmarketcap.ExchangeApiImpl(OkHttpHelper.getOkHttpClient()); return new com.m2049r.xmrwallet.service.exchange.krakenEcb.ExchangeApiImpl(OkHttpHelper.getOkHttpClient());
} }
public interface Action { public interface Action {

View File

@@ -105,6 +105,10 @@ public class RestoreHeight {
blockheight.put("2019-05-01", 1824671L); blockheight.put("2019-05-01", 1824671L);
blockheight.put("2019-06-01", 1847005L); blockheight.put("2019-06-01", 1847005L);
blockheight.put("2019-07-01", 1868590L); blockheight.put("2019-07-01", 1868590L);
blockheight.put("2019-08-01", 1890878L);
blockheight.put("2019-09-01", 1913201L);
blockheight.put("2019-10-01", 1934732L);
blockheight.put("2019-11-01", 1957051L);
} }
public long getHeight(String date) { public long getHeight(String date) {

View File

@@ -1,187 +0,0 @@
/*
* Copyright (c) 2017 m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// based on https://code.tutsplus.com/tutorials/creating-compound-views-on-android--cms-22889
package com.m2049r.xmrwallet.widget;
import android.content.Context;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.util.Helper;
import timber.log.Timber;
public class ExchangeBtcEditText extends LinearLayout {
String btcAmount = null;
String xmrAmount = null;
private boolean validate(String amount, double max, double min) {
boolean ok = true;
if (amount != null) {
try {
double x = Double.parseDouble(amount);
if ((x < min) || (x > max)) {
ok = false;
}
} catch (NumberFormatException ex) {
Timber.e(ex.getLocalizedMessage());
ok = false;
}
} else {
ok = false;
}
return ok;
}
public boolean validate(double maxBtc, double minBtc) {
Timber.d("validate(maxBtc=%f,minBtc=%f)", maxBtc, minBtc);
boolean ok = true;
if (!validate(btcAmount, maxBtc, minBtc)) {
Timber.d("btcAmount invalid %s", btcAmount);
shakeAmountField();
return false;
}
return true;
}
void shakeAmountField() {
etAmountA.startAnimation(Helper.getShakeAnimation(getContext()));
}
void shakeExchangeField() {
tvAmountB.startAnimation(Helper.getShakeAnimation(getContext()));
}
public void setRate(double xmrBtcRate) {
this.xmrBtcRate = xmrBtcRate;
post(new Runnable() {
@Override
public void run() {
exchange();
}
});
}
public void setAmount(String btcAmount) {
this.btcAmount = btcAmount;
etAmountA.setText(btcAmount);
xmrAmount = null;
exchange();
}
public void setEditable(boolean editable) {
etAmountA.setEnabled(editable);
}
public String getAmount() {
return btcAmount;
}
EditText etAmountA;
TextView tvAmountB;
Spinner sCurrencyA;
Spinner sCurrencyB;
public ExchangeBtcEditText(Context context) {
super(context);
initializeViews(context);
}
public ExchangeBtcEditText(Context context, AttributeSet attrs) {
super(context, attrs);
initializeViews(context);
}
public ExchangeBtcEditText(Context context,
AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
initializeViews(context);
}
/**
* Inflates the views in the layout.
*
* @param context the current context for the view.
*/
private void initializeViews(Context context) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.view_exchange_btc_edit, this);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
etAmountA = findViewById(R.id.etAmountA);
etAmountA.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {
exchange();
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
});
tvAmountB = findViewById(R.id.tvAmountB);
sCurrencyA = findViewById(R.id.sCurrencyA);
sCurrencyB = findViewById(R.id.sCurrencyB);
ArrayAdapter<String> btcAdapter = new ArrayAdapter<String>(getContext(),
android.R.layout.simple_spinner_item,
new String[]{"BTC"});
sCurrencyA.setAdapter(btcAdapter);
sCurrencyA.setEnabled(false);
ArrayAdapter<String> xmrAdapter = new ArrayAdapter<String>(getContext(),
android.R.layout.simple_spinner_item,
new String[]{"XMR"});
sCurrencyB.setAdapter(xmrAdapter);
sCurrencyB.setEnabled(false);
etAmountA.setFocusable(true);
etAmountA.setFocusableInTouchMode(true);
}
double xmrBtcRate = 0;
public void exchange() {
btcAmount = etAmountA.getText().toString();
if (!btcAmount.isEmpty() && (xmrBtcRate > 0)) {
double xmr = xmrBtcRate * Double.parseDouble(btcAmount);
xmrAmount = Helper.getFormattedAmount(xmr, true);
} else {
xmrAmount = "";
}
tvAmountB.setText(getResources().getString(R.string.send_amount_btc_xmr, xmrAmount));
Timber.d("%s BTC =%f> %s XMR", btcAmount, xmrBtcRate, xmrAmount);
}
}

View File

@@ -0,0 +1,196 @@
/*
* Copyright (c) 2017-2019 m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// based on https://code.tutsplus.com/tutorials/creating-compound-views-on-android--cms-22889
package com.m2049r.xmrwallet.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.widget.Spinner;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
import com.m2049r.xmrwallet.util.Helper;
import java.util.ArrayList;
import java.util.List;
import timber.log.Timber;
public class ExchangeOtherEditText extends ExchangeEditText {
/*
all exchanges are done through XMR
baseCurrency is the native currency
*/
String baseCurrency = null; // not XMR
private double exchangeRate = 0; // baseCurrency to XMR
public void setExchangeRate(double rate) {
exchangeRate = rate;
post(new Runnable() {
@Override
public void run() {
startExchange();
}
});
}
private void setBaseCurrency(Context context, AttributeSet attrs) {
TypedArray ta = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ExchangeEditText, 0, 0);
try {
baseCurrency = ta.getString(R.styleable.ExchangeEditText_baseSymbol);
if (baseCurrency == null)
throw new IllegalArgumentException("base currency must be set");
} finally {
ta.recycle();
}
}
public ExchangeOtherEditText(Context context, AttributeSet attrs) {
super(context, attrs);
setBaseCurrency(context, attrs);
}
public ExchangeOtherEditText(Context context,
AttributeSet attrs,
int defStyle) {
super(context, attrs, defStyle);
setBaseCurrency(context, attrs);
}
@Override
void setCurrencyAdapter(Spinner spinner) {
List<String> currencies = new ArrayList<>();
if (!baseCurrency.equals(Helper.BASE_CRYPTO)) currencies.add(baseCurrency);
currencies.add(Helper.BASE_CRYPTO);
setCurrencyAdapter(spinner, currencies);
}
@Override
void setInitialSpinnerSelections(Spinner baseSpinner, Spinner quoteSpinner) {
baseSpinner.setSelection(0, true);
quoteSpinner.setSelection(1, true);
}
private void localExchange(final String base, final String quote, final double rate) {
exchange(new ExchangeRate() {
@Override
public String getServiceName() {
return "Local";
}
@Override
public String getBaseCurrency() {
return base;
}
@Override
public String getQuoteCurrency() {
return quote;
}
@Override
public double getRate() {
return rate;
}
});
}
@Override
void execExchange(String currencyA, String currencyB) {
if (!currencyA.equals(baseCurrency) && !currencyB.equals(baseCurrency)) {
throw new IllegalStateException("I can only exchange " + baseCurrency);
}
showProgress();
Timber.d("execExchange(%s, %s)", currencyA, currencyB);
// first deal with XMR/baseCurrency & baseCurrency/XMR
if (currencyA.equals(Helper.BASE_CRYPTO) && (currencyB.equals(baseCurrency))) {
localExchange(currencyA, currencyB, 1.0d / exchangeRate);
return;
}
if (currencyA.equals(baseCurrency) && (currencyB.equals(Helper.BASE_CRYPTO))) {
localExchange(currencyA, currencyB, exchangeRate);
return;
}
// next, deal with XMR/baseCurrency
if (currencyA.equals(baseCurrency)) {
queryExchangeRate(Helper.BASE_CRYPTO, currencyB, exchangeRate, true);
} else {
queryExchangeRate(currencyA, Helper.BASE_CRYPTO, 1.0d / exchangeRate, false);
}
}
private void queryExchangeRate(final String base, final String quote, final double factor,
final boolean baseIsBaseCrypto) {
queryExchangeRate(base, quote,
new ExchangeCallback() {
@Override
public void onSuccess(final ExchangeRate exchangeRate) {
if (isAttachedToWindow())
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
ExchangeRate xchange = new ExchangeRate() {
@Override
public String getServiceName() {
return exchangeRate.getServiceName() + "+" + baseCurrency;
}
@Override
public String getBaseCurrency() {
return baseIsBaseCrypto ? baseCurrency : base;
}
@Override
public String getQuoteCurrency() {
return baseIsBaseCrypto ? quote : baseCurrency;
}
@Override
public double getRate() {
return exchangeRate.getRate() * factor;
}
};
exchange(xchange);
}
});
}
@Override
public void onError(final Exception e) {
Timber.e(e.getLocalizedMessage());
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
exchangeFailed();
}
});
}
});
}
}

View File

@@ -73,6 +73,8 @@ public interface QueryOrderStatus {
String getXmrReceivingAddress(); // "xmr_receiving_address": "xmr_old_style_address_user_can_send_funds_to_as_string", String getXmrReceivingAddress(); // "xmr_receiving_address": "xmr_old_style_address_user_can_send_funds_to_as_string",
String getXmrReceivingSubaddress(); // <xmr_subaddress_user_needs_to_send_funds_to_as_string>,
String getXmrReceivingIntegratedAddress(); // "xmr_receiving_integrated_address": "xmr_integrated_address_user_needs_to_send_funds_to_as_string", String getXmrReceivingIntegratedAddress(); // "xmr_receiving_integrated_address": "xmr_integrated_address_user_needs_to_send_funds_to_as_string",
int getXmrRecommendedMixin(); // "xmr_recommended_mixin": <xmr_recommended_mixin_as_integer>, int getXmrRecommendedMixin(); // "xmr_recommended_mixin": <xmr_recommended_mixin_as_integer>,

View File

@@ -52,6 +52,7 @@ class QueryOrderStatusImpl implements QueryOrderStatus {
private double xmrAmountRemaining; // "xmr_amount_remaining": <amount_in_xmr_that_the_user_must_still_send_as_float>, private double xmrAmountRemaining; // "xmr_amount_remaining": <amount_in_xmr_that_the_user_must_still_send_as_float>,
private int xmrNumConfirmationsRemaining; // "xmr_num_confirmations_remaining": <num_xmr_confirmations_remaining_before_bitcoins_will_be_sent_as_integer>, private int xmrNumConfirmationsRemaining; // "xmr_num_confirmations_remaining": <num_xmr_confirmations_remaining_before_bitcoins_will_be_sent_as_integer>,
private double xmrPriceBtc; // "xmr_price_btc": <price_of_1_btc_in_xmr_as_offered_by_service_as_float>, private double xmrPriceBtc; // "xmr_price_btc": <price_of_1_btc_in_xmr_as_offered_by_service_as_float>,
private String xmrReceivingSubaddress; // <xmr_subaddress_user_needs_to_send_funds_to_as_string>,
private String xmrReceivingAddress; // "xmr_receiving_address": "xmr_old_style_address_user_can_send_funds_to_as_string", private String xmrReceivingAddress; // "xmr_receiving_address": "xmr_old_style_address_user_can_send_funds_to_as_string",
private String xmrReceivingIntegratedAddress; // "xmr_receiving_integrated_address": "xmr_integrated_address_user_needs_to_send_funds_to_as_string", private String xmrReceivingIntegratedAddress; // "xmr_receiving_integrated_address": "xmr_integrated_address_user_needs_to_send_funds_to_as_string",
private int xmrRecommendedMixin; // "xmr_recommended_mixin": <xmr_recommended_mixin_as_integer>, private int xmrRecommendedMixin; // "xmr_recommended_mixin": <xmr_recommended_mixin_as_integer>,
@@ -115,6 +116,10 @@ class QueryOrderStatusImpl implements QueryOrderStatus {
return xmrPriceBtc; return xmrPriceBtc;
} }
public String getXmrReceivingSubaddress() {
return xmrReceivingSubaddress;
}
public String getXmrReceivingAddress() { public String getXmrReceivingAddress() {
return xmrReceivingAddress; return xmrReceivingAddress;
} }
@@ -208,6 +213,7 @@ class QueryOrderStatusImpl implements QueryOrderStatus {
xmrAmountRemaining = jsonObject.getDouble("xmr_amount_remaining"); // "xmr_amount_remaining": <amount_in_xmr_that_the_user_must_still_send_as_float>, xmrAmountRemaining = jsonObject.getDouble("xmr_amount_remaining"); // "xmr_amount_remaining": <amount_in_xmr_that_the_user_must_still_send_as_float>,
xmrNumConfirmationsRemaining = jsonObject.getInt("xmr_num_confirmations_remaining"); // "xmr_num_confirmations_remaining": <num_xmr_confirmations_remaining_before_bitcoins_will_be_sent_as_integer>, xmrNumConfirmationsRemaining = jsonObject.getInt("xmr_num_confirmations_remaining"); // "xmr_num_confirmations_remaining": <num_xmr_confirmations_remaining_before_bitcoins_will_be_sent_as_integer>,
xmrPriceBtc = jsonObject.getDouble("xmr_price_btc"); // "xmr_price_btc": <price_of_1_btc_in_xmr_as_offered_by_service_as_float>, xmrPriceBtc = jsonObject.getDouble("xmr_price_btc"); // "xmr_price_btc": <price_of_1_btc_in_xmr_as_offered_by_service_as_float>,
xmrReceivingSubaddress = jsonObject.getString("xmr_receiving_subaddress"); // <xmr_subaddress_user_needs_to_send_funds_to_as_string>,
xmrReceivingAddress = jsonObject.getString("xmr_receiving_address"); // "xmr_receiving_address": "xmr_old_style_address_user_can_send_funds_to_as_string", xmrReceivingAddress = jsonObject.getString("xmr_receiving_address"); // "xmr_receiving_address": "xmr_old_style_address_user_can_send_funds_to_as_string",
xmrReceivingIntegratedAddress = jsonObject.getString("xmr_receiving_integrated_address"); // "xmr_receiving_integrated_address": "xmr_integrated_address_user_needs_to_send_funds_to_as_string", xmrReceivingIntegratedAddress = jsonObject.getString("xmr_receiving_integrated_address"); // "xmr_receiving_integrated_address": "xmr_integrated_address_user_needs_to_send_funds_to_as_string",
xmrRecommendedMixin = jsonObject.getInt("xmr_recommended_mixin"); // "xmr_recommended_mixin": <xmr_recommended_mixin_as_integer>, xmrRecommendedMixin = jsonObject.getInt("xmr_recommended_mixin"); // "xmr_recommended_mixin": <xmr_recommended_mixin_as_integer>,

View File

@@ -10,54 +10,43 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<LinearLayout <android.support.design.widget.TextInputLayout
android:id="@+id/etWalletName"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:layout_marginBottom="@dimen/header_top_first"
android:weightSum="2"> app:errorEnabled="true">
<android.support.design.widget.TextInputLayout <android.support.design.widget.TextInputEditText
android:id="@+id/etWalletName" style="@style/MoneroEdit"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox" android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:hint="@string/generate_name_hint"
app:counterEnabled="true" android:imeOptions="actionNext"
app:counterMaxLength="20" android:inputType="text"
android:layout_marginEnd="4dp" android:maxLines="1"
app:errorEnabled="true"> android:textAlignment="textStart" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputEditText <android.support.design.widget.TextInputLayout
style="@style/MoneroEdit" android:id="@+id/etWalletPassword"
android:layout_width="match_parent" style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:hint="@string/generate_name_hint" android:layout_height="wrap_content"
android:imeOptions="actionNext" android:layout_marginBottom="@dimen/header_top_first"
android:inputType="text" app:errorEnabled="true">
android:maxLines="1"
android:textAlignment="textStart" />
</android.support.design.widget.TextInputLayout> <android.support.design.widget.TextInputEditText
style="@style/MoneroEdit"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/generate_password_hint"
android:imeOptions="actionNext"
android:inputType="textVisiblePassword"
android:textAlignment="textStart" />
<android.support.design.widget.TextInputLayout </android.support.design.widget.TextInputLayout>
android:id="@+id/etWalletPassword"
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="0dp"
android:layout_marginStart="4dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<android.support.design.widget.TextInputEditText
style="@style/MoneroEdit"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:hint="@string/generate_password_hint"
android:imeOptions="actionNext"
android:inputType="textVisiblePassword"
android:textAlignment="textStart" />
</android.support.design.widget.TextInputLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/llFingerprintAuth" android:id="@+id/llFingerprintAuth"
@@ -85,6 +74,7 @@
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox" style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/header_top_first"
android:visibility="gone" android:visibility="gone"
app:errorEnabled="true"> app:errorEnabled="true">
@@ -103,8 +93,8 @@
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox" style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/header_top_first"
android:visibility="gone" android:visibility="gone"
app:counterEnabled="true" app:counterEnabled="true"
app:counterMaxLength="95" app:counterMaxLength="95"
app:errorEnabled="true"> app:errorEnabled="true">
@@ -124,6 +114,7 @@
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox" style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/header_top_first"
android:visibility="gone" android:visibility="gone"
app:counterEnabled="true" app:counterEnabled="true"
app:counterMaxLength="64" app:counterMaxLength="64"
@@ -144,6 +135,7 @@
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox" style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/header_top_first"
android:visibility="gone" android:visibility="gone"
app:counterEnabled="true" app:counterEnabled="true"
app:counterMaxLength="64" app:counterMaxLength="64"
@@ -164,6 +156,7 @@
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox" style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/header_top_first"
android:visibility="gone" android:visibility="gone"
app:errorEnabled="true"> app:errorEnabled="true">

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -53,11 +54,12 @@
</LinearLayout> </LinearLayout>
</FrameLayout> </FrameLayout>
<com.m2049r.xmrwallet.widget.ExchangeBtcEditText <com.m2049r.xmrwallet.widget.ExchangeOtherEditText
android:id="@+id/etAmount" android:id="@+id/etAmount"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:orientation="vertical" /> android:layout_marginBottom="16dp"
android:orientation="vertical"
app:baseSymbol="BTC" />
</LinearLayout> </LinearLayout>

View File

@@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="LinearLayout">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:orientation="horizontal">
<Spinner
android:id="@+id/sCurrencyA"
android:layout_width="56dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:textAlignment="center" />
<android.support.design.widget.TextInputLayout
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.TextInputEditText
android:id="@+id/etAmountA"
style="@style/MoneroText.Balance.Orange"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center|start"
android:layout_marginStart="16dp"
android:layout_weight="3"
android:hint="@string/send_amount_hint"
android:imeOptions="actionDone"
android:inputType="numberDecimal"
android:padding="4dp"
android:singleLine="true"
android:textAlignment="textStart"
tools:text="87.00000" />
</android.support.design.widget.TextInputLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<Spinner
android:id="@+id/sCurrencyB"
android:layout_width="56sp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:textAlignment="center" />
<TextView
android:id="@+id/tvAmountB"
style="@style/MoneroText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center|start"
android:layout_marginStart="16dp"
android:layout_weight="3"
android:hint="@string/send_amount_hint"
android:padding="4dp"
android:singleLine="true"
tools:text="87.00000" />
</LinearLayout>
</merge>

View File

@@ -14,30 +14,23 @@
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:entries="@array/currency"
android:gravity="center" android:gravity="center"
android:textAlignment="center" /> android:textAlignment="center" />
<android.support.design.widget.TextInputLayout <EditText
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox" android:id="@+id/etAmountA"
android:layout_width="match_parent" style="@style/MoneroText.Balance.Orange"
android:layout_height="wrap_content"> android:layout_width="0dp"
android:layout_height="wrap_content"
<android.support.design.widget.TextInputEditText android:layout_gravity="center|start"
android:id="@+id/etAmountA" android:layout_marginStart="16dp"
style="@style/MoneroText.Balance.Orange" android:layout_weight="3"
android:layout_width="match_parent" android:imeOptions="actionDone"
android:layout_height="wrap_content" android:inputType="numberDecimal"
android:layout_gravity="center|start" android:padding="4dp"
android:layout_marginStart="16dp" android:singleLine="true"
android:layout_weight="3" android:textAlignment="textStart"
android:imeOptions="actionDone" tools:text="87.00000" />
android:inputType="numberDecimal"
android:padding="4dp"
android:singleLine="true"
android:textAlignment="textStart"
tools:text="87.00000" />
</android.support.design.widget.TextInputLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -52,7 +45,6 @@
android:layout_width="56sp" android:layout_width="56sp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:entries="@array/currency"
android:gravity="center" android:gravity="center"
android:textAlignment="center" /> android:textAlignment="center" />

View File

@@ -292,43 +292,6 @@
<string name="archive_alert_yes">はい、お願いします!</string> <string name="archive_alert_yes">はい、お願いします!</string>
<string name="archive_alert_no">いいえ、結構です!</string> <string name="archive_alert_no">いいえ、結構です!</string>
<string-array name="currency" translatable="false">
<item>XMR</item>
<item>EUR</item>
<item>USD</item>
<item>JPY</item>
<item>GBP</item>
<item>CHF</item>
<item>CAD</item>
<item>AUD</item>
<item>ZAR</item>
<item>BRL</item>
<item>CLP</item>
<item>CNY</item>
<item>CZK</item>
<item>DKK</item>
<item>HKD</item>
<item>HUF</item>
<item>IDR</item>
<item>ILS</item>
<item>INR</item>
<item>KRW</item>
<item>MXN</item>
<item>MYR</item>
<item>NOK</item>
<item>NZD</item>
<item>PHP</item>
<item>PKR</item>
<item>PLN</item>
<item>RUB</item>
<item>SEK</item>
<item>SGD</item>
<item>THB</item>
<item>TRY</item>
<item>TWD</item>
</string-array>
<string name="fab_create_new">新しいウォレットの作成</string> <string name="fab_create_new">新しいウォレットの作成</string>
<string name="fab_restore_viewonly">閲覧専用ウォレットを復元</string> <string name="fab_restore_viewonly">閲覧専用ウォレットを復元</string>
<string name="fab_restore_key">秘密鍵からウォレットを復元</string> <string name="fab_restore_key">秘密鍵からウォレットを復元</string>

View File

@@ -7,4 +7,7 @@
<attr name="activeDot" format="integer" /> <attr name="activeDot" format="integer" />
<attr name="numberDots" format="integer" /> <attr name="numberDots" format="integer" />
</declare-styleable> </declare-styleable>
<declare-styleable name="ExchangeEditText">
<attr name="baseSymbol" format="string" />
</declare-styleable>
</resources> </resources>

View File

@@ -297,40 +297,40 @@
<string name="archive_alert_no">No thanks!</string> <string name="archive_alert_no">No thanks!</string>
<string-array name="currency" translatable="false"> <string-array name="currency" translatable="false">
<item>XMR</item>
<item>EUR</item> <item>EUR</item>
<item>USD</item> <item>USD</item>
<item>JPY</item> <item>JPY</item>
<item>GBP</item> <item>GBP</item>
<item>CHF</item> <item>CHF</item>
<item>CAD</item> <item>CAD</item>
<item>AUD</item>
<item>ZAR</item>
<item>AUD</item>
<item>BGN</item>
<item>BRL</item> <item>BRL</item>
<item>CLP</item>
<item>CNY</item> <item>CNY</item>
<item>CZK</item> <item>CZK</item>
<item>DKK</item> <item>DKK</item>
<item>HKD</item> <item>HKD</item>
<item>HRK</item>
<item>HUF</item> <item>HUF</item>
<item>IDR</item> <item>IDR</item>
<item>ILS</item> <item>ILS</item>
<item>INR</item> <item>INR</item>
<item>ISK</item>
<item>KRW</item> <item>KRW</item>
<item>MXN</item> <item>MXN</item>
<item>MYR</item> <item>MYR</item>
<item>NOK</item> <item>NOK</item>
<item>NZD</item> <item>NZD</item>
<item>PHP</item> <item>PHP</item>
<item>PKR</item>
<item>PLN</item> <item>PLN</item>
<item>RON</item>
<item>RUB</item> <item>RUB</item>
<item>SEK</item> <item>SEK</item>
<item>SGD</item> <item>SGD</item>
<item>THB</item> <item>THB</item>
<item>TRY</item> <item>TRY</item>
<item>TWD</item> <item>ZAR</item>
</string-array> </string-array>
<string name="fab_create_new">Create new wallet</string> <string name="fab_create_new">Create new wallet</string>

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2017-2018 m2049r et al. * Copyright (c) 2017 m2049r et al.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package com.m2049r.xmrwallet.service.exchange.coinmarketcap; package com.m2049r.xmrwallet.service.exchange.kraken;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi; import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback; import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
@@ -71,9 +71,9 @@ public class ExchangeRateTest {
@Test @Test
public void queryExchangeRate_shouldBeGetMethod() public void queryExchangeRate_shouldBeGetMethod()
throws InterruptedException { throws InterruptedException, TimeoutException {
exchangeApi.queryExchangeRate("XMR", "EUR", mockExchangeCallback); exchangeApi.queryExchangeRate("XMR", "USD", mockExchangeCallback);
RecordedRequest request = mockWebServer.takeRequest(); RecordedRequest request = mockWebServer.takeRequest();
assertEquals("GET", request.getMethod()); assertEquals("GET", request.getMethod());
@@ -81,48 +81,20 @@ public class ExchangeRateTest {
@Test @Test
public void queryExchangeRate_shouldHavePairInUrl() public void queryExchangeRate_shouldHavePairInUrl()
throws InterruptedException { throws InterruptedException, TimeoutException {
exchangeApi.queryExchangeRate("XMR", "EUR", mockExchangeCallback); exchangeApi.queryExchangeRate("XMR", "USD", mockExchangeCallback);
RecordedRequest request = mockWebServer.takeRequest(); RecordedRequest request = mockWebServer.takeRequest();
assertEquals("/328/?convert=EUR", request.getPath()); assertEquals("/?pair=XMRUSD", request.getPath());
} }
@Test @Test
public void queryExchangeRate_wasSuccessfulShouldRespondWithRate() public void queryExchangeRate_wasSuccessfulShouldRespondWithRate()
throws TimeoutException { throws InterruptedException, JSONException, TimeoutException {
final String base = "XMR";
final String quote = "EUR";
final double rate = 1.56;
MockResponse jsonMockResponse = new MockResponse().setBody(
createMockExchangeRateResponse(base, quote, rate));
mockWebServer.enqueue(jsonMockResponse);
exchangeApi.queryExchangeRate(base, quote, new ExchangeCallback() {
@Override
public void onSuccess(final ExchangeRate exchangeRate) {
waiter.assertEquals(exchangeRate.getBaseCurrency(), base);
waiter.assertEquals(exchangeRate.getQuoteCurrency(), quote);
waiter.assertEquals(exchangeRate.getRate(), rate);
waiter.resume();
}
@Override
public void onError(final Exception e) {
waiter.fail(e);
waiter.resume();
}
});
waiter.await();
}
@Test
public void queryExchangeRate_wasSuccessfulShouldRespondWithRateUSD()
throws TimeoutException {
final String base = "XMR"; final String base = "XMR";
final String quote = "USD"; final String quote = "USD";
final double rate = 1.56; final double rate = 100;
MockResponse jsonMockResponse = new MockResponse().setBody( MockResponse jsonMockResponse = new MockResponse().setBody(
createMockExchangeRateResponse(base, quote, rate)); createMockExchangeRateResponse(base, quote, rate));
mockWebServer.enqueue(jsonMockResponse); mockWebServer.enqueue(jsonMockResponse);
@@ -147,9 +119,8 @@ public class ExchangeRateTest {
@Test @Test
public void queryExchangeRate_wasNotSuccessfulShouldCallOnError() public void queryExchangeRate_wasNotSuccessfulShouldCallOnError()
throws TimeoutException { throws InterruptedException, JSONException, TimeoutException {
mockWebServer.enqueue(new MockResponse().setResponseCode(500)); mockWebServer.enqueue(new MockResponse().setResponseCode(500));
exchangeApi.queryExchangeRate("XMR", "USD", new ExchangeCallback() { exchangeApi.queryExchangeRate("XMR", "USD", new ExchangeCallback() {
@Override @Override
public void onSuccess(final ExchangeRate exchangeRate) { public void onSuccess(final ExchangeRate exchangeRate) {
@@ -170,11 +141,10 @@ public class ExchangeRateTest {
@Test @Test
public void queryExchangeRate_unknownAssetShouldCallOnError() public void queryExchangeRate_unknownAssetShouldCallOnError()
throws TimeoutException { throws InterruptedException, JSONException, TimeoutException {
MockResponse jsonMockResponse = new MockResponse().setBody( mockWebServer.enqueue(new MockResponse().
createMockExchangeRateErrorResponse()); setResponseCode(200).
mockWebServer.enqueue(jsonMockResponse); setBody("{\"error\":[\"EQuery:Unknown asset pair\"]}"));
exchangeApi.queryExchangeRate("XMR", "ABC", new ExchangeCallback() { exchangeApi.queryExchangeRate("XMR", "ABC", new ExchangeCallback() {
@Override @Override
public void onSuccess(final ExchangeRate exchangeRate) { public void onSuccess(final ExchangeRate exchangeRate) {
@@ -187,7 +157,7 @@ public class ExchangeRateTest {
waiter.assertTrue(e instanceof ExchangeException); waiter.assertTrue(e instanceof ExchangeException);
ExchangeException ex = (ExchangeException) e; ExchangeException ex = (ExchangeException) e;
waiter.assertTrue(ex.getCode() == 200); waiter.assertTrue(ex.getCode() == 200);
waiter.assertEquals(ex.getErrorMsg(), "id not found"); waiter.assertEquals(ex.getErrorMsg(), "EQuery:Unknown asset pair");
waiter.resume(); waiter.resume();
} }
@@ -195,52 +165,22 @@ public class ExchangeRateTest {
waiter.await(); waiter.await();
} }
private String createMockExchangeRateResponse(final String base, final String quote, final double rate) { static public String createMockExchangeRateResponse(final String base, final String quote, final double rate) {
return "{\n" + return "{\n" +
" \"data\": {\n" + " \"error\":[],\n" +
" \"id\": 328, \n" + " \"result\":{\n" +
" \"name\": \"Monero\", \n" + " \"X" + base + "Z" + quote + "\":{\n" +
" \"symbol\": \"" + base + "\", \n" + " \"a\":[\"" + rate + "\",\"322\",\"322.000\"],\n" +
" \"website_slug\": \"monero\", \n" + " \"b\":[\"" + rate + "\",\"76\",\"76.000\"],\n" +
" \"rank\": 12, \n" + " \"c\":[\"" + rate + "\",\"2.90000000\"],\n" +
" \"circulating_supply\": 16112286.0, \n" + " \"v\":[\"4559.03962053\",\"5231.33235586\"],\n" +
" \"total_supply\": 16112286.0, \n" + " \"p\":[\"" + rate + "\",\"" + rate + "\"],\n" +
" \"max_supply\": null, \n" + " \"t\":[801,1014],\n" +
" \"quotes\": {\n" + " \"l\":[\"" + (rate * 0.8) + "\",\"" + rate + "\"],\n" +
" \"USD\": {\n" + " \"h\":[\"" + (rate * 1.2) + "\",\"" + rate + "\"],\n" +
" \"price\": " + rate + ", \n" + " \"o\":\"" + rate + "\"\n" +
" \"volume_24h\": 35763700.0, \n" + " }\n" +
" \"market_cap\": 2559791130.0, \n" + " }\n" +
" \"percent_change_1h\": -0.16, \n" +
" \"percent_change_24h\": -3.46, \n" +
" \"percent_change_7d\": 1.49\n" +
" }, \n" +
(!"USD".equals(quote) ? (
" \"" + quote + "\": {\n" +
" \"price\": " + rate + ", \n" +
" \"volume_24h\": 30377728.701265607, \n" +
" \"market_cap\": 2174289586.0, \n" +
" \"percent_change_1h\": -0.16, \n" +
" \"percent_change_24h\": -3.46, \n" +
" \"percent_change_7d\": 1.49\n" +
" }\n") : "") +
" }, \n" +
" \"last_updated\": 1528492746\n" +
" }, \n" +
" \"metadata\": {\n" +
" \"timestamp\": 1528492705, \n" +
" \"error\": null\n" +
" }\n" +
"}";
}
private String createMockExchangeRateErrorResponse() {
return "{\n" +
" \"data\": null, \n" +
" \"metadata\": {\n" +
" \"timestamp\": 1525137187, \n" +
" \"error\": \"id not found\"\n" +
" }\n" +
"}"; "}";
} }
} }

View File

@@ -132,6 +132,7 @@ public class XmrToApiQueryOrderTest {
final double xmrAmountRemaining = 6.464; final double xmrAmountRemaining = 6.464;
final int xmrNumConfirmationsRemaining = -1; final int xmrNumConfirmationsRemaining = -1;
final double xmrPriceBtc = 0.0154703; final double xmrPriceBtc = 0.0154703;
final String xmrReceivingSubaddress = "83BGzCTthheE2KxNTBPnPJjJUthYPfDfCf3ENSVQcpga8RYSxNz9qCz1qp9MLye9euMjckGi11cRdeVGqsVqTLgH8w5fJ1D";
final String xmrReceivingAddress = "44TVPcCSHebEQp4LnapPkhb2pondb2Ed7GJJLc6TkKwtSyumUnQ6QzkCCkojZycH2MRfLcujCM7QR1gdnRULRraV4UpB5n4"; final String xmrReceivingAddress = "44TVPcCSHebEQp4LnapPkhb2pondb2Ed7GJJLc6TkKwtSyumUnQ6QzkCCkojZycH2MRfLcujCM7QR1gdnRULRraV4UpB5n4";
final String xmrReceivingIntegratedAddress = "4EAAQR1vtv7EQp4LnapPkhb2pondb2Ed7GJJLc6TkKwtSyumUnQ6QzkCCkojZycH2MRfLcujCM7QR1gdnRULRraV6B5rRtHLeXGQSECXy9"; final String xmrReceivingIntegratedAddress = "4EAAQR1vtv7EQp4LnapPkhb2pondb2Ed7GJJLc6TkKwtSyumUnQ6QzkCCkojZycH2MRfLcujCM7QR1gdnRULRraV6B5rRtHLeXGQSECXy9";
final int xmrRecommendedMixin = 5; final int xmrRecommendedMixin = 5;
@@ -155,6 +156,7 @@ public class XmrToApiQueryOrderTest {
xmrAmountRemaining, xmrAmountRemaining,
xmrNumConfirmationsRemaining, xmrNumConfirmationsRemaining,
xmrPriceBtc, xmrPriceBtc,
xmrReceivingSubaddress,
xmrReceivingAddress, xmrReceivingAddress,
xmrReceivingIntegratedAddress, xmrReceivingIntegratedAddress,
xmrRecommendedMixin, xmrRecommendedMixin,
@@ -184,6 +186,7 @@ public class XmrToApiQueryOrderTest {
waiter.assertEquals(orderStatus.getXmrAmountRemaining(), xmrAmountRemaining); waiter.assertEquals(orderStatus.getXmrAmountRemaining(), xmrAmountRemaining);
waiter.assertEquals(orderStatus.getXmrNumConfirmationsRemaining(), xmrNumConfirmationsRemaining); waiter.assertEquals(orderStatus.getXmrNumConfirmationsRemaining(), xmrNumConfirmationsRemaining);
waiter.assertEquals(orderStatus.getXmrPriceBtc(), xmrPriceBtc); waiter.assertEquals(orderStatus.getXmrPriceBtc(), xmrPriceBtc);
waiter.assertEquals(orderStatus.getXmrReceivingSubaddress(), xmrReceivingSubaddress);
waiter.assertEquals(orderStatus.getXmrReceivingAddress(), xmrReceivingAddress); waiter.assertEquals(orderStatus.getXmrReceivingAddress(), xmrReceivingAddress);
waiter.assertEquals(orderStatus.getXmrReceivingIntegratedAddress(), xmrReceivingIntegratedAddress); waiter.assertEquals(orderStatus.getXmrReceivingIntegratedAddress(), xmrReceivingIntegratedAddress);
waiter.assertEquals(orderStatus.getXmrRecommendedMixin(), xmrRecommendedMixin); waiter.assertEquals(orderStatus.getXmrRecommendedMixin(), xmrRecommendedMixin);
@@ -267,6 +270,7 @@ public class XmrToApiQueryOrderTest {
final double xmrAmountRemaining, final double xmrAmountRemaining,
final int xmrNumConfirmationsRemaining, final int xmrNumConfirmationsRemaining,
final double xmrPriceBtc, final double xmrPriceBtc,
final String xmrReceivingSubaddress,
final String xmrReceivingAddress, final String xmrReceivingAddress,
final String xmrReceivingIntegratedAddress, final String xmrReceivingIntegratedAddress,
final int xmrRecommendedMixin, final int xmrRecommendedMixin,
@@ -281,6 +285,7 @@ public class XmrToApiQueryOrderTest {
" \"btc_amount\":\"" + btcAmount + "\",\n" + " \"btc_amount\":\"" + btcAmount + "\",\n" +
" \"btc_dest_address\":\"" + btcDestAddress + "\",\n" + " \"btc_dest_address\":\"" + btcDestAddress + "\",\n" +
" \"xmr_required_amount\":\"" + xmrRequiredAmount + "\",\n" + " \"xmr_required_amount\":\"" + xmrRequiredAmount + "\",\n" +
" \"xmr_receiving_subaddress\":\"" + xmrReceivingSubaddress + "\",\n" +
" \"xmr_receiving_address\":\"" + xmrReceivingAddress + "\",\n" + " \"xmr_receiving_address\":\"" + xmrReceivingAddress + "\",\n" +
" \"xmr_receiving_integrated_address\":\"" + xmrReceivingIntegratedAddress + "\",\n" + " \"xmr_receiving_integrated_address\":\"" + xmrReceivingIntegratedAddress + "\",\n" +
" \"xmr_required_payment_id_long\":\"" + xmrRequiredPaymentIdLong + "\",\n" + " \"xmr_required_payment_id_long\":\"" + xmrRequiredPaymentIdLong + "\",\n" +