mirror of
https://github.com/m2049r/xmrwallet
synced 2025-09-04 00:53:36 +02:00
Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e076c19e3e | ||
![]() |
35b717756d | ||
![]() |
c14486306e | ||
![]() |
c2ef25c377 | ||
![]() |
b7164ef200 | ||
![]() |
f94a366d51 | ||
![]() |
286a04b5ef | ||
![]() |
1209295a8c | ||
![]() |
037b019d4d | ||
![]() |
7a1d788f2a | ||
![]() |
87d9a8cd95 | ||
![]() |
f637d7f617 | ||
![]() |
a4b9a7c6fb | ||
![]() |
9f01155cb7 | ||
![]() |
08e8a48138 | ||
![]() |
551c3b9fb6 | ||
![]() |
2258cb7096 | ||
![]() |
6d61841cf3 | ||
![]() |
c65508d288 | ||
![]() |
2c3f582672 | ||
![]() |
f46ba75771 | ||
![]() |
0ce5f2b6ca | ||
![]() |
110057c294 | ||
![]() |
7553d3c5f4 | ||
![]() |
317976b34a | ||
![]() |
6ad423567f | ||
![]() |
d497158856 | ||
![]() |
f4cada5fa1 | ||
![]() |
352f0ad09c | ||
![]() |
ff1a9c1570 | ||
![]() |
fa811a39a2 | ||
![]() |
cf5018be33 | ||
![]() |
8ec027f9d4 | ||
![]() |
f28428e677 | ||
![]() |
294084bec5 | ||
![]() |
4349907627 | ||
![]() |
f7cef24a83 |
@@ -155,6 +155,18 @@ add_library(net STATIC IMPORTED)
|
||||
set_target_properties(net PROPERTIES IMPORTED_LOCATION
|
||||
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libnet.a)
|
||||
|
||||
add_library(hardforks STATIC IMPORTED)
|
||||
set_target_properties(hardforks PROPERTIES IMPORTED_LOCATION
|
||||
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libhardforks.a)
|
||||
|
||||
add_library(randomx STATIC IMPORTED)
|
||||
set_target_properties(randomx PROPERTIES IMPORTED_LOCATION
|
||||
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/librandomx.a)
|
||||
|
||||
add_library(rpc_base STATIC IMPORTED)
|
||||
set_target_properties(rpc_base PROPERTIES IMPORTED_LOCATION
|
||||
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/librpc_base.a)
|
||||
|
||||
#############
|
||||
# System
|
||||
#############
|
||||
@@ -188,6 +200,9 @@ target_link_libraries( monerujo
|
||||
device_trezor
|
||||
multisig
|
||||
version
|
||||
randomx
|
||||
hardforks
|
||||
rpc_base
|
||||
|
||||
boost_chrono
|
||||
boost_date_time
|
||||
|
@@ -7,8 +7,8 @@ android {
|
||||
applicationId "com.m2049r.xmrwallet"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 28
|
||||
versionCode 180
|
||||
versionName "1.11.10 'Chernushka'"
|
||||
versionCode 199
|
||||
versionName "1.12.9 'Caerbannog'"
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
externalNativeBuild {
|
||||
|
@@ -1310,7 +1310,6 @@ Java_com_m2049r_xmrwallet_model_PendingTransaction_getFirstTxIdJ(JNIEnv *env, jo
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_com_m2049r_xmrwallet_model_PendingTransaction_getTxCount(JNIEnv *env, jobject instance) {
|
||||
Bitmonero::PendingTransaction *tx = getHandle<Bitmonero::PendingTransaction>(env, instance);
|
||||
@@ -1396,6 +1395,11 @@ Java_com_m2049r_xmrwallet_model_WalletManager_setLogLevel(JNIEnv *env, jclass cl
|
||||
Bitmonero::WalletManagerFactory::setLogLevel(level);
|
||||
}
|
||||
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_com_m2049r_xmrwallet_model_WalletManager_moneroVersion(JNIEnv *env, jclass clazz) {
|
||||
return env->NewStringUTF(MONERO_VERSION);
|
||||
}
|
||||
|
||||
//
|
||||
// Ledger Stuff
|
||||
//
|
||||
|
@@ -54,6 +54,8 @@ extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
extern const char* const MONERO_VERSION; // the actual monero core version
|
||||
|
||||
// from monero-core crypto/hash-ops.h - avoid #including monero code here
|
||||
enum {
|
||||
HASH_SIZE = 32,
|
||||
|
@@ -46,8 +46,12 @@ public class BTChipTransportAndroidHID implements BTChipTransport {
|
||||
HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
|
||||
for (UsbDevice device : deviceList.values()) {
|
||||
Timber.d("%04X:%04X %s, %s", device.getVendorId(), device.getProductId(), device.getManufacturerName(), device.getProductName());
|
||||
if ((device.getVendorId() == VID) && (device.getProductId() == PID_HID)) {
|
||||
return device;
|
||||
if (device.getVendorId() == VID) {
|
||||
final int deviceProductId = device.getProductId();
|
||||
for (int pid : PID_HIDS) {
|
||||
if (deviceProductId == pid)
|
||||
return device;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -74,7 +78,7 @@ public class BTChipTransportAndroidHID implements BTChipTransport {
|
||||
}
|
||||
|
||||
private static final int VID = 0x2C97;
|
||||
private static final int PID_HID = 0x0001;
|
||||
private static final int[] PID_HIDS = {0x0001, 0x0004};
|
||||
|
||||
private UsbDeviceConnection connection;
|
||||
private UsbInterface dongleInterface;
|
||||
|
@@ -79,6 +79,23 @@ public class GenerateFragment extends Fragment {
|
||||
|
||||
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
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
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() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
@@ -118,6 +152,8 @@ public class GenerateFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
});
|
||||
clearErrorOnTextEntry(etWalletMnemonic);
|
||||
|
||||
etWalletAddress.getEditText().setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
@@ -126,6 +162,8 @@ public class GenerateFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
});
|
||||
clearErrorOnTextEntry(etWalletAddress);
|
||||
|
||||
etWalletViewKey.getEditText().setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
@@ -134,6 +172,8 @@ public class GenerateFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
});
|
||||
clearErrorOnTextEntry(etWalletViewKey);
|
||||
|
||||
etWalletSpendKey.getEditText().setOnFocusChangeListener(new View.OnFocusChangeListener() {
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {
|
||||
@@ -142,6 +182,7 @@ public class GenerateFragment extends Fragment {
|
||||
}
|
||||
}
|
||||
});
|
||||
clearErrorOnTextEntry(etWalletSpendKey);
|
||||
|
||||
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();
|
||||
initZxcvbn();
|
||||
|
||||
|
@@ -55,6 +55,8 @@ import com.m2049r.xmrwallet.util.KeyStoreHelper;
|
||||
import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor;
|
||||
import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class GenerateReviewFragment extends Fragment {
|
||||
@@ -72,6 +74,7 @@ public class GenerateReviewFragment extends Fragment {
|
||||
private TextView tvWalletPassword;
|
||||
private TextView tvWalletAddress;
|
||||
private TextView tvWalletMnemonic;
|
||||
private TextView tvWalletHeight;
|
||||
private TextView tvWalletViewKey;
|
||||
private TextView tvWalletSpendKey;
|
||||
private ImageButton bCopyAddress;
|
||||
@@ -99,6 +102,7 @@ public class GenerateReviewFragment extends Fragment {
|
||||
tvWalletViewKey = view.findViewById(R.id.tvWalletViewKey);
|
||||
tvWalletSpendKey = view.findViewById(R.id.tvWalletSpendKey);
|
||||
tvWalletMnemonic = view.findViewById(R.id.tvWalletMnemonic);
|
||||
tvWalletHeight = view.findViewById(R.id.tvWalletHeight);
|
||||
bCopyAddress = view.findViewById(R.id.bCopyAddress);
|
||||
bAdvancedInfo = view.findViewById(R.id.bAdvancedInfo);
|
||||
llAdvancedInfo = view.findViewById(R.id.llAdvancedInfo);
|
||||
@@ -188,6 +192,7 @@ public class GenerateReviewFragment extends Fragment {
|
||||
private class AsyncShow extends AsyncTask<String, Void, Boolean> {
|
||||
String name;
|
||||
String address;
|
||||
long height;
|
||||
String seed;
|
||||
String viewKey;
|
||||
String spendKey;
|
||||
@@ -232,6 +237,7 @@ public class GenerateReviewFragment extends Fragment {
|
||||
}
|
||||
|
||||
address = wallet.getAddress();
|
||||
height = wallet.getRestoreHeight();
|
||||
seed = wallet.getSeed();
|
||||
switch (wallet.getDeviceType()) {
|
||||
case Device_Ledger:
|
||||
@@ -264,6 +270,7 @@ public class GenerateReviewFragment extends Fragment {
|
||||
llPassword.setVisibility(View.VISIBLE);
|
||||
tvWalletPassword.setText(getPassword());
|
||||
tvWalletAddress.setText(address);
|
||||
tvWalletHeight.setText(NumberFormat.getInstance().format(height));
|
||||
if (!seed.isEmpty()) {
|
||||
llMnemonic.setVisibility(View.VISIBLE);
|
||||
tvWalletMnemonic.setText(seed);
|
||||
@@ -288,6 +295,7 @@ public class GenerateReviewFragment extends Fragment {
|
||||
} else {
|
||||
// TODO show proper error message and/or end the fragment?
|
||||
tvWalletAddress.setText(walletStatus.toString());
|
||||
tvWalletHeight.setText(walletStatus.toString());
|
||||
tvWalletMnemonic.setText(walletStatus.toString());
|
||||
tvWalletViewKey.setText(walletStatus.toString());
|
||||
tvWalletSpendKey.setText(walletStatus.toString());
|
||||
|
@@ -940,8 +940,9 @@ public class LoginActivity extends BaseActivity
|
||||
@Override
|
||||
public boolean createWallet(File aFile, String password) {
|
||||
NodeInfo currentNode = getNode();
|
||||
// get it from the connected node if we have one, and go back ca. 4 days
|
||||
final long restoreHeight =
|
||||
(currentNode != null) ? currentNode.getHeight() - 20 : -1;
|
||||
(currentNode != null) ? currentNode.getHeight() - 2000 : -1;
|
||||
Wallet newWallet = WalletManager.getInstance()
|
||||
.createWallet(aFile, password, MNEMONIC_LANGUAGE, restoreHeight);
|
||||
return checkAndCloseWallet(newWallet);
|
||||
@@ -1359,17 +1360,30 @@ public class LoginActivity extends BaseActivity
|
||||
if (Ledger.ENABLED)
|
||||
try {
|
||||
Ledger.connect(usbManager, usbDevice);
|
||||
registerDetachReceiver();
|
||||
onLedgerAction();
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(LoginActivity.this,
|
||||
getString(R.string.toast_ledger_attached, usbDevice.getProductName()),
|
||||
Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
if (!Ledger.check()) {
|
||||
Ledger.disconnect();
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(LoginActivity.this,
|
||||
getString(R.string.toast_ledger_start_app, usbDevice.getProductName()),
|
||||
Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
registerDetachReceiver();
|
||||
onLedgerAction();
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(LoginActivity.this,
|
||||
getString(R.string.toast_ledger_attached, usbDevice.getProductName()),
|
||||
Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
|
@@ -439,10 +439,13 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
||||
}
|
||||
Collections.sort(nodesToTest, NodeInfo.BestNodeComparator);
|
||||
NodeInfo bestNode = nodesToTest.get(0);
|
||||
if (bestNode.isValid())
|
||||
if (bestNode.isValid()) {
|
||||
activityCallback.setNode(bestNode);
|
||||
return bestNode;
|
||||
else
|
||||
} else {
|
||||
activityCallback.setNode(null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -450,7 +453,6 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
|
||||
if (!isAdded()) return;
|
||||
pbNode.setVisibility(View.INVISIBLE);
|
||||
llNode.setVisibility(View.VISIBLE);
|
||||
activityCallback.setNode(result);
|
||||
if (result != null) {
|
||||
Timber.d("found a good node %s", result.toString());
|
||||
showNode(result);
|
||||
|
@@ -266,6 +266,7 @@ public class NodeFragment extends Fragment
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("198.74.231.92", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("195.154.123.123", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("212.83.172.165", 18080)));
|
||||
seedList.add(new NodeInfo(new InetSocketAddress("192.110.160.146", 18080)));
|
||||
d.seedPeers(seedList);
|
||||
d.awaitTermination(NODES_TO_FIND);
|
||||
}
|
||||
|
@@ -50,9 +50,8 @@ import com.m2049r.xmrwallet.widget.Toolbar;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
@@ -114,9 +113,12 @@ public class WalletFragment extends Fragment
|
||||
ivSynced = view.findViewById(R.id.ivSynced);
|
||||
|
||||
sCurrency = view.findViewById(R.id.sCurrency);
|
||||
ArrayAdapter currencyAdapter = ArrayAdapter.createFromResource(getContext(), R.array.currency, R.layout.item_spinner_balance);
|
||||
currencyAdapter.setDropDownViewResource(R.layout.item_spinner_dropdown_item);
|
||||
sCurrency.setAdapter(currencyAdapter);
|
||||
List<String> currencies = new ArrayList<>();
|
||||
currencies.add(Helper.BASE_CRYPTO);
|
||||
currencies.addAll(Arrays.asList(getResources().getStringArray(R.array.currency)));
|
||||
ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(getContext(), R.layout.item_spinner_balance, currencies);
|
||||
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
sCurrency.setAdapter(spinnerAdapter);
|
||||
|
||||
bSend = view.findViewById(R.id.bSend);
|
||||
bReceive = view.findViewById(R.id.bReceive);
|
||||
@@ -220,7 +222,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 +231,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 +247,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 +303,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());
|
||||
|
@@ -235,8 +235,10 @@ public class NodeInfo extends Node {
|
||||
String rpcVersion = json.getString("jsonrpc");
|
||||
if (!RPC_VERSION.equals(rpcVersion))
|
||||
return false;
|
||||
final JSONObject header = json.getJSONObject(
|
||||
"result").getJSONObject("block_header");
|
||||
final JSONObject result = json.getJSONObject("result");
|
||||
if (!result.has("credits")) // introduced in monero v0.15.0
|
||||
return false;
|
||||
final JSONObject header = result.getJSONObject("block_header");
|
||||
height = header.getLong("height");
|
||||
timestamp = header.getLong("timestamp");
|
||||
majorVersion = header.getInt("major_version");
|
||||
|
@@ -140,28 +140,12 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||
next = null;
|
||||
} else {
|
||||
// maybe a bip72 or 70 URI
|
||||
String bip70 = PaymentProtocolHelper.getBip70(enteredAddress);
|
||||
final String bip70 = PaymentProtocolHelper.getBip70(enteredAddress);
|
||||
if (bip70 != null) {
|
||||
// looks good - resolve through xmr.to
|
||||
processBip70(bip70);
|
||||
next = null;
|
||||
} else if (checkAddress()) {
|
||||
if (llPaymentId.getVisibility() == View.VISIBLE) {
|
||||
next = etPaymentId;
|
||||
} else {
|
||||
next = etNotes;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (next != null) {
|
||||
final View focus = next;
|
||||
etAddress.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
focus.requestFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -176,6 +160,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||
Timber.d("isIntegratedAddress");
|
||||
etPaymentId.getEditText().getText().clear();
|
||||
llPaymentId.setVisibility(View.INVISIBLE);
|
||||
etAddress.setError(getString(R.string.info_paymentid_integrated));
|
||||
tvPaymentIdIntegrated.setVisibility(View.VISIBLE);
|
||||
llXmrTo.setVisibility(View.INVISIBLE);
|
||||
sendListener.setMode(SendFragment.Mode.XMR);
|
||||
@@ -208,10 +193,21 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||
if (clip == null) return;
|
||||
// clean it up
|
||||
final String address = clip.replaceAll("[^0-9A-Z-a-z]", "");
|
||||
if (Wallet.isAddressValid(address) || BitcoinAddressValidator.validate(address))
|
||||
etAddress.getEditText().setText(address);
|
||||
else
|
||||
Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show();
|
||||
if (Wallet.isAddressValid(address) || BitcoinAddressValidator.validate(address)) {
|
||||
final EditText et = etAddress.getEditText();
|
||||
et.setText(address);
|
||||
et.setSelection(et.getText().length());
|
||||
etAddress.requestFocus();
|
||||
} else {
|
||||
final String bip70 = PaymentProtocolHelper.getBip70(clip);
|
||||
if (bip70 != null) {
|
||||
final EditText et = etAddress.getEditText();
|
||||
et.setText(clip);
|
||||
et.setSelection(et.getText().length());
|
||||
processBip70(bip70);
|
||||
} else
|
||||
Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -248,7 +244,10 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||
bPaymentId.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
etPaymentId.getEditText().setText((Wallet.generatePaymentId()));
|
||||
final EditText et = etPaymentId.getEditText();
|
||||
et.setText((Wallet.generatePaymentId()));
|
||||
et.setSelection(et.getText().length());
|
||||
etPaymentId.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -259,7 +258,6 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|
||||
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
|
||||
etDummy.requestFocus();
|
||||
Helper.hideKeyboard(getActivity());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -277,7 +275,6 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||
etDummy = view.findViewById(R.id.etDummy);
|
||||
etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
etDummy.requestFocus();
|
||||
Helper.hideKeyboard(getActivity());
|
||||
|
||||
View tvNfc = view.findViewById(R.id.tvNfc);
|
||||
NfcManager manager = (NfcManager) getContext().getSystemService(Context.NFC_SERVICE);
|
||||
@@ -551,7 +548,6 @@ public class SendAddressWizardFragment extends SendWizardFragment {
|
||||
public void onResumeFragment() {
|
||||
super.onResumeFragment();
|
||||
Timber.d("onResumeFragment()");
|
||||
Helper.hideKeyboard(getActivity());
|
||||
etDummy.requestFocus();
|
||||
}
|
||||
|
||||
|
@@ -21,8 +21,6 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
@@ -30,8 +28,7 @@ import com.m2049r.xmrwallet.data.BarcodeData;
|
||||
import com.m2049r.xmrwallet.data.TxData;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.widget.ExchangeTextView;
|
||||
import com.m2049r.xmrwallet.widget.NumberPadView;
|
||||
import com.m2049r.xmrwallet.widget.ExchangeEditText;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
@@ -59,8 +56,7 @@ public class SendAmountWizardFragment extends SendWizardFragment {
|
||||
}
|
||||
|
||||
private TextView tvFunds;
|
||||
private ExchangeTextView evAmount;
|
||||
private View llAmount;
|
||||
private ExchangeEditText etAmount;
|
||||
private View rlSweep;
|
||||
private ImageButton ibSweep;
|
||||
|
||||
@@ -75,12 +71,9 @@ public class SendAmountWizardFragment extends SendWizardFragment {
|
||||
View view = inflater.inflate(R.layout.fragment_send_amount, container, false);
|
||||
|
||||
tvFunds = view.findViewById(R.id.tvFunds);
|
||||
|
||||
evAmount = view.findViewById(R.id.evAmount);
|
||||
((NumberPadView) view.findViewById(R.id.numberPad)).setListener(evAmount);
|
||||
|
||||
etAmount = view.findViewById(R.id.etAmount);
|
||||
rlSweep = view.findViewById(R.id.rlSweep);
|
||||
llAmount = view.findViewById(R.id.llAmount);
|
||||
|
||||
view.findViewById(R.id.ivSweep).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
@@ -97,8 +90,7 @@ public class SendAmountWizardFragment extends SendWizardFragment {
|
||||
}
|
||||
});
|
||||
|
||||
Helper.hideKeyboard(getActivity());
|
||||
|
||||
etAmount.requestFocus();
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -107,11 +99,11 @@ public class SendAmountWizardFragment extends SendWizardFragment {
|
||||
private void sweepAll(boolean spendAllMode) {
|
||||
if (spendAllMode) {
|
||||
ibSweep.setVisibility(View.INVISIBLE);
|
||||
llAmount.setVisibility(View.GONE);
|
||||
etAmount.setVisibility(View.GONE);
|
||||
rlSweep.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
ibSweep.setVisibility(View.VISIBLE);
|
||||
llAmount.setVisibility(View.VISIBLE);
|
||||
etAmount.setVisibility(View.VISIBLE);
|
||||
rlSweep.setVisibility(View.GONE);
|
||||
}
|
||||
this.spendAllMode = spendAllMode;
|
||||
@@ -124,12 +116,12 @@ public class SendAmountWizardFragment extends SendWizardFragment {
|
||||
sendListener.getTxData().setAmount(Wallet.SWEEP_ALL);
|
||||
}
|
||||
} else {
|
||||
if (!evAmount.validate(maxFunds)) {
|
||||
if (!etAmount.validate(maxFunds, 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sendListener != null) {
|
||||
String xmr = evAmount.getAmount();
|
||||
String xmr = etAmount.getNativeAmount();
|
||||
if (xmr != null) {
|
||||
sendListener.getTxData().setAmount(Wallet.getAmountFromString(xmr));
|
||||
} else {
|
||||
@@ -146,7 +138,7 @@ public class SendAmountWizardFragment extends SendWizardFragment {
|
||||
public void onResumeFragment() {
|
||||
super.onResumeFragment();
|
||||
Timber.d("onResumeFragment()");
|
||||
Helper.hideKeyboard(getActivity());
|
||||
Helper.showKeyboard(getActivity());
|
||||
final long funds = getTotalFunds();
|
||||
maxFunds = 1.0 * funds / 1000000000000L;
|
||||
if (!sendListener.getActivityCallback().isStreetMode()) {
|
||||
@@ -156,11 +148,11 @@ 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 ((evAmount.getAmount() != null) && evAmount.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)) {
|
||||
evAmount.setAmount(data.amount);
|
||||
etAmount.setAmount(data.amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -31,8 +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.ExchangeBtcTextView;
|
||||
import com.m2049r.xmrwallet.widget.NumberPadView;
|
||||
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;
|
||||
@@ -62,8 +62,7 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||
}
|
||||
|
||||
private TextView tvFunds;
|
||||
private ExchangeBtcTextView evAmount;
|
||||
private NumberPadView numberPad;
|
||||
private ExchangeOtherEditText etAmount;
|
||||
|
||||
private TextView tvXmrToParms;
|
||||
private SendProgressView evParams;
|
||||
@@ -86,24 +85,20 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||
|
||||
tvXmrToParms = view.findViewById(R.id.tvXmrToParms);
|
||||
|
||||
evAmount = view.findViewById(R.id.evAmount);
|
||||
numberPad = view.findViewById(R.id.numberPad);
|
||||
numberPad.setListener(evAmount);
|
||||
|
||||
Helper.hideKeyboard(getActivity());
|
||||
|
||||
etAmount = view.findViewById(R.id.etAmount);
|
||||
etAmount.requestFocus();
|
||||
return view;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onValidateFields() {
|
||||
if (!evAmount.validate(maxBtc, minBtc)) {
|
||||
if (!etAmount.validate(maxBtc, minBtc)) {
|
||||
return false;
|
||||
}
|
||||
if (sendListener != null) {
|
||||
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
||||
String btcString = evAmount.getAmount();
|
||||
String btcString = etAmount.getNativeAmount();
|
||||
if (btcString != null) {
|
||||
try {
|
||||
double btc = Double.parseDouble(btcString);
|
||||
@@ -122,10 +117,12 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||
|
||||
private void setBip70Mode() {
|
||||
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
|
||||
if (txDataBtc.getBip70() != null) {
|
||||
numberPad.setVisibility(View.INVISIBLE);
|
||||
if (txDataBtc.getBip70() == null) {
|
||||
etAmount.setEditable(true);
|
||||
Helper.showKeyboard(getActivity());
|
||||
} else {
|
||||
numberPad.setVisibility(View.VISIBLE);
|
||||
etAmount.setEditable(false);
|
||||
Helper.hideKeyboard(getActivity());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +138,6 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||
public void onResumeFragment() {
|
||||
super.onResumeFragment();
|
||||
Timber.d("onResumeFragment()");
|
||||
Helper.hideKeyboard(getActivity());
|
||||
final long funds = getTotalFunds();
|
||||
if (!sendListener.getActivityCallback().isStreetMode()) {
|
||||
tvFunds.setText(getString(R.string.send_available,
|
||||
@@ -153,7 +149,7 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||
final BarcodeData data = sendListener.popBarcodeData();
|
||||
if (data != null) {
|
||||
if (data.amount != null) {
|
||||
evAmount.setAmount(data.amount);
|
||||
etAmount.setAmount(data.amount);
|
||||
}
|
||||
}
|
||||
setBip70Mode();
|
||||
@@ -171,7 +167,7 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||
getView().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
evAmount.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());
|
||||
@@ -211,7 +207,7 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
|
||||
}
|
||||
|
||||
private void processOrderParmsError(final Exception ex) {
|
||||
evAmount.setRate(0);
|
||||
etAmount.setExchangeRate(0);
|
||||
orderParameters = null;
|
||||
maxBtc = 0;
|
||||
minBtc = 0;
|
||||
|
@@ -457,8 +457,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
|
||||
}
|
||||
showProgress(3, getString(R.string.label_send_progress_create_tx));
|
||||
TxData txData = sendListener.getTxData();
|
||||
txData.setDestinationAddress(xmrtoStatus.getXmrReceivingAddress());
|
||||
txData.setPaymentId(xmrtoStatus.getXmrRequiredPaymentIdShort());
|
||||
txData.setDestinationAddress(xmrtoStatus.getXmrReceivingSubaddress());
|
||||
txData.setAmount(Wallet.getAmountFromDouble(xmrtoStatus.getXmrAmountTotal()));
|
||||
getActivityCallback().onPrepareSend(xmrtoStatus.getUuid(), txData);
|
||||
}
|
||||
|
@@ -134,6 +134,10 @@ public enum Instruction {
|
||||
return value;
|
||||
}
|
||||
|
||||
public byte getByteValue() {
|
||||
return (byte) (value & 0xFF);
|
||||
}
|
||||
|
||||
private int value;
|
||||
|
||||
Instruction(int value) {
|
||||
|
@@ -27,9 +27,11 @@ import com.btchip.BTChipException;
|
||||
import com.btchip.comm.BTChipTransport;
|
||||
import com.btchip.comm.android.BTChipTransportAndroidHID;
|
||||
import com.m2049r.xmrwallet.BuildConfig;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
@@ -40,9 +42,11 @@ public class Ledger {
|
||||
static public final int LOOKAHEAD_SUBADDRESSES = 20;
|
||||
static public final String SUBADDRESS_LOOKAHEAD = LOOKAHEAD_ACCOUNTS + ":" + LOOKAHEAD_SUBADDRESSES;
|
||||
|
||||
private static final byte PROTOCOL_VERSION = 0x02;
|
||||
public static final int SW_OK = 0x9000;
|
||||
public static final int SW_INS_NOT_SUPPORTED = 0x6D00;
|
||||
public static final int OK[] = {SW_OK};
|
||||
public static final int MINIMUM_LEDGER_VERSION = (1 << 16) + (3 << 8) + (1); // 1.3.1
|
||||
|
||||
public static UsbDevice findDevice(UsbManager usbManager) {
|
||||
if (!ENABLED) return null;
|
||||
@@ -89,6 +93,21 @@ public class Ledger {
|
||||
}
|
||||
}
|
||||
|
||||
static public boolean check() {
|
||||
if (Instance == null) return false;
|
||||
byte[] moneroVersion = WalletManager.moneroVersion().getBytes(StandardCharsets.US_ASCII);
|
||||
|
||||
try {
|
||||
byte[] resp = Instance.exchangeApduNoOpt(Instruction.INS_RESET, moneroVersion, OK);
|
||||
int deviceVersion = (resp[0] << 16) + (resp[1] << 8) + (resp[2]);
|
||||
if (deviceVersion < MINIMUM_LEDGER_VERSION)
|
||||
return false;
|
||||
} catch (BTChipException ex) { // comm error - probably wrong app started on device
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
final private BTChipTransport transport;
|
||||
final private String name;
|
||||
private int lastSW = 0;
|
||||
@@ -112,7 +131,7 @@ public class Ledger {
|
||||
synchronized private byte[] exchangeRaw(byte[] apdu) {
|
||||
if (transport == null)
|
||||
throw new IllegalStateException("No transport (probably closed previously)");
|
||||
Timber.i("exchangeRaw %02x", apdu[1]);
|
||||
Timber.d("exchangeRaw %02x", apdu[1]);
|
||||
Instruction ins = Instruction.fromByte(apdu[1]);
|
||||
if (listener != null) listener.onInstructionSend(ins, apdu);
|
||||
sniffOut(ins, apdu);
|
||||
@@ -120,7 +139,6 @@ public class Ledger {
|
||||
if (listener != null) listener.onInstructionReceive(ins, data);
|
||||
sniffIn(data);
|
||||
return data;
|
||||
|
||||
}
|
||||
|
||||
private byte[] exchange(byte[] apdu) throws BTChipException {
|
||||
@@ -148,68 +166,19 @@ public class Ledger {
|
||||
throw new BTChipException("Invalid status", lastSW);
|
||||
}
|
||||
|
||||
private byte[] exchangeApdu(byte cla, byte ins, byte p1, byte p2, byte[] data, int acceptedSW[]) throws BTChipException {
|
||||
byte[] apdu = new byte[data.length + 5];
|
||||
apdu[0] = cla;
|
||||
apdu[1] = ins;
|
||||
apdu[2] = p1;
|
||||
apdu[3] = p2;
|
||||
apdu[4] = (byte) (data.length);
|
||||
System.arraycopy(data, 0, apdu, 5, data.length);
|
||||
private byte[] exchangeApduNoOpt(Instruction instruction, byte[] data, int acceptedSW[])
|
||||
throws BTChipException {
|
||||
byte[] apdu = new byte[data.length + 6];
|
||||
apdu[0] = PROTOCOL_VERSION;
|
||||
apdu[1] = instruction.getByteValue();
|
||||
apdu[2] = 0; // p1
|
||||
apdu[3] = 0; // p2
|
||||
apdu[4] = (byte) (data.length + 1); // +1 because the opt byte is part of the data
|
||||
apdu[5] = 0; // opt
|
||||
System.arraycopy(data, 0, apdu, 6, data.length);
|
||||
return exchangeCheck(apdu, acceptedSW);
|
||||
}
|
||||
|
||||
private byte[] exchangeApdu(byte cla, byte ins, byte p1, byte p2, int length, int acceptedSW[]) throws BTChipException {
|
||||
byte[] apdu = new byte[5];
|
||||
apdu[0] = cla;
|
||||
apdu[1] = ins;
|
||||
apdu[2] = p1;
|
||||
apdu[3] = p2;
|
||||
apdu[4] = (byte) (length);
|
||||
return exchangeCheck(apdu, acceptedSW);
|
||||
}
|
||||
|
||||
private byte[] exchangeApduSplit(byte cla, byte ins, byte p1, byte p2, byte[] data, int acceptedSW[]) throws BTChipException {
|
||||
int offset = 0;
|
||||
byte[] result = null;
|
||||
while (offset < data.length) {
|
||||
int blockLength = ((data.length - offset) > 255 ? 255 : data.length - offset);
|
||||
byte[] apdu = new byte[blockLength + 5];
|
||||
apdu[0] = cla;
|
||||
apdu[1] = ins;
|
||||
apdu[2] = p1;
|
||||
apdu[3] = p2;
|
||||
apdu[4] = (byte) (blockLength);
|
||||
System.arraycopy(data, offset, apdu, 5, blockLength);
|
||||
result = exchangeCheck(apdu, acceptedSW);
|
||||
offset += blockLength;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private byte[] exchangeApduSplit2(byte cla, byte ins, byte p1, byte p2, byte[] data, byte[] data2, int acceptedSW[]) throws BTChipException {
|
||||
int offset = 0;
|
||||
byte[] result = null;
|
||||
int maxBlockSize = 255 - data2.length;
|
||||
while (offset < data.length) {
|
||||
int blockLength = ((data.length - offset) > maxBlockSize ? maxBlockSize : data.length - offset);
|
||||
boolean lastBlock = ((offset + blockLength) == data.length);
|
||||
byte[] apdu = new byte[blockLength + 5 + (lastBlock ? data2.length : 0)];
|
||||
apdu[0] = cla;
|
||||
apdu[1] = ins;
|
||||
apdu[2] = p1;
|
||||
apdu[3] = p2;
|
||||
apdu[4] = (byte) (blockLength + (lastBlock ? data2.length : 0));
|
||||
System.arraycopy(data, offset, apdu, 5, blockLength);
|
||||
if (lastBlock) {
|
||||
System.arraycopy(data2, 0, apdu, 5 + blockLength, data2.length);
|
||||
}
|
||||
result = exchangeCheck(apdu, acceptedSW);
|
||||
offset += blockLength;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onInstructionSend(Instruction ins, byte[] apdu);
|
||||
|
||||
@@ -251,7 +220,6 @@ public class Ledger {
|
||||
if (ins == Instruction.INS_GET_KEY) {
|
||||
snoopKey = (apdu[2] == 2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void sniffIn(byte[] data) {
|
||||
|
@@ -354,4 +354,6 @@ public class WalletManager {
|
||||
static public native void logWarning(String category, String message);
|
||||
|
||||
static public native void logError(String category, String message);
|
||||
|
||||
static public native String moneroVersion();
|
||||
}
|
@@ -290,7 +290,7 @@ public class WalletService extends Service {
|
||||
showProgress(10);
|
||||
Wallet.Status walletStatus = start(walletId, walletPw);
|
||||
if (observer != null) observer.onWalletStarted(walletStatus);
|
||||
if (!walletStatus.isOk()) {
|
||||
if ((walletStatus == null) || !walletStatus.isOk()) {
|
||||
errorState = true;
|
||||
stop();
|
||||
}
|
||||
|
@@ -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,33 @@ 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 < 2) decimals = 2;
|
||||
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 +634,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 {
|
||||
|
@@ -103,6 +103,12 @@ public class RestoreHeight {
|
||||
blockheight.put("2019-03-01", 1781681L);
|
||||
blockheight.put("2019-04-01", 1803081L);
|
||||
blockheight.put("2019-05-01", 1824671L);
|
||||
blockheight.put("2019-06-01", 1847005L);
|
||||
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) {
|
||||
|
@@ -1,198 +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.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.widget.ArrayAdapter;
|
||||
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 ExchangeBtcTextView extends LinearLayout
|
||||
implements NumberPadView.NumberPadListener {
|
||||
|
||||
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() {
|
||||
tvAmountA.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;
|
||||
tvAmountA.setText(btcAmount);
|
||||
xmrAmount = null;
|
||||
exchange();
|
||||
}
|
||||
|
||||
public String getAmount() {
|
||||
return btcAmount;
|
||||
}
|
||||
|
||||
TextView tvAmountA;
|
||||
TextView tvAmountB;
|
||||
Spinner sCurrencyA;
|
||||
Spinner sCurrencyB;
|
||||
|
||||
public ExchangeBtcTextView(Context context) {
|
||||
super(context);
|
||||
initializeViews(context);
|
||||
}
|
||||
|
||||
public ExchangeBtcTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initializeViews(context);
|
||||
}
|
||||
|
||||
public ExchangeBtcTextView(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_text, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
tvAmountA = findViewById(R.id.tvAmountA);
|
||||
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);
|
||||
}
|
||||
|
||||
double xmrBtcRate = 0;
|
||||
|
||||
public void exchange() {
|
||||
btcAmount = tvAmountA.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);
|
||||
}
|
||||
|
||||
// deal with attached numpad
|
||||
@Override
|
||||
public void onDigitPressed(final int digit) {
|
||||
tvAmountA.append(String.valueOf(digit));
|
||||
exchange();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPointPressed() {
|
||||
//TODO locale?
|
||||
if (tvAmountA.getText().toString().indexOf('.') == -1) {
|
||||
if (tvAmountA.getText().toString().isEmpty()) {
|
||||
tvAmountA.append("0");
|
||||
}
|
||||
tvAmountA.append(".");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackSpacePressed() {
|
||||
String entry = tvAmountA.getText().toString();
|
||||
int length = entry.length();
|
||||
if (length > 0) {
|
||||
tvAmountA.setText(entry.substring(0, entry.length() - 1));
|
||||
exchange();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClearAll() {
|
||||
tvAmountA.setText(null);
|
||||
exchange();
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user