mirror of https://github.com/m2049r/xmrwallet.git
use kraken for EURXMR exchange rate (#637)
combine with ECB rates for other fiat conversions
This commit is contained in:
parent
f637d7f617
commit
87d9a8cd95
|
@ -50,9 +50,7 @@ import com.m2049r.xmrwallet.widget.Toolbar;
|
|||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
|
@ -220,7 +218,7 @@ public class WalletFragment extends Fragment
|
|||
// at this point selection is XMR in case of error
|
||||
String displayB;
|
||||
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;
|
||||
displayB = Helper.getFormattedAmount(amountB, false);
|
||||
} else { // XMR
|
||||
|
@ -229,7 +227,7 @@ public class WalletFragment extends Fragment
|
|||
showBalance(displayB);
|
||||
}
|
||||
|
||||
String balanceCurrency = Helper.CRYPTO;
|
||||
String balanceCurrency = Helper.BASE_CRYPTO;
|
||||
double balanceRate = 1.0;
|
||||
|
||||
private final ExchangeApi exchangeApi = Helper.getExchangeApi();
|
||||
|
@ -245,7 +243,7 @@ public class WalletFragment extends Fragment
|
|||
Timber.d(currency);
|
||||
if (!currency.equals(balanceCurrency) || (balanceRate <= 0)) {
|
||||
showExchanging();
|
||||
exchangeApi.queryExchangeRate(Helper.CRYPTO, currency,
|
||||
exchangeApi.queryExchangeRate(Helper.BASE_CRYPTO, currency,
|
||||
new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
|
@ -301,10 +299,10 @@ public class WalletFragment extends Fragment
|
|||
|
||||
public void exchange(final ExchangeRate exchangeRate) {
|
||||
hideExchanging();
|
||||
if (!Helper.CRYPTO.equals(exchangeRate.getBaseCurrency())) {
|
||||
if (!Helper.BASE_CRYPTO.equals(exchangeRate.getBaseCurrency())) {
|
||||
Timber.e("Not XMR");
|
||||
sCurrency.setSelection(0, true);
|
||||
balanceCurrency = Helper.CRYPTO;
|
||||
balanceCurrency = Helper.BASE_CRYPTO;
|
||||
balanceRate = 1.0;
|
||||
} else {
|
||||
int spinnerPosition = ((ArrayAdapter) sCurrency.getAdapter()).getPosition(exchangeRate.getQuoteCurrency());
|
||||
|
|
|
@ -116,12 +116,12 @@ public class SendAmountWizardFragment extends SendWizardFragment {
|
|||
sendListener.getTxData().setAmount(Wallet.SWEEP_ALL);
|
||||
}
|
||||
} else {
|
||||
if (!etAmount.validate(maxFunds)) {
|
||||
if (!etAmount.validate(maxFunds, 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sendListener != null) {
|
||||
String xmr = etAmount.getAmount();
|
||||
String xmr = etAmount.getNativeAmount();
|
||||
if (xmr != null) {
|
||||
sendListener.getTxData().setAmount(Wallet.getAmountFromString(xmr));
|
||||
} else {
|
||||
|
@ -148,8 +148,8 @@ public class SendAmountWizardFragment extends SendWizardFragment {
|
|||
tvFunds.setText(getString(R.string.send_available,
|
||||
getString(R.string.unknown_amount)));
|
||||
}
|
||||
// getAmount is null if exchange is in progress
|
||||
if ((etAmount.getAmount() != null) && etAmount.getAmount().isEmpty()) {
|
||||
// getNativeAmount is null if exchange is in progress
|
||||
if ((etAmount.getNativeAmount() != null) && etAmount.getNativeAmount().isEmpty()) {
|
||||
final BarcodeData data = sendListener.popBarcodeData();
|
||||
if ((data != null) && (data.amount != null)) {
|
||||
etAmount.setAmount(data.amount);
|
||||
|
|
|
@ -31,7 +31,8 @@ import com.m2049r.xmrwallet.data.TxDataBtc;
|
|||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
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.xmrto.XmrToError;
|
||||
import com.m2049r.xmrwallet.xmrto.XmrToException;
|
||||
|
@ -61,7 +62,7 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
|||
}
|
||||
|
||||
private TextView tvFunds;
|
||||
private ExchangeBtcEditText etAmount;
|
||||
private ExchangeOtherEditText etAmount;
|
||||
|
||||
private TextView tvXmrToParms;
|
||||
private SendProgressView evParams;
|
||||
|
@ -97,7 +98,7 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
|||
}
|
||||
if (sendListener != null) {
|
||||
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
||||
String btcString = etAmount.getAmount();
|
||||
String btcString = etAmount.getNativeAmount();
|
||||
if (btcString != null) {
|
||||
try {
|
||||
double btc = Double.parseDouble(btcString);
|
||||
|
@ -166,7 +167,7 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
|||
getView().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
etAmount.setRate(1.0d / orderParameters.getPrice());
|
||||
etAmount.setExchangeRate(1.0d / orderParameters.getPrice());
|
||||
NumberFormat df = NumberFormat.getInstance(Locale.US);
|
||||
df.setMaximumFractionDigits(6);
|
||||
String min = df.format(orderParameters.getLowerLimit());
|
||||
|
@ -206,7 +207,7 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
|||
}
|
||||
|
||||
private void processOrderParmsError(final Exception ex) {
|
||||
etAmount.setRate(0);
|
||||
etAmount.setExchangeRate(0);
|
||||
orderParameters = null;
|
||||
maxBtc = 0;
|
||||
minBtc = 0;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -14,7 +14,7 @@
|
|||
* 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.VisibleForTesting;
|
||||
|
@ -36,9 +36,9 @@ import okhttp3.HttpUrl;
|
|||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ExchangeApiImpl implements ExchangeApi {
|
||||
static final String CRYPTO_ID = "328";
|
||||
|
||||
@NonNull
|
||||
private final OkHttpClient okHttpClient;
|
||||
|
@ -47,14 +47,13 @@ public class ExchangeApiImpl implements ExchangeApi {
|
|||
|
||||
//so we can inject the mockserver url
|
||||
@VisibleForTesting
|
||||
ExchangeApiImpl(@NonNull final OkHttpClient okHttpClient, final HttpUrl baseUrl) {
|
||||
|
||||
public ExchangeApiImpl(@NonNull final OkHttpClient okHttpClient, final HttpUrl baseUrl) {
|
||||
this.okHttpClient = okHttpClient;
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -66,29 +65,25 @@ public class ExchangeApiImpl implements ExchangeApi {
|
|||
return;
|
||||
}
|
||||
|
||||
boolean inverse = false;
|
||||
String fiat = null;
|
||||
boolean invertQuery;
|
||||
|
||||
if (baseCurrency.equals(Helper.CRYPTO)) {
|
||||
fiat = quoteCurrency;
|
||||
inverse = false;
|
||||
}
|
||||
|
||||
if (quoteCurrency.equals(Helper.CRYPTO)) {
|
||||
fiat = baseCurrency;
|
||||
inverse = true;
|
||||
}
|
||||
|
||||
if (fiat == null) {
|
||||
callback.onError(new IllegalArgumentException("no fiat specified"));
|
||||
if (Helper.BASE_CRYPTO.equals(baseCurrency)) {
|
||||
invertQuery = false;
|
||||
} else if (Helper.BASE_CRYPTO.equals(quoteCurrency)) {
|
||||
invertQuery = true;
|
||||
} else {
|
||||
callback.onError(new IllegalArgumentException("no crypto specified"));
|
||||
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()
|
||||
.addEncodedPathSegments(CRYPTO_ID + "/")
|
||||
.addQueryParameter("convert", fiat)
|
||||
.addQueryParameter("pair", base + (quote.equals("BTC") ? "XBT" : quote))
|
||||
.build();
|
||||
|
||||
final Request httpRequest = createHttpRequest(url);
|
||||
|
@ -104,13 +99,13 @@ public class ExchangeApiImpl implements ExchangeApi {
|
|||
if (response.isSuccessful()) {
|
||||
try {
|
||||
final JSONObject json = new JSONObject(response.body().string());
|
||||
final JSONObject metadata = json.getJSONObject("metadata");
|
||||
if (!metadata.isNull("error")) {
|
||||
final String errorMsg = metadata.getString("error");
|
||||
final JSONArray jsonError = json.getJSONArray("error");
|
||||
if (jsonError.length() > 0) {
|
||||
final String errorMsg = jsonError.getString(0);
|
||||
callback.onError(new ExchangeException(response.code(), errorMsg));
|
||||
} else {
|
||||
final JSONObject jsonResult = json.getJSONObject("data");
|
||||
reportSuccess(jsonResult, swapAssets, callback);
|
||||
final JSONObject jsonResult = json.getJSONObject("result");
|
||||
reportSuccess(jsonResult, invert, callback);
|
||||
}
|
||||
} catch (JSONException ex) {
|
||||
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
|
|
@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.m2049r.xmrwallet.service.exchange.coinmarketcap;
|
||||
package com.m2049r.xmrwallet.service.exchange.kraken;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
|
@ -25,7 +25,6 @@ import org.json.JSONArray;
|
|||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
@ -38,7 +37,7 @@ class ExchangeRateImpl implements ExchangeRate {
|
|||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return "coinmarketcap.com";
|
||||
return "kraken.com";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -65,21 +64,29 @@ class ExchangeRateImpl implements ExchangeRate {
|
|||
|
||||
ExchangeRateImpl(final JSONObject jsonObject, final boolean swapAssets) throws JSONException, ExchangeException {
|
||||
try {
|
||||
final String baseC = jsonObject.getString("symbol");
|
||||
final JSONObject quotes = jsonObject.getJSONObject("quotes");
|
||||
final Iterator<String> keys = quotes.keys();
|
||||
String key = null;
|
||||
// get key which is not USD unless it is the only one
|
||||
while (keys.hasNext()) {
|
||||
key = keys.next();
|
||||
if (!key.equals("USD")) break;
|
||||
final String key = jsonObject.keys().next(); // we expect only one
|
||||
Pattern pattern = Pattern.compile("^X(.*?)Z(.*?)$");
|
||||
Matcher matcher = pattern.matcher(key);
|
||||
if (matcher.find()) {
|
||||
baseCurrency = swapAssets ? matcher.group(2) : matcher.group(1);
|
||||
quoteCurrency = swapAssets ? matcher.group(1) : matcher.group(2);
|
||||
} else {
|
||||
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) {
|
||||
throw new ExchangeException(ex.getLocalizedMessage());
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -58,7 +58,6 @@ import android.widget.TextView;
|
|||
import com.m2049r.xmrwallet.BuildConfig;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.model.NetworkType;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
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 CRYPTO = "XMR";
|
||||
static public final String BASE_CRYPTO = "XMR";
|
||||
|
||||
static private final String WALLET_DIR = "monerujo" + FLAVOR_SUFFIX;
|
||||
static private final String HOME_DIR = "monero" + FLAVOR_SUFFIX;
|
||||
|
@ -207,22 +206,32 @@ public class Helper {
|
|||
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
|
||||
String displayB;
|
||||
if (isXmr) { // XMR
|
||||
long xmr = Wallet.getAmountFromDouble(amount);
|
||||
if ((xmr > 0) || (amount == 0)) {
|
||||
if (isCrypto) {
|
||||
if ((amount >= 0) || (amount == 0)) {
|
||||
displayB = String.format(Locale.US, "%,.5f", amount);
|
||||
} else {
|
||||
displayB = null;
|
||||
}
|
||||
} else { // not XMR
|
||||
} else { // not crypto
|
||||
displayB = String.format(Locale.US, "%,.2f", amount);
|
||||
}
|
||||
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) {
|
||||
Drawable drawable = ContextCompat.getDrawable(context, drawableId);
|
||||
if (drawable instanceof BitmapDrawable) {
|
||||
|
@ -624,7 +633,7 @@ public class Helper {
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2017 m2049r
|
||||
* 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.
|
||||
|
@ -27,6 +27,7 @@ import android.util.AttributeSet;
|
|||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
@ -35,50 +36,45 @@ import android.widget.Spinner;
|
|||
import android.widget.TextView;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
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 java.util.Locale;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ExchangeEditText extends LinearLayout {
|
||||
|
||||
String xmrAmount = null;
|
||||
String notXmrAmount = null;
|
||||
|
||||
void setXmr(String xmr) {
|
||||
xmrAmount = xmr;
|
||||
if (onNewAmountListener != null) {
|
||||
onNewAmountListener.onNewAmount(xmr);
|
||||
private double getEnteredAmount() {
|
||||
String enteredAmount = etAmountA.getText().toString();
|
||||
try {
|
||||
return Double.parseDouble(enteredAmount);
|
||||
} catch (NumberFormatException ex) {
|
||||
Timber.i(ex.getLocalizedMessage());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public boolean validate(double max) {
|
||||
public boolean validate(double max, double min) {
|
||||
Timber.d("inProgress=%b", isExchangeInProgress());
|
||||
if (isExchangeInProgress()) {
|
||||
shakeExchangeField();
|
||||
return false;
|
||||
}
|
||||
boolean ok = true;
|
||||
if (xmrAmount != null) {
|
||||
try {
|
||||
double amount = Double.parseDouble(xmrAmount);
|
||||
if (amount > max) {
|
||||
ok = false;
|
||||
}
|
||||
if (amount <= 0) { /////////////////////////////
|
||||
ok = false;
|
||||
}
|
||||
} catch (NumberFormatException ex) {
|
||||
// this cannot be
|
||||
Timber.e(ex.getLocalizedMessage());
|
||||
String enteredAmount = etAmountA.getText().toString();
|
||||
try {
|
||||
double amount = Double.parseDouble(enteredAmount);
|
||||
if ((amount < min) || (amount > max)) {
|
||||
ok = false;
|
||||
}
|
||||
} else {
|
||||
} catch (NumberFormatException ex) {
|
||||
// this cannot be
|
||||
Timber.e(ex.getLocalizedMessage());
|
||||
ok = false;
|
||||
}
|
||||
if (!ok) {
|
||||
|
@ -95,22 +91,29 @@ public class ExchangeEditText extends LinearLayout {
|
|||
tvAmountB.startAnimation(Helper.getShakeAnimation(getContext()));
|
||||
}
|
||||
|
||||
public void setAmount(String xmrAmount) {
|
||||
if (xmrAmount != null) {
|
||||
setCurrencyA(0);
|
||||
etAmountA.setText(xmrAmount);
|
||||
setXmr(xmrAmount);
|
||||
this.notXmrAmount = null;
|
||||
doExchange();
|
||||
public void setAmount(String nativeAmount) {
|
||||
if (nativeAmount != null) {
|
||||
etAmountA.setText(nativeAmount);
|
||||
tvAmountB.setText(null);
|
||||
if (sCurrencyA.getSelectedItemPosition() != 0)
|
||||
sCurrencyA.setSelection(0, true); // set native currency & trigger exchange
|
||||
else
|
||||
doExchange();
|
||||
} else {
|
||||
setXmr(null);
|
||||
this.notXmrAmount = null;
|
||||
tvAmountB.setText(null);
|
||||
}
|
||||
}
|
||||
|
||||
public String getAmount() {
|
||||
return xmrAmount;
|
||||
public void setEditable(boolean editable) {
|
||||
etAmountA.setEnabled(editable);
|
||||
}
|
||||
|
||||
public String getNativeAmount() {
|
||||
if (isExchangeInProgress()) return null;
|
||||
if (sCurrencyA.getSelectedItemPosition() == 0)
|
||||
return getCleanAmountString(etAmountA.getText().toString());
|
||||
else
|
||||
return getCleanAmountString(tvAmountB.getText().toString());
|
||||
}
|
||||
|
||||
EditText etAmountA;
|
||||
|
@ -120,23 +123,6 @@ public class ExchangeEditText extends LinearLayout {
|
|||
ImageView evExchange;
|
||||
ProgressBar pbExchange;
|
||||
|
||||
|
||||
public void setCurrencyA(int currency) {
|
||||
if ((currency != 0) && (getCurrencyB() != 0)) {
|
||||
setCurrencyB(0);
|
||||
}
|
||||
sCurrencyA.setSelection(currency, true);
|
||||
doExchange();
|
||||
}
|
||||
|
||||
public void setCurrencyB(int currency) {
|
||||
if ((currency != 0) && (getCurrencyA() != 0)) {
|
||||
setCurrencyA(0);
|
||||
}
|
||||
sCurrencyB.setSelection(currency, true);
|
||||
doExchange();
|
||||
}
|
||||
|
||||
public int getCurrencyA() {
|
||||
return sCurrencyA.getSelectedItemPosition();
|
||||
}
|
||||
|
@ -167,12 +153,32 @@ public class ExchangeEditText extends LinearLayout {
|
|||
*
|
||||
* @param context the current context for the view.
|
||||
*/
|
||||
private void initializeViews(Context context) {
|
||||
void initializeViews(Context context) {
|
||||
LayoutInflater inflater = (LayoutInflater) context
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
inflater.inflate(R.layout.view_exchange_edit, this);
|
||||
}
|
||||
|
||||
void setCurrencyAdapter(Spinner spinner) {
|
||||
List<String> currencies = new ArrayList<>();
|
||||
currencies.add(Helper.BASE_CRYPTO);
|
||||
setCurrencyAdapter(spinner, currencies);
|
||||
}
|
||||
|
||||
protected void setCurrencyAdapter(Spinner spinner, List<String> currencies) {
|
||||
currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency)));
|
||||
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_item, currencies);
|
||||
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
spinner.setAdapter(spinnerAdapter);
|
||||
}
|
||||
|
||||
void setInitialSpinnerSelections(Spinner baseSpinner, Spinner quoteSpinner) {
|
||||
baseSpinner.setSelection(0, true);
|
||||
quoteSpinner.setSelection(0, true);
|
||||
}
|
||||
|
||||
private boolean isInitialized = false;
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
|
@ -198,16 +204,28 @@ public class ExchangeEditText extends LinearLayout {
|
|||
evExchange = findViewById(R.id.evExchange);
|
||||
pbExchange = findViewById(R.id.pbExchange);
|
||||
|
||||
setCurrencyAdapter(sCurrencyA);
|
||||
setCurrencyAdapter(sCurrencyB);
|
||||
|
||||
post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setInitialSpinnerSelections(sCurrencyA, sCurrencyB);
|
||||
isInitialized = true;
|
||||
startExchange();
|
||||
}
|
||||
});
|
||||
|
||||
// make progress circle gray
|
||||
pbExchange.getIndeterminateDrawable().
|
||||
setColorFilter(getResources().getColor(R.color.trafficGray),
|
||||
android.graphics.PorterDuff.Mode.MULTIPLY);
|
||||
|
||||
|
||||
sCurrencyA.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parentView, View selectedItemView, int position, long id) {
|
||||
if (position != 0) { // if not XMR, select XMR on other
|
||||
if (!isInitialized) return;
|
||||
if (position != 0) { // if not native, select native on other
|
||||
sCurrencyB.setSelection(0, true);
|
||||
}
|
||||
doExchange();
|
||||
|
@ -222,7 +240,8 @@ public class ExchangeEditText extends LinearLayout {
|
|||
sCurrencyB.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(final AdapterView<?> parentView, View selectedItemView, int position, long id) {
|
||||
if (position != 0) { // if not XMR, select XMR on other
|
||||
if (!isInitialized) return;
|
||||
if (position != 0) { // if not native, select native on other
|
||||
sCurrencyA.setSelection(0, true);
|
||||
}
|
||||
doExchange();
|
||||
|
@ -235,43 +254,56 @@ public class ExchangeEditText extends LinearLayout {
|
|||
});
|
||||
}
|
||||
|
||||
public void doExchange() {
|
||||
tvAmountB.setText(null);
|
||||
// use cached exchange rate if we have it
|
||||
if (!isExchangeInProgress()) {
|
||||
String enteredCurrencyA = (String) sCurrencyA.getSelectedItem();
|
||||
String enteredCurrencyB = (String) sCurrencyB.getSelectedItem();
|
||||
if ((enteredCurrencyA + enteredCurrencyB).equals(assetPair)) {
|
||||
if (prepareExchange()) {
|
||||
exchange(assetRate);
|
||||
} else {
|
||||
clearAmounts();
|
||||
}
|
||||
} else {
|
||||
clearAmounts();
|
||||
startExchange();
|
||||
}
|
||||
private boolean exchangeRateCacheIsUsable() {
|
||||
return (exchangeRateCache != null) &&
|
||||
((exchangeRateCache.getBaseCurrency().equals(sCurrencyA.getSelectedItem()) &&
|
||||
exchangeRateCache.getQuoteCurrency().equals(sCurrencyB.getSelectedItem())) ||
|
||||
(exchangeRateCache.getBaseCurrency().equals(sCurrencyB.getSelectedItem()) &&
|
||||
exchangeRateCache.getQuoteCurrency().equals(sCurrencyA.getSelectedItem())));
|
||||
}
|
||||
|
||||
private double exchangeRateFromCache() {
|
||||
if (!exchangeRateCacheIsUsable()) return 0;
|
||||
if (exchangeRateCache.getBaseCurrency().equals(sCurrencyA.getSelectedItem())) {
|
||||
return exchangeRateCache.getRate();
|
||||
} else {
|
||||
clearAmounts();
|
||||
return 1.0d / exchangeRateCache.getRate();
|
||||
}
|
||||
}
|
||||
|
||||
private void clearAmounts() {
|
||||
Timber.d("clearAmounts");
|
||||
if ((xmrAmount != null) || (notXmrAmount != null)) {
|
||||
tvAmountB.setText(null);
|
||||
setXmr(null);
|
||||
notXmrAmount = null;
|
||||
public void doExchange() {
|
||||
if (!isInitialized) return;
|
||||
tvAmountB.setText(null);
|
||||
if (getCurrencyA() == getCurrencyB()) {
|
||||
exchange(1);
|
||||
return;
|
||||
}
|
||||
// use cached exchange rate if we have it
|
||||
if (!isExchangeInProgress()) {
|
||||
double rate = exchangeRateFromCache();
|
||||
if (rate > 0) {
|
||||
if (prepareExchange()) {
|
||||
exchange(rate);
|
||||
}
|
||||
} else {
|
||||
startExchange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final ExchangeApi exchangeApi = Helper.getExchangeApi();
|
||||
|
||||
// starts exchange through exchange api
|
||||
void startExchange() {
|
||||
showProgress();
|
||||
String currencyA = (String) sCurrencyA.getSelectedItem();
|
||||
String currencyB = (String) sCurrencyB.getSelectedItem();
|
||||
exchangeApi.queryExchangeRate(currencyA, currencyB,
|
||||
if ((currencyA == null) || (currencyB == null)) return; // nothing to do
|
||||
execExchange(currencyA, currencyB);
|
||||
}
|
||||
|
||||
void execExchange(String currencyA, String currencyB) {
|
||||
showProgress();
|
||||
queryExchangeRate(currencyA, currencyB,
|
||||
new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
|
@ -297,35 +329,30 @@ public class ExchangeEditText extends LinearLayout {
|
|||
});
|
||||
}
|
||||
|
||||
public void exchange(double rate) {
|
||||
Timber.d("%s / %s", xmrAmount, notXmrAmount);
|
||||
if (getCurrencyA() == 0) {
|
||||
if (xmrAmount == null) return;
|
||||
if (!xmrAmount.isEmpty() && (rate > 0)) {
|
||||
double amountB = rate * Double.parseDouble(xmrAmount);
|
||||
notXmrAmount = Helper.getFormattedAmount(amountB, getCurrencyB() == 0);
|
||||
void queryExchangeRate(final String base, final String quote, ExchangeCallback callback) {
|
||||
exchangeApi.queryExchangeRate(base, quote, callback);
|
||||
}
|
||||
|
||||
private void exchange(double rate) {
|
||||
double amount = getEnteredAmount();
|
||||
if (rate > 0) {
|
||||
tvAmountB.setText(Helper.getFormattedAmount(rate * amount));
|
||||
} else {
|
||||
tvAmountB.setText(null);
|
||||
Timber.w("No rate!");
|
||||
}
|
||||
}
|
||||
|
||||
private String getCleanAmountString(String enteredAmount) {
|
||||
try {
|
||||
double amount = Double.parseDouble(enteredAmount);
|
||||
if (amount >= 0) {
|
||||
return Helper.getFormattedAmount(amount);
|
||||
} else {
|
||||
notXmrAmount = "";
|
||||
return null;
|
||||
}
|
||||
tvAmountB.setText(notXmrAmount);
|
||||
Timber.d("%s / %s", xmrAmount, notXmrAmount);
|
||||
} else if (getCurrencyB() == 0) {
|
||||
if (notXmrAmount == null) return;
|
||||
if (!notXmrAmount.isEmpty() && (rate > 0)) {
|
||||
double amountB = rate * Double.parseDouble(notXmrAmount);
|
||||
setXmr(Helper.getFormattedAmount(amountB, true));
|
||||
} else {
|
||||
setXmr("");
|
||||
}
|
||||
tvAmountB.setText(xmrAmount);
|
||||
if (xmrAmount == null) {
|
||||
shakeAmountField();
|
||||
}
|
||||
} else { // no XMR currency - cannot happen!
|
||||
Timber.e("No XMR currency!");
|
||||
setXmr(null);
|
||||
notXmrAmount = null;
|
||||
return;
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -333,38 +360,14 @@ public class ExchangeEditText extends LinearLayout {
|
|||
Timber.d("prepareExchange()");
|
||||
String enteredAmount = etAmountA.getText().toString();
|
||||
if (!enteredAmount.isEmpty()) {
|
||||
String cleanAmount = "";
|
||||
if (getCurrencyA() == 0) {
|
||||
// sanitize the input
|
||||
long xmr = Wallet.getAmountFromString(enteredAmount);
|
||||
if (xmr >= 0) {
|
||||
cleanAmount = Helper.getDisplayAmount(xmr);
|
||||
} else {
|
||||
cleanAmount = null;
|
||||
}
|
||||
setXmr(cleanAmount);
|
||||
notXmrAmount = null;
|
||||
Timber.d("cleanAmount = %s", cleanAmount);
|
||||
if (cleanAmount == null) {
|
||||
shakeAmountField();
|
||||
return false;
|
||||
}
|
||||
} else if (getCurrencyB() == 0) { // we use B & 0 here for the else below ...
|
||||
// sanitize the input
|
||||
double amountA = Double.parseDouble(enteredAmount);
|
||||
cleanAmount = String.format(Locale.US, "%.2f", amountA);
|
||||
setXmr(null);
|
||||
notXmrAmount = cleanAmount;
|
||||
} else { // no XMR currency - cannot happen!
|
||||
Timber.e("No XMR currency!");
|
||||
setXmr(null);
|
||||
notXmrAmount = null;
|
||||
String cleanAmount = getCleanAmountString(enteredAmount);
|
||||
Timber.d("cleanAmount = %s", cleanAmount);
|
||||
if (cleanAmount == null) {
|
||||
shakeAmountField();
|
||||
return false;
|
||||
}
|
||||
Timber.d("prepareExchange() %s", cleanAmount);
|
||||
} else {
|
||||
setXmr("");
|
||||
notXmrAmount = "";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -372,33 +375,30 @@ public class ExchangeEditText extends LinearLayout {
|
|||
public void exchangeFailed() {
|
||||
hideProgress();
|
||||
exchange(0);
|
||||
if (onFailedExchangeListener != null) {
|
||||
onFailedExchangeListener.onFailedExchange();
|
||||
}
|
||||
}
|
||||
|
||||
String assetPair = null;
|
||||
double assetRate = 0;
|
||||
// cache for exchange rate
|
||||
ExchangeRate exchangeRateCache = null;
|
||||
|
||||
public void exchange(ExchangeRate exchangeRate) {
|
||||
hideProgress();
|
||||
// first, make sure this is what we want
|
||||
String enteredCurrencyA = (String) sCurrencyA.getSelectedItem();
|
||||
String enteredCurrencyB = (String) sCurrencyB.getSelectedItem();
|
||||
if (!exchangeRate.getBaseCurrency().equals(enteredCurrencyA)
|
||||
|| !exchangeRate.getQuoteCurrency().equals(enteredCurrencyB)) {
|
||||
// make sure this is what we want
|
||||
if (!exchangeRate.getBaseCurrency().equals(sCurrencyA.getSelectedItem()) ||
|
||||
!exchangeRate.getQuoteCurrency().equals(sCurrencyB.getSelectedItem())) {
|
||||
// something's wrong
|
||||
Timber.e("Currencies don't match!");
|
||||
Timber.i("Currencies don't match! A: %s==%s B: %s==%s",
|
||||
exchangeRate.getBaseCurrency(), sCurrencyA.getSelectedItem(),
|
||||
exchangeRate.getQuoteCurrency(), sCurrencyB.getSelectedItem());
|
||||
return;
|
||||
}
|
||||
assetPair = enteredCurrencyA + enteredCurrencyB;
|
||||
assetRate = exchangeRate.getRate();
|
||||
|
||||
exchangeRateCache = exchangeRate;
|
||||
if (prepareExchange()) {
|
||||
exchange(exchangeRate.getRate());
|
||||
}
|
||||
}
|
||||
|
||||
private void showProgress() {
|
||||
void showProgress() {
|
||||
pbExchange.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
|
@ -409,35 +409,4 @@ public class ExchangeEditText extends LinearLayout {
|
|||
private void hideProgress() {
|
||||
pbExchange.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
// Hooks
|
||||
public interface OnNewAmountListener {
|
||||
void onNewAmount(String xmr);
|
||||
}
|
||||
|
||||
OnNewAmountListener onNewAmountListener;
|
||||
|
||||
public void setOnNewAmountListener(OnNewAmountListener listener) {
|
||||
onNewAmountListener = listener;
|
||||
}
|
||||
|
||||
public interface OnAmountInvalidatedListener {
|
||||
void onAmountInvalidated();
|
||||
}
|
||||
|
||||
OnAmountInvalidatedListener onAmountInvalidatedListener;
|
||||
|
||||
public void setOnAmountInvalidatedListener(OnAmountInvalidatedListener listener) {
|
||||
onAmountInvalidatedListener = listener;
|
||||
}
|
||||
|
||||
public interface OnFailedExchangeListener {
|
||||
void onFailedExchange();
|
||||
}
|
||||
|
||||
OnFailedExchangeListener onFailedExchangeListener;
|
||||
|
||||
public void setOnFailedExchangeListener(OnFailedExchangeListener listener) {
|
||||
onFailedExchangeListener = listener;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, exchangeRate);
|
||||
return;
|
||||
}
|
||||
if (currencyA.equals(baseCurrency) && (currencyB.equals(Helper.BASE_CRYPTO))) {
|
||||
localExchange(currencyA, currencyB, 1.0d / 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -53,11 +54,12 @@
|
|||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<com.m2049r.xmrwallet.widget.ExchangeBtcEditText
|
||||
<com.m2049r.xmrwallet.widget.ExchangeOtherEditText
|
||||
android:id="@+id/etAmount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="vertical" />
|
||||
android:layout_marginBottom="16dp"
|
||||
android:orientation="vertical"
|
||||
app:baseSymbol="BTC" />
|
||||
</LinearLayout>
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
android:layout_width="56dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:entries="@array/currency"
|
||||
android:gravity="center"
|
||||
android:textAlignment="center" />
|
||||
|
||||
|
@ -52,7 +51,6 @@
|
|||
android:layout_width="56sp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:entries="@array/currency"
|
||||
android:gravity="center"
|
||||
android:textAlignment="center" />
|
||||
|
||||
|
|
|
@ -292,43 +292,6 @@
|
|||
<string name="archive_alert_yes">はい、お願いします!</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_restore_viewonly">閲覧専用ウォレットを復元</string>
|
||||
<string name="fab_restore_key">秘密鍵からウォレットを復元</string>
|
||||
|
|
|
@ -7,4 +7,7 @@
|
|||
<attr name="activeDot" format="integer" />
|
||||
<attr name="numberDots" format="integer" />
|
||||
</declare-styleable>
|
||||
<declare-styleable name="ExchangeEditText">
|
||||
<attr name="baseSymbol" format="string" />
|
||||
</declare-styleable>
|
||||
</resources>
|
|
@ -297,40 +297,40 @@
|
|||
<string name="archive_alert_no">No thanks!</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>AUD</item>
|
||||
<item>BGN</item>
|
||||
<item>BRL</item>
|
||||
<item>CLP</item>
|
||||
<item>CNY</item>
|
||||
<item>CZK</item>
|
||||
<item>DKK</item>
|
||||
<item>HKD</item>
|
||||
<item>HRK</item>
|
||||
<item>HUF</item>
|
||||
<item>IDR</item>
|
||||
<item>ILS</item>
|
||||
<item>INR</item>
|
||||
<item>ISK</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>RON</item>
|
||||
<item>RUB</item>
|
||||
<item>SEK</item>
|
||||
<item>SGD</item>
|
||||
<item>THB</item>
|
||||
<item>TRY</item>
|
||||
<item>TWD</item>
|
||||
<item>ZAR</item>
|
||||
</string-array>
|
||||
|
||||
<string name="fab_create_new">Create new wallet</string>
|
||||
|
|
|
@ -0,0 +1,301 @@
|
|||
/*
|
||||
* Copyright (c) 2017 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 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 net.jodah.concurrentunit.Waiter;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
|
||||
public class ExchangeRateTest {
|
||||
|
||||
private MockWebServer mockWebServer;
|
||||
|
||||
private ExchangeApi exchangeApi;
|
||||
|
||||
private final OkHttpClient okHttpClient = new OkHttpClient();
|
||||
private Waiter waiter;
|
||||
|
||||
@Mock
|
||||
ExchangeCallback mockExchangeCallback;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
mockWebServer = new MockWebServer();
|
||||
mockWebServer.start();
|
||||
|
||||
waiter = new Waiter();
|
||||
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
exchangeApi = new ExchangeApiImpl(okHttpClient, mockWebServer.url("/"));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
mockWebServer.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_shouldBeGetMethod()
|
||||
throws InterruptedException, TimeoutException {
|
||||
|
||||
exchangeApi.queryExchangeRate("EUR", "USD", mockExchangeCallback);
|
||||
|
||||
RecordedRequest request = mockWebServer.takeRequest();
|
||||
assertEquals("GET", request.getMethod());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_shouldBeEUR()
|
||||
throws InterruptedException, TimeoutException {
|
||||
|
||||
exchangeApi.queryExchangeRate("CHF", "USD", new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.assertTrue(e instanceof IllegalArgumentException);
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_shouldBeOneForEur()
|
||||
throws InterruptedException, TimeoutException {
|
||||
|
||||
exchangeApi.queryExchangeRate("EUR", "EUR", new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.assertEquals(1.0, exchangeRate.getRate());
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_wasSuccessfulShouldRespondWithUsdRate()
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
final String base = "EUR";
|
||||
final String quote = "USD";
|
||||
final double rate = 1.1043;
|
||||
|
||||
MockResponse jsonMockResponse = new MockResponse().setBody(createMockExchangeRateResponse());
|
||||
mockWebServer.enqueue(jsonMockResponse);
|
||||
|
||||
exchangeApi.queryExchangeRate(base, quote, new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.assertEquals(base, exchangeRate.getBaseCurrency());
|
||||
waiter.assertEquals(quote, exchangeRate.getQuoteCurrency());
|
||||
waiter.assertEquals(rate, exchangeRate.getRate());
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.fail(e);
|
||||
waiter.resume();
|
||||
}
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_wasSuccessfulShouldRespondWithAudRate()
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
final String base = "EUR";
|
||||
final String quote = "AUD";
|
||||
final double rate = 1.6246;
|
||||
|
||||
MockResponse jsonMockResponse = new MockResponse().setBody(createMockExchangeRateResponse());
|
||||
mockWebServer.enqueue(jsonMockResponse);
|
||||
|
||||
exchangeApi.queryExchangeRate(base, quote, new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.assertEquals(base, exchangeRate.getBaseCurrency());
|
||||
waiter.assertEquals(quote, exchangeRate.getQuoteCurrency());
|
||||
waiter.assertEquals(rate, exchangeRate.getRate());
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.fail(e);
|
||||
waiter.resume();
|
||||
}
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_wasSuccessfulShouldRespondWithZarRate()
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
final String base = "EUR";
|
||||
final String quote = "ZAR";
|
||||
final double rate = 16.3978;
|
||||
|
||||
MockResponse jsonMockResponse = new MockResponse().setBody(createMockExchangeRateResponse());
|
||||
mockWebServer.enqueue(jsonMockResponse);
|
||||
|
||||
exchangeApi.queryExchangeRate(base, quote, new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.assertEquals(base, exchangeRate.getBaseCurrency());
|
||||
waiter.assertEquals(quote, exchangeRate.getQuoteCurrency());
|
||||
waiter.assertEquals(rate, exchangeRate.getRate());
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.fail(e);
|
||||
waiter.resume();
|
||||
}
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_wasNotSuccessfulShouldCallOnError()
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
mockWebServer.enqueue(new MockResponse().setResponseCode(500));
|
||||
exchangeApi.queryExchangeRate("EUR", "USD", new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.assertTrue(e instanceof ExchangeException);
|
||||
waiter.assertTrue(((ExchangeException) e).getCode() == 500);
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_unknownAssetShouldCallOnError()
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
MockResponse jsonMockResponse = new MockResponse().setBody(createMockExchangeRateResponse());
|
||||
mockWebServer.enqueue(jsonMockResponse);
|
||||
exchangeApi.queryExchangeRate("EUR", "ABC", new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.assertTrue(e instanceof ExchangeException);
|
||||
ExchangeException ex = (ExchangeException) e;
|
||||
waiter.assertTrue(ex.getCode() == 404);
|
||||
waiter.assertEquals(ex.getErrorMsg(), "Currency not supported: ABC");
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
static public String createMockExchangeRateResponse() {
|
||||
return "<gesmes:Envelope xmlns:gesmes=\"http://www.gesmes.org/xml/2002-08-01\" xmlns=\"http://www.ecb.int/vocabulary/2002-08-01/eurofxref\"><script xmlns=\"\"/>\n" +
|
||||
"\t<gesmes:subject>Reference rates</gesmes:subject>\n" +
|
||||
"\t<gesmes:Sender>\n" +
|
||||
"\t\t<gesmes:name>European Central Bank</gesmes:name>\n" +
|
||||
"\t</gesmes:Sender>\n" +
|
||||
"\t<Cube>\n" +
|
||||
"\t\t<Cube time=\"2019-10-11\">\n" +
|
||||
"\t\t\t<Cube currency=\"USD\" rate=\"1.1043\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"JPY\" rate=\"119.75\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"BGN\" rate=\"1.9558\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"CZK\" rate=\"25.807\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"DKK\" rate=\"7.4688\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"GBP\" rate=\"0.87518\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"HUF\" rate=\"331.71\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"PLN\" rate=\"4.3057\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"RON\" rate=\"4.7573\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"SEK\" rate=\"10.8448\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"CHF\" rate=\"1.1025\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"ISK\" rate=\"137.70\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"NOK\" rate=\"10.0375\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"HRK\" rate=\"7.4280\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"RUB\" rate=\"70.8034\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"TRY\" rate=\"6.4713\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"AUD\" rate=\"1.6246\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"BRL\" rate=\"4.5291\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"CAD\" rate=\"1.4679\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"CNY\" rate=\"7.8417\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"HKD\" rate=\"8.6614\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"IDR\" rate=\"15601.55\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"ILS\" rate=\"3.8673\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"INR\" rate=\"78.4875\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"KRW\" rate=\"1308.61\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"MXN\" rate=\"21.3965\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"MYR\" rate=\"4.6220\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"NZD\" rate=\"1.7419\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"PHP\" rate=\"56.927\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"SGD\" rate=\"1.5177\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"THB\" rate=\"33.642\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"ZAR\" rate=\"16.3978\"/>\n" +
|
||||
"\t\t</Cube>\n" +
|
||||
"\t</Cube>\n" +
|
||||
"</gesmes:Envelope>";
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -14,7 +14,7 @@
|
|||
* 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.ExchangeCallback;
|
||||
|
@ -71,9 +71,9 @@ public class ExchangeRateTest {
|
|||
|
||||
@Test
|
||||
public void queryExchangeRate_shouldBeGetMethod()
|
||||
throws InterruptedException {
|
||||
throws InterruptedException, TimeoutException {
|
||||
|
||||
exchangeApi.queryExchangeRate("XMR", "EUR", mockExchangeCallback);
|
||||
exchangeApi.queryExchangeRate("XMR", "USD", mockExchangeCallback);
|
||||
|
||||
RecordedRequest request = mockWebServer.takeRequest();
|
||||
assertEquals("GET", request.getMethod());
|
||||
|
@ -81,48 +81,20 @@ public class ExchangeRateTest {
|
|||
|
||||
@Test
|
||||
public void queryExchangeRate_shouldHavePairInUrl()
|
||||
throws InterruptedException {
|
||||
throws InterruptedException, TimeoutException {
|
||||
|
||||
exchangeApi.queryExchangeRate("XMR", "EUR", mockExchangeCallback);
|
||||
exchangeApi.queryExchangeRate("XMR", "USD", mockExchangeCallback);
|
||||
|
||||
RecordedRequest request = mockWebServer.takeRequest();
|
||||
assertEquals("/328/?convert=EUR", request.getPath());
|
||||
assertEquals("/?pair=XMRUSD", request.getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_wasSuccessfulShouldRespondWithRate()
|
||||
throws 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 {
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
final String base = "XMR";
|
||||
final String quote = "USD";
|
||||
final double rate = 1.56;
|
||||
final double rate = 100;
|
||||
MockResponse jsonMockResponse = new MockResponse().setBody(
|
||||
createMockExchangeRateResponse(base, quote, rate));
|
||||
mockWebServer.enqueue(jsonMockResponse);
|
||||
|
@ -147,9 +119,8 @@ public class ExchangeRateTest {
|
|||
|
||||
@Test
|
||||
public void queryExchangeRate_wasNotSuccessfulShouldCallOnError()
|
||||
throws TimeoutException {
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
mockWebServer.enqueue(new MockResponse().setResponseCode(500));
|
||||
|
||||
exchangeApi.queryExchangeRate("XMR", "USD", new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
|
@ -170,11 +141,10 @@ public class ExchangeRateTest {
|
|||
|
||||
@Test
|
||||
public void queryExchangeRate_unknownAssetShouldCallOnError()
|
||||
throws TimeoutException {
|
||||
MockResponse jsonMockResponse = new MockResponse().setBody(
|
||||
createMockExchangeRateErrorResponse());
|
||||
mockWebServer.enqueue(jsonMockResponse);
|
||||
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
mockWebServer.enqueue(new MockResponse().
|
||||
setResponseCode(200).
|
||||
setBody("{\"error\":[\"EQuery:Unknown asset pair\"]}"));
|
||||
exchangeApi.queryExchangeRate("XMR", "ABC", new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
|
@ -187,7 +157,7 @@ public class ExchangeRateTest {
|
|||
waiter.assertTrue(e instanceof ExchangeException);
|
||||
ExchangeException ex = (ExchangeException) e;
|
||||
waiter.assertTrue(ex.getCode() == 200);
|
||||
waiter.assertEquals(ex.getErrorMsg(), "id not found");
|
||||
waiter.assertEquals(ex.getErrorMsg(), "EQuery:Unknown asset pair");
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
|
@ -195,52 +165,22 @@ public class ExchangeRateTest {
|
|||
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" +
|
||||
" \"data\": {\n" +
|
||||
" \"id\": 328, \n" +
|
||||
" \"name\": \"Monero\", \n" +
|
||||
" \"symbol\": \"" + base + "\", \n" +
|
||||
" \"website_slug\": \"monero\", \n" +
|
||||
" \"rank\": 12, \n" +
|
||||
" \"circulating_supply\": 16112286.0, \n" +
|
||||
" \"total_supply\": 16112286.0, \n" +
|
||||
" \"max_supply\": null, \n" +
|
||||
" \"quotes\": {\n" +
|
||||
" \"USD\": {\n" +
|
||||
" \"price\": " + rate + ", \n" +
|
||||
" \"volume_24h\": 35763700.0, \n" +
|
||||
" \"market_cap\": 2559791130.0, \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" +
|
||||
" \"error\":[],\n" +
|
||||
" \"result\":{\n" +
|
||||
" \"X" + base + "Z" + quote + "\":{\n" +
|
||||
" \"a\":[\"" + rate + "\",\"322\",\"322.000\"],\n" +
|
||||
" \"b\":[\"" + rate + "\",\"76\",\"76.000\"],\n" +
|
||||
" \"c\":[\"" + rate + "\",\"2.90000000\"],\n" +
|
||||
" \"v\":[\"4559.03962053\",\"5231.33235586\"],\n" +
|
||||
" \"p\":[\"" + rate + "\",\"" + rate + "\"],\n" +
|
||||
" \"t\":[801,1014],\n" +
|
||||
" \"l\":[\"" + (rate * 0.8) + "\",\"" + rate + "\"],\n" +
|
||||
" \"h\":[\"" + (rate * 1.2) + "\",\"" + rate + "\"],\n" +
|
||||
" \"o\":\"" + rate + "\"\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
"}";
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue