Merge branch 'm2049r:master' into arabic-translate

This commit is contained in:
XD22 2024-11-06 01:24:02 +02:00 committed by GitHub
commit 4e8d30e2e1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
265 changed files with 8149 additions and 4527 deletions

8
.gitignore vendored
View File

@ -8,12 +8,8 @@
.DS_Store
/app/build
/app/release
/app/alpha
/app/prod
/app/alphaMainnet
/app/prodMainnet
/app/alphaStagenet
/app/prodStagenet
/app/alpha*
/app/prod*
/app/.cxx
/monerujo.id
/external-libs/VERSION

View File

@ -4,12 +4,12 @@ android {
ndkVersion '17.2.4988734'
defaultConfig {
applicationId "com.m2049r.xmrwallet"
buildToolsVersion = '34.0.0'
compileSdk 34
buildToolsVersion = '35.0.0'
compileSdk 35
minSdkVersion 21
targetSdkVersion 33
versionCode 3311
versionName "3.3.11 'Argentina'"
targetSdkVersion 35
versionCode 4104
versionName "4.1.4 'Exolix'"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
@ -24,7 +24,7 @@ android {
}
}
flavorDimensions 'type', 'net'
flavorDimensions = ['type', 'net']
productFlavors {
mainnet {
dimension 'net'
@ -58,7 +58,7 @@ android {
applicationIdSuffix ".debug"
}
applicationVariants.all { variant ->
variant.buildConfigField "String", "ID_A", "\"" + getId("ID_A") + "\""
variant.buildConfigField "String", "ID_F", "\"" + getId("ID_F") + "\""
}
}
@ -132,10 +132,10 @@ static def getId(name) {
}
dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22"))
implementation 'androidx.core:core:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core:1.13.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.cardview:cardview:1.0.0'
@ -143,7 +143,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.preference:preference:1.2.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'me.dm7.barcodescanner:zxing:1.9.8'
implementation "com.squareup.okhttp3:okhttp:4.12.0"

View File

@ -5,6 +5,11 @@
android:name="android.hardware.camera"
android:required="false" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
@ -12,6 +17,7 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<queries>
<intent>
@ -98,7 +104,12 @@
android:name=".service.WalletService"
android:description="@string/service_description"
android:exported="false"
android:label="Monero Wallet Service" />
android:foregroundServiceType="specialUse"
android:label="Monero Wallet Service">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Keeps app in sync with the blockchain" />
</service>
<provider
android:name="androidx.core.content.FileProvider"

View File

@ -1,5 +1,5 @@
/**
* Copyright (c) 2017 m2049r
* Copyright (c) 2017-2024 m2049r
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,8 +18,6 @@
#include "monerujo.h"
#include "wallet2_api.h"
//TODO explicit casting jlong, jint, jboolean to avoid warnings
#ifdef __cplusplus
extern "C"
{
@ -34,7 +32,6 @@ extern "C"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , LOG_TAG,__VA_ARGS__)
static JavaVM *cachedJVM;
static jclass class_String;
static jclass class_ArrayList;
static jclass class_WalletListener;
static jclass class_CoinsInfo;
@ -42,17 +39,11 @@ static jclass class_TransactionInfo;
static jclass class_Transfer;
static jclass class_Ledger;
static jclass class_WalletStatus;
static jclass class_BluetoothService;
static jclass class_SidekickService;
std::mutex _listenerMutex;
//void jstringToString(JNIEnv *env, std::string &str, jstring jstr) {
// if (!jstr) return;
// const int len = env->GetStringUTFLength(jstr);
// const char *chars = env->GetStringUTFChars(jstr, nullptr);
// str.assign(chars, len);
// env->ReleaseStringUTFChars(jstr, chars);
//}
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
cachedJVM = jvm;
LOGI("JNI_OnLoad");
@ -62,8 +53,6 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
}
//LOGI("JNI_OnLoad ok");
class_String = static_cast<jclass>(jenv->NewGlobalRef(
jenv->FindClass("java/lang/String")));
class_ArrayList = static_cast<jclass>(jenv->NewGlobalRef(
jenv->FindClass("java/util/ArrayList")));
class_CoinsInfo = static_cast<jclass>(jenv->NewGlobalRef(
@ -78,6 +67,8 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
jenv->FindClass("com/m2049r/xmrwallet/ledger/Ledger")));
class_WalletStatus = static_cast<jclass>(jenv->NewGlobalRef(
jenv->FindClass("com/m2049r/xmrwallet/model/Wallet$Status")));
class_BluetoothService = static_cast<jclass>(jenv->NewGlobalRef(
jenv->FindClass("com/m2049r/xmrwallet/service/BluetoothService")));
return JNI_VERSION_1_6;
}
#ifdef __cplusplus
@ -1686,6 +1677,79 @@ int LedgerFind(char *buffer, size_t len) {
return ret;
}
//
// SidekickWallet Stuff
//
/**
* @brief BtExchange - exchange data with Monerujo Device
* @param request - buffer for data to send
* @param request_len - length of data to send
* @param response - buffer for received data
* @param max_resp_len - size of receive buffer
*
* @return length of received data in response or -1 if error, -2 if response buffer too small
*/
int BtExchange(
unsigned char *request,
unsigned int request_len,
unsigned char *response,
unsigned int max_resp_len) {
JNIEnv *jenv;
int envStat = attachJVM(&jenv);
if (envStat == JNI_ERR) return -16;
jmethodID exchangeMethod = jenv->GetStaticMethodID(class_BluetoothService, "Exchange",
"([B)[B");
auto reqLen = static_cast<jsize>(request_len);
jbyteArray reqData = jenv->NewByteArray(reqLen);
jenv->SetByteArrayRegion(reqData, 0, reqLen, (jbyte *) request);
LOGD("BtExchange cmd: 0x%02x with %u bytes", request[0], reqLen);
auto dataRecv = (jbyteArray)
jenv->CallStaticObjectMethod(class_BluetoothService, exchangeMethod, reqData);
jenv->DeleteLocalRef(reqData);
if (dataRecv == nullptr) {
detachJVM(jenv, envStat);
LOGD("BtExchange: error reading");
return -1;
}
jsize respLen = jenv->GetArrayLength(dataRecv);
LOGD("BtExchange response is %u bytes", respLen);
if (respLen <= max_resp_len) {
jenv->GetByteArrayRegion(dataRecv, 0, respLen, (jbyte *) response);
jenv->DeleteLocalRef(dataRecv);
detachJVM(jenv, envStat);
return static_cast<int>(respLen);;
} else {
jenv->DeleteLocalRef(dataRecv);
detachJVM(jenv, envStat);
LOGE("BtExchange response buffer too small: %u < %u", respLen, max_resp_len);
return -2;
}
}
/**
* @brief ConfirmTransfers
* @param transfers - string of "fee (':' address ':' amount)+"
*
* @return true on accept, false on reject
*/
bool ConfirmTransfers(const char *transfers) {
JNIEnv *jenv;
int envStat = attachJVM(&jenv);
if (envStat == JNI_ERR) return -16;
jmethodID confirmMethod = jenv->GetStaticMethodID(class_SidekickService, "ConfirmTransfers",
"(Ljava/lang/String;)Z");
jstring _transfers = jenv->NewStringUTF(transfers);
auto confirmed =
jenv->CallStaticBooleanMethod(class_SidekickService, confirmMethod, _transfers);
jenv->DeleteLocalRef(_transfers);
return confirmed;
}
#ifdef __cplusplus
}
#endif

View File

@ -1,5 +1,5 @@
/**
* Copyright (c) 2017 m2049r
* Copyright (c) 2017-2024 m2049r
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,6 +19,8 @@
#include <jni.h>
#include <string>
/*
#include <android/log.h>
@ -28,6 +30,10 @@
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
*/
void ThrowException(JNIEnv *jenv, const char* type, const char* msg) {
jenv->ThrowNew(jenv->FindClass(type), msg);
}
jfieldID getHandleField(JNIEnv *env, jobject obj, const char *fieldName = "handle") {
jclass c = env->GetObjectClass(obj);
return env->GetFieldID(c, fieldName, "J"); // of type long
@ -35,8 +41,16 @@ jfieldID getHandleField(JNIEnv *env, jobject obj, const char *fieldName = "handl
template<typename T>
T *getHandle(JNIEnv *env, jobject obj, const char *fieldName = "handle") {
return reinterpret_cast<T *>(env->GetLongField(obj, getHandleField(env, obj, fieldName)));
}
template<typename T>
void destroyNativeObject(JNIEnv *env, T nativeObjectHandle, jobject obj, const char *fieldName = "handle") {
jlong handle = env->GetLongField(obj, getHandleField(env, obj, fieldName));
return reinterpret_cast<T *>(handle);
if (handle != 0) {
ThrowException(env, "java/lang/IllegalStateException", "invalid handle (destroy)");
}
delete reinterpret_cast<T *>(nativeObjectHandle);
}
void setHandleFromLong(JNIEnv *env, jobject obj, jlong handle) {
@ -54,7 +68,7 @@ extern "C"
{
#endif
extern const char* const MONERO_VERSION; // the actual monero core version
extern const char *const MONERO_VERSION; // the actual monero core version
// from monero-core crypto/hash-ops.h - avoid #including monero code here
enum {
@ -62,18 +76,40 @@ enum {
HASH_DATA_AREA = 136
};
void cn_slow_hash(const void *data, size_t length, char *hash, int variant, int prehashed, uint64_t height);
void cn_slow_hash(const void *data, size_t length, char *hash, int variant, int prehashed,
uint64_t height);
inline void slow_hash(const void *data, const size_t length, char *hash) {
cn_slow_hash(data, length, hash, 0 /*variant*/, 0 /*prehashed*/, 0 /*height*/);
}
inline void slow_hash_broken(const void *data, char *hash, int variant) {
cn_slow_hash(data, 200 /*sizeof(union hash_state)*/, hash, variant, 1 /*prehashed*/, 0 /*height*/);
cn_slow_hash(data, 200 /*sizeof(union hash_state)*/, hash, variant, 1 /*prehashed*/,
0 /*height*/);
}
#ifdef __cplusplus
}
#endif
namespace Monerujo {
class SidekickWallet {
public:
enum Status {
Status_Ok,
Status_Error,
Status_Critical
};
SidekickWallet(uint8_t networkType, std::string a, std::string b);
~SidekickWallet();
std::string call(int commandId, const std::string &request);
void reset();
Status status() const;
};
}
#endif //XMRWALLET_WALLET_LIB_H

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -16,7 +16,17 @@
package com.m2049r.xmrwallet;
import android.Manifest;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@ -24,7 +34,8 @@ import android.os.PowerManager;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.CallSuper;
import androidx.fragment.app.FragmentActivity;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.dialog.ProgressDialog;
@ -159,4 +170,87 @@ public class BaseActivity extends SecureActivity
barcodeData = null;
return popped;
}
public boolean isNetworkAvailable() {
ConnectivityManager connectivityManager = (ConnectivityManager) getApplication().getSystemService(Context.CONNECTIVITY_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Network network = connectivityManager.getActiveNetwork();
if (network == null) return false;
NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network);
return networkCapabilities != null && (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) || networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH));
} else {
NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnected();
}
}
static private final int REQUEST_CODE_BLUETOOTH_PERMISSIONS = 32423;
void btPermissionGranted() {
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_BLUETOOTH_PERMISSIONS) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
btPermissionGranted();
} // else onResume() takes care of trying again
}
}
private void showBtPermissionsDialog() {
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
alertDialogBuilder.setMessage(R.string.bluetooth_permissions);
alertDialogBuilder.setPositiveButton(R.string.bluetooth_permissions_ok,
(dialog, which) -> requestBtPermissions());
alertDialogBuilder.setCancelable(false);
AlertDialog alertDialog = alertDialogBuilder.create();
alertDialog.show();
}
private void showAppInfoDialog() {
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
alertDialogBuilder.setMessage(R.string.bluetooth_permissions);
alertDialogBuilder.setPositiveButton(R.string.bluetooth_permissions_settings, (dialog, which) -> {
Intent i = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
i.addCategory(Intent.CATEGORY_DEFAULT);
i.setData(Uri.parse("package:" + getPackageName()));
startActivity(i);
});
alertDialogBuilder.setNegativeButton(R.string.bluetooth_permissions_cancel, (dialog, which) -> {
finish();
});
alertDialogBuilder.setCancelable(false);
AlertDialog alertDialog = alertDialogBuilder.create();
alertDialog.show();
}
private void requestBtPermissions() {
ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_SCAN}, REQUEST_CODE_BLUETOOTH_PERMISSIONS);
}
private boolean firstCheck = true;
void checkBtPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if ((ActivityCompat.checkSelfPermission(this, android.Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) ||
(ActivityCompat.checkSelfPermission(this, android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED)) {
if (shouldShowRequestPermissionRationale(android.Manifest.permission.BLUETOOTH_SCAN) || shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) {
showBtPermissionsDialog();
} else {
if (firstCheck) {
requestBtPermissions();
} else {
showAppInfoDialog();
}
}
} else {
btPermissionGranted();
}
firstCheck = false;
} else {
btPermissionGranted();
}
}
}

View File

@ -0,0 +1,326 @@
/*
* Copyright (C) 2014 The Android Open Source Project
*
* 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.
*/
// mostly from BluetoothChatFragment https://github.com/android/connectivity-samples
package com.m2049r.xmrwallet;
import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.m2049r.xmrwallet.service.BluetoothService;
import com.m2049r.xmrwallet.util.Flasher;
import java.security.SecureRandom;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import timber.log.Timber;
public class BluetoothFragment extends Fragment {
public BluetoothFragment() {
super();
}
interface Listener {
void onDeviceConnected(String connectedDeviceName);
void abort(String message);
void onReceive(int commandId);
}
//TODO enable discover only after wallet is loaded
Listener activityCallback;
// Intent request codes
private static final int REQUEST_ENABLE_BT = 3;
private ImageView btIcon;
private ProgressBar pbConnecting;
private TextView btCode;
private TextView btName;
private String connectedDeviceName = null;
private BluetoothAdapter bluetoothAdapter = null;
private BluetoothService bluetoothService = null;
public enum Mode {
CLIENT, SERVER
}
private Mode mode = Mode.CLIENT;
public BluetoothFragment(Mode mode) {
super();
this.mode = mode;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Get local Bluetooth adapter
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
// If the adapter is null, then Bluetooth is not supported
if (bluetoothAdapter == null) {
if (activityCallback != null)
activityCallback.abort("Bluetooth is not available"); //TODO strings.xml
}
}
public void start() {
if (bluetoothAdapter == null) return;
// If BT is not on, request that it be enabled.
// setupComm() will then be called during onActivityResult
if (!bluetoothAdapter.isEnabled()) {
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE_BT);
} else if (bluetoothService == null) {
setupCommunication();
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (bluetoothService != null) {
bluetoothService.setUiHandler(null);
bluetoothService = null;
}
// The BluetoothService is stopped in LoginActivity::onDestroy
}
@Override
public void onPause() {
Timber.d("onPause %s", mode);
super.onPause();
}
@Override
public void onResume() {
Timber.d("onResume %s", mode);
super.onResume();
// Performing this check in onResume() covers the case in which BT was
// not enabled during onStart(), so we were paused to enable it...
// onResume() will be called when ACTION_REQUEST_ENABLE activity returns.
if (bluetoothService != null) {
// Only if the state is STATE_NONE, do we know that we haven't started already
if (!bluetoothService.isStarted()) {
// Start the Bluetooth services
bluetoothService.start();
}
}
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof BluetoothFragment.Listener) {
activityCallback = (BluetoothFragment.Listener) context;
} else {
throw new ClassCastException(context.toString()
+ " must implement Listener");
}
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
Timber.d("onCreateView");
return inflater.inflate(R.layout.fragment_bluetooth, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
Timber.d("onViewCreated");
super.onCreate(savedInstanceState);
if (savedInstanceState != null) return;
btName = view.findViewById(R.id.btName);
btCode = view.findViewById(R.id.btCode);
btIcon = view.findViewById(R.id.btIcon);
pbConnecting = view.findViewById(R.id.pbConnecting);
setConnecting(false);
setInfo(null, null);
}
/**
* Set up the UI and background operations for comms.
*/
private void setupCommunication() {
Timber.d("startCommunication()");
if (!isAdded()) {
return;
}
if (bluetoothService != null) throw new IllegalStateException("bluetoothService != null");
// Initialize the BluetoothService to perform bluetooth connections
bluetoothService = BluetoothService.GetInstance();
bluetoothService.setUiHandler(handler);
if (mode == Mode.SERVER)
bluetoothService.start();
setInfo(bluetoothService.getConnectedName(), bluetoothService.getConnectedCode());
showState(bluetoothService.getState());
}
private void showState(int state) {
if (!isAdded()) return;
Light light;
switch (state) {
case BluetoothService.State.LISTEN:
light = Light.LISTEN;
break;
case BluetoothService.State.CONNECTING:
light = Light.CONNECTING;
break;
case BluetoothService.State.CONNECTED:
light = Light.CONNECTED;
break;
case BluetoothService.State.NONE:
default:
light = Light.NONE;
}
final Flasher flash = new Flasher(requireContext(), light);
btIcon.setImageDrawable(flash.getDrawable());
btFlash = flash;
}
@Getter
@RequiredArgsConstructor
public enum Light implements Flasher.Light {
LISTEN(R.drawable.ic_bluetooth_24),
CONNECTING(R.drawable.ic_bluetooth_searching_24),
CONNECTED(R.drawable.ic_bluetooth_connected_24),
NONE(R.drawable.ic_bluetooth_disabled_24);
final private int drawableId;
}
Flasher btFlash;
private void flashState() {
if (btFlash != null)
btFlash.flash(getView());
}
/**
* The Handler that gets information back from the BluetoothService
*/
private final Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case BluetoothService.MessageType.STATE_CHANGE:
showState(msg.arg1);
if (msg.arg1 <= BluetoothService.State.LISTEN) {
setInfo(null, null);
connectedDeviceName = null;
activityCallback.onDeviceConnected(null); // i.e. disconnected - ugly :(
setConnecting(false);
}
break;
case BluetoothService.MessageType.WRITE:
Timber.d("WRITE_MESSAGE: %d bytes", msg.arg1);
break;
case BluetoothService.MessageType.READ_CMD:
Timber.d("READ_COMMAND 0x%x (%d bytes)", msg.arg2, msg.arg1);
if (activityCallback != null) {
activityCallback.onReceive(msg.arg2);
}
break;
case BluetoothService.MessageType.CODE:
Timber.d("CODE: %s", msg.obj);
btCode.setText((String) msg.obj);
break;
case BluetoothService.MessageType.DEVICE_NAME:
connectedDeviceName = (String) msg.obj;
setInfo(connectedDeviceName, null);
activityCallback.onDeviceConnected(connectedDeviceName);
setConnecting(false);
if (mode == Mode.CLIENT) {
final int code = new SecureRandom().nextInt(10000);
bluetoothService.write(code);
}
break;
case BluetoothService.MessageType.TOAST:
if (isAdded())
Toast.makeText(getActivity(), (String) msg.obj, Toast.LENGTH_SHORT).show();
break;
}
flashState();
}
};
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_ENABLE_BT) {// When the request to enable Bluetooth returns
if (resultCode == Activity.RESULT_OK) {
// Bluetooth is now enabled, so set up a chat session
setupCommunication();
} else {
// User did not enable Bluetooth or an error occurred
Timber.d("BT not enabled");
if (activityCallback != null)
activityCallback.abort("Bluetooth is not enabled"); //TODO strings.xml
}
} else {
Timber.w("Unhandled request code %d", requestCode);
}
flashState();
}
private void setConnecting(boolean enable) {
pbConnecting.setVisibility(enable ? View.VISIBLE : View.INVISIBLE);
}
public void connectDevice(String address) {
setConnecting(true);
bluetoothService.connect(bluetoothAdapter.getRemoteDevice(address));
}
private void setInfo(String name, String code) {
try {
btName.setText(name == null ? getResources().getString(R.string.sidekick_not_connected) : name);
btCode.setText(getResources().getString(R.string.sidekick_pin, code != null ? code : "----"));
} catch (IllegalStateException ex) { // no context, so no strings
// never mind
}
}
}

View File

@ -18,7 +18,6 @@ package com.m2049r.xmrwallet;
import android.app.Activity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Editable;
import android.text.Html;
@ -67,8 +66,20 @@ public class GenerateFragment extends Fragment {
static final String TYPE_KEY = "key";
static final String TYPE_SEED = "seed";
static final String TYPE_LEDGER = "ledger";
static final String TYPE_SIDEKICK = "sidekick";
static final String TYPE_VIEWONLY = "view";
static Wallet.Device getDeviceType(String type) {
switch (type) {
case TYPE_SIDEKICK:
return Wallet.Device.Sidekick;
case TYPE_LEDGER:
return Wallet.Device.Ledger;
default:
return Wallet.Device.Software;
}
}
private TextInputLayout etWalletName;
private PasswordEntryView etWalletPassword;
private LinearLayout llFingerprintAuth;
@ -195,6 +206,7 @@ public class GenerateFragment extends Fragment {
etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_UNSPECIFIED);
break;
case TYPE_LEDGER:
case TYPE_SIDEKICK:
etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_DONE);
etWalletPassword.getEditText().setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
@ -308,7 +320,7 @@ public class GenerateFragment extends Fragment {
private boolean checkName() {
String name = etWalletName.getEditText().getText().toString();
boolean ok = true;
if (name.length() == 0) {
if (name.isEmpty()) {
etWalletName.setError(getString(R.string.generate_wallet_name));
ok = false;
} else if (name.charAt(0) == '.') {
@ -450,11 +462,12 @@ public class GenerateFragment extends Fragment {
activityCallback.onGenerate(name, crazyPass, seed, offset, height);
break;
case TYPE_LEDGER:
case TYPE_SIDEKICK:
bGenerate.setEnabled(false);
if (fingerprintAuthAllowed) {
KeyStoreHelper.saveWalletUserPass(requireActivity(), name, password);
}
activityCallback.onGenerateLedger(name, crazyPass, height);
activityCallback.onGenerateDevice(getDeviceType(type), name, crazyPass, height);
break;
case TYPE_KEY:
case TYPE_VIEWONLY:
@ -498,6 +511,8 @@ public class GenerateFragment extends Fragment {
return getString(R.string.generate_wallet_type_seed);
case TYPE_LEDGER:
return getString(R.string.generate_wallet_type_ledger);
case TYPE_SIDEKICK:
return getString(R.string.generate_wallet_type_sidekick);
case TYPE_VIEWONLY:
return getString(R.string.generate_wallet_type_view);
default:
@ -515,7 +530,7 @@ public class GenerateFragment extends Fragment {
void onGenerate(String name, String password, String address, String viewKey, String spendKey, long height);
void onGenerateLedger(String name, String password, long height);
void onGenerateDevice(Wallet.Device device, String name, String password, long height);
void setTitle(String title);
@ -555,6 +570,9 @@ public class GenerateFragment extends Fragment {
case TYPE_LEDGER:
inflater.inflate(R.menu.create_wallet_ledger, menu);
break;
case TYPE_SIDEKICK:
inflater.inflate(R.menu.create_wallet_sidekick, menu);
break;
case TYPE_VIEWONLY:
inflater.inflate(R.menu.create_wallet_view, menu);
break;
@ -581,13 +599,11 @@ public class GenerateFragment extends Fragment {
.setCancelable(false)
.setPositiveButton(getString(R.string.label_ok), null)
.setNegativeButton(getString(R.string.label_cancel),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
Helper.hideKeyboardAlways(activity);
etWalletMnemonic.getEditText().getText().clear();
dialog.cancel();
ledgerDialog = null;
}
(dialog, id) -> {
Helper.hideKeyboardAlways(activity);
etWalletMnemonic.getEditText().getText().clear();
dialog.cancel();
ledgerDialog = null;
});
ledgerDialog = alertDialogBuilder.create();

View File

@ -99,7 +99,7 @@ public class GenerateReviewFragment extends Fragment {
private String walletPath;
private String walletName;
private OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(false) {
private final OnBackPressedCallback backPressedCallback = new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
// nothing
@ -164,6 +164,7 @@ public class GenerateReviewFragment extends Fragment {
});
Bundle args = getArguments();
assert args != null;
type = args.getString(REQUEST_TYPE);
walletPath = args.getString(REQUEST_PATH);
localPassword = args.getString(REQUEST_PASSWORD);
@ -254,7 +255,7 @@ public class GenerateReviewFragment extends Fragment {
showProgress();
if ((walletPath != null)
&& (WalletManager.getInstance().queryWalletDevice(walletPath + ".keys", getPassword())
== Wallet.Device.Device_Ledger)
== Wallet.Device.Ledger)
&& (progressCallback != null)) {
progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE);
dialogOpened = true;
@ -286,10 +287,11 @@ public class GenerateReviewFragment extends Fragment {
height = wallet.getRestoreHeight();
seed = wallet.getSeed(getSeedOffset());
switch (wallet.getDeviceType()) {
case Device_Ledger:
case Ledger:
viewKey = Ledger.Key();
break;
case Device_Software:
case Software:
case Sidekick:
viewKey = wallet.getSecretViewKey();
break;
default:

View File

@ -22,13 +22,14 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import timber.log.Timber;
public class LockFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Timber.d("onCreateView");
final FrameLayout frame = new FrameLayout(requireContext());
frame.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

View File

@ -40,6 +40,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
@ -55,6 +56,7 @@ import com.m2049r.xmrwallet.ledger.LedgerProgressDialog;
import com.m2049r.xmrwallet.model.NetworkType;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.service.BluetoothService;
import com.m2049r.xmrwallet.service.WalletService;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.KeyStoreHelper;
@ -76,13 +78,12 @@ import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import lombok.Getter;
import timber.log.Timber;
public class LoginActivity extends BaseActivity
implements LoginFragment.Listener, GenerateFragment.Listener,
GenerateReviewFragment.Listener, GenerateReviewFragment.AcceptListener,
NodeFragment.Listener, SettingsFragment.Listener {
NodeFragment.Listener, SettingsFragment.Listener, SidekickConnectFragment.Listener, BluetoothFragment.Listener {
private static final String GENERATE_STACK = "gen";
private static final String NODES_PREFS_NAME = "nodes";
@ -278,8 +279,15 @@ public class LoginActivity extends BaseActivity
}
@Override
public boolean hasLedger() {
return Ledger.isConnected();
public boolean hasDevice(Wallet.Device type) {
switch (type) {
case Ledger:
return Ledger.isConnected();
case Sidekick:
return BluetoothService.IsConnected();
default:
return true;
}
}
@Override
@ -311,8 +319,8 @@ public class LoginActivity extends BaseActivity
}
});
loadFavouritesWithNetwork();
if (isNetworkAvailable())
loadFavouritesWithNetwork();
LegacyStorageHelper.migrateWallets(this);
if (savedInstanceState == null) startLoginFragment();
@ -334,7 +342,7 @@ public class LoginActivity extends BaseActivity
@Override
public boolean onWalletSelected(String walletName, boolean streetmode) {
if (node == null) {
if (isNetworkAvailable() && (node == null)) {
Toast.makeText(this, getString(R.string.prompt_daemon_missing), Toast.LENGTH_SHORT).show();
return false;
}
@ -611,6 +619,7 @@ public class LoginActivity extends BaseActivity
try {
GenerateReviewFragment detailsFragment = (GenerateReviewFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
assert detailsFragment != null;
AlertDialog dialog = detailsFragment.createChangePasswordDialog();
if (dialog != null) {
Helper.showKeyboard(dialog);
@ -682,6 +691,7 @@ public class LoginActivity extends BaseActivity
dismissProgressDialog();
unregisterDetachReceiver();
Ledger.disconnect();
BluetoothService.Stop();
super.onDestroy();
}
@ -695,6 +705,7 @@ public class LoginActivity extends BaseActivity
new AsyncWaitForService().execute();
}
if (!Ledger.isConnected()) attachLedger();
if (BluetoothService.IsConnected()) onLedgerAction(); //TODO sidekick & show sidekick fab
registerTor();
}
@ -727,14 +738,14 @@ public class LoginActivity extends BaseActivity
}
}
void startWallet(String walletName, String walletPassword,
boolean fingerprintUsed, boolean streetmode) {
void startWallet(String walletName, String walletPassword, boolean fingerprintUsed, StartMode mode) {
Timber.d("startWallet()");
Intent intent = new Intent(getApplicationContext(), WalletActivity.class);
intent.putExtra(WalletActivity.REQUEST_ID, walletName);
intent.putExtra(WalletActivity.REQUEST_PW, walletPassword);
intent.putExtra(WalletActivity.REQUEST_FINGERPRINT_USED, fingerprintUsed);
intent.putExtra(WalletActivity.REQUEST_STREETMODE, streetmode);
intent.putExtra(WalletActivity.REQUEST_STREETMODE, mode == StartMode.Street);
if (uri != null) {
intent.putExtra(WalletActivity.REQUEST_URI, uri);
uri = null; // use only once
@ -783,6 +794,11 @@ public class LoginActivity extends BaseActivity
Timber.d("SettingsFragment placed");
}
void startSidekickConnectFragment() {
replaceFragment(new SidekickConnectFragment(), null, null);
Timber.d("SidekickConnectFragment placed");
}
void replaceFragment(Fragment newFragment, String stackName, Bundle extras) {
if (extras != null) {
newFragment.setArguments(extras);
@ -821,7 +837,7 @@ public class LoginActivity extends BaseActivity
protected void onPreExecute() {
super.onPreExecute();
acquireWakeLock();
if (walletCreator.isLedger()) {
if (walletCreator.device() == Wallet.Device.Ledger) {
showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE);
} else {
showProgressDialog(R.string.generate_wallet_creating);
@ -881,6 +897,7 @@ public class LoginActivity extends BaseActivity
try {
GenerateFragment genFragment = (GenerateFragment)
getSupportFragmentManager().findFragmentById(R.id.fragment_container);
assert genFragment != null;
genFragment.walletGenerateError();
} catch (ClassCastException ex) {
Timber.e("walletGenerateError() but not in GenerateFragment");
@ -890,8 +907,7 @@ public class LoginActivity extends BaseActivity
interface WalletCreator {
boolean createWallet(File aFile, String password);
boolean isLedger();
Wallet.Device device();
}
boolean checkAndCloseWallet(Wallet aWallet) {
@ -909,8 +925,8 @@ public class LoginActivity extends BaseActivity
createWallet(name, password,
new WalletCreator() {
@Override
public boolean isLedger() {
return false;
public Wallet.Device device() {
return Wallet.Device.Software;
}
@Override
@ -933,8 +949,8 @@ public class LoginActivity extends BaseActivity
createWallet(name, password,
new WalletCreator() {
@Override
public boolean isLedger() {
return false;
public Wallet.Device device() {
return Wallet.Device.Software;
}
@Override
@ -947,20 +963,19 @@ public class LoginActivity extends BaseActivity
}
@Override
public void onGenerateLedger(final String name, final String password,
final long restoreHeight) {
public void onGenerateDevice(final Wallet.Device device, final String name, final String password, long restoreHeight) {
createWallet(name, password,
new WalletCreator() {
@Override
public boolean isLedger() {
return true;
public Wallet.Device device() {
return device;
}
@Override
public boolean createWallet(File aFile, String password) {
Wallet newWallet = WalletManager.getInstance()
.createWalletFromDevice(aFile, password,
restoreHeight, "Ledger");
restoreHeight, device);
return checkAndCloseWallet(newWallet);
}
});
@ -973,8 +988,8 @@ public class LoginActivity extends BaseActivity
createWallet(name, password,
new WalletCreator() {
@Override
public boolean isLedger() {
return false;
public Wallet.Device device() {
return Wallet.Device.Software;
}
@Override
@ -1105,6 +1120,9 @@ public class LoginActivity extends BaseActivity
} else if (id == R.id.action_create_help_ledger) {
HelpFragment.display(getSupportFragmentManager(), R.string.help_create_ledger);
return true;
} else if (id == R.id.action_create_help_sidekick) {
HelpFragment.display(getSupportFragmentManager(), R.string.help_create_sidekick);
return true;
} else if (id == R.id.action_details_help) {
HelpFragment.display(getSupportFragmentManager(), R.string.help_details);
return true;
@ -1117,6 +1135,9 @@ public class LoginActivity extends BaseActivity
} else if (id == R.id.action_help_node) {
HelpFragment.display(getSupportFragmentManager(), R.string.help_node);
return true;
} else if (id == R.id.action_help_sidekick) {
HelpFragment.display(getSupportFragmentManager(), R.string.help_sidekick);
return true;
} else if (id == R.id.action_default_nodes) {
Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if ((WalletManager.getInstance().getNetworkType() == NetworkType.NetworkType_Mainnet) &&
@ -1124,6 +1145,9 @@ public class LoginActivity extends BaseActivity
((NodeFragment) f).restoreDefaultNodes();
}
return true;
} else if (id == R.id.action_sidekick) {
checkBtPermissions();
return true;
} else if (id == R.id.action_ledger_seed) {
Fragment f = getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if (f instanceof GenerateFragment) {
@ -1135,6 +1159,11 @@ public class LoginActivity extends BaseActivity
}
}
@Override
void btPermissionGranted() {
startSidekickConnectFragment();
}
// an AsyncTask which tests the node before trying to open the wallet
private class AsyncOpenWallet extends AsyncTask<Void, Void, Boolean> {
final static int OK = 0;
@ -1172,7 +1201,7 @@ public class LoginActivity extends BaseActivity
if (result) {
Timber.d("selected wallet is .%s.", node.getName());
// now it's getting real, onValidateFields if wallet exists
promptAndStart(walletName, streetmode);
promptAndStart(walletName, streetmode ? StartMode.Street : StartMode.Normal);
} else {
if (node.getResponseCode() == 0) { // IOException
Toast.makeText(LoginActivity.this, getString(R.string.status_wallet_node_invalid), Toast.LENGTH_LONG).show();
@ -1184,25 +1213,25 @@ public class LoginActivity extends BaseActivity
}
boolean checkDevice(String walletName, String password) {
String keyPath = new File(Helper.getWalletRoot(LoginActivity.this),
walletName + ".keys").getAbsolutePath();
private boolean checkDevice(String walletName, String password) {
String keyPath = new File(Helper.getWalletRoot(this), walletName + ".keys").getAbsolutePath();
// check if we need connected hardware
Wallet.Device device = WalletManager.getInstance().queryWalletDevice(keyPath, password);
if (device == Wallet.Device.Device_Ledger) {
if (!hasLedger()) {
final Wallet.Device device = WalletManager.getInstance().queryWalletDevice(keyPath, password);
if (!hasDevice(device)) {
if (device == Wallet.Device.Ledger) {
toast(R.string.open_wallet_ledger_missing);
} else {
return true;
} else if (device == Wallet.Device.Sidekick) {
toast(R.string.open_wallet_sidekick_missing);
}
} else {// device could be undefined meaning the password is wrong
// this gets dealt with later
return true;
return false;
}
return false;
// else // device could be undefined meaning the password is wrong
return true;
}
void promptAndStart(String walletName, final boolean streetmode) {
enum StartMode {Normal, Street}
void promptAndStart(String walletName, final StartMode mode) {
File walletFile = Helper.getWalletFile(this, walletName);
if (WalletManager.getInstance().walletExists(walletFile)) {
Helper.promptPassword(LoginActivity.this, walletName, false,
@ -1210,7 +1239,7 @@ public class LoginActivity extends BaseActivity
@Override
public void act(String walletName, String password, boolean fingerprintUsed) {
if (checkDevice(walletName, password))
startWallet(walletName, password, fingerprintUsed, streetmode);
startWallet(walletName, password, fingerprintUsed, mode);
}
@Override
@ -1234,7 +1263,7 @@ public class LoginActivity extends BaseActivity
if (usbManager.hasPermission(device)) {
connectLedger(usbManager, device);
} else {
registerReceiver(usbPermissionReceiver, new IntentFilter(ACTION_USB_PERMISSION));
ContextCompat.registerReceiver(this, usbPermissionReceiver, new IntentFilter(ACTION_USB_PERMISSION), ContextCompat.RECEIVER_EXPORTED);
usbManager.requestPermission(device,
PendingIntent.getBroadcast(this, 0,
new Intent(ACTION_USB_PERMISSION),
@ -1339,7 +1368,7 @@ public class LoginActivity extends BaseActivity
private void registerDetachReceiver() {
detachReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(intent.getAction())) {
unregisterDetachReceiver();
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
Timber.i("Ledger detached!");
@ -1353,8 +1382,7 @@ public class LoginActivity extends BaseActivity
}
}
};
registerReceiver(detachReceiver, new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED));
ContextCompat.registerReceiver(this, detachReceiver, new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED), ContextCompat.RECEIVER_EXPORTED);
}
public void onLedgerAction() {
@ -1378,6 +1406,34 @@ public class LoginActivity extends BaseActivity
return usbManager;
}
@Override
public void onDeviceConnected(String connectedDeviceName) {
Timber.d("onDeviceConnected: %s", connectedDeviceName);
try {
SidekickConnectFragment f = (SidekickConnectFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_container);
if (f == null) return;
f.allowClick();
} catch (ClassCastException ex) {
// ignore it
}
if (connectedDeviceName != null) {
setSubtitle(getString(R.string.sidekick_connected));
} else {
setSubtitle(null);
}
}
@Override
public void abort(String message) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
onBackPressed();
}
@Override
public void onReceive(int command) {
Timber.e("this should not be");
}
//
// Tor (Orbot) stuff
//

View File

@ -46,6 +46,7 @@ import com.google.android.material.progressindicator.CircularProgressIndicator;
import com.m2049r.xmrwallet.data.NodeInfo;
import com.m2049r.xmrwallet.dialog.HelpFragment;
import com.m2049r.xmrwallet.layout.WalletInfoAdapter;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.KeyStoreHelper;
@ -61,6 +62,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import lombok.Getter;
import timber.log.Timber;
public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInteractionListener,
@ -115,7 +117,9 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
Set<NodeInfo> getOrPopulateFavourites();
boolean hasLedger();
boolean hasDevice(Wallet.Device type);
boolean isNetworkAvailable();
void runOnNetCipher(Runnable runnable);
}
@ -146,10 +150,9 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
activityCallback.setToolbarButton(Toolbar.BUTTON_SETTINGS);
activityCallback.showNet();
showNetwork();
//activityCallback.runOnNetCipher(this::pingSelectedNode);
}
private OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
animateFAB();
@ -172,6 +175,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
fabSeed = view.findViewById(R.id.fabSeed);
fabImport = view.findViewById(R.id.fabImport);
fabLedger = view.findViewById(R.id.fabLedger);
fabSidekick = view.findViewById(R.id.fabSidekick);
fabNewL = view.findViewById(R.id.fabNewL);
fabViewL = view.findViewById(R.id.fabViewL);
@ -179,6 +183,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
fabSeedL = view.findViewById(R.id.fabSeedL);
fabImportL = view.findViewById(R.id.fabImportL);
fabLedgerL = view.findViewById(R.id.fabLedgerL);
fabSidekickL = view.findViewById(R.id.fabSidekickL);
fab_pulse = AnimationUtils.loadAnimation(getContext(), R.anim.fab_pulse);
fab_open_screen = AnimationUtils.loadAnimation(getContext(), R.anim.fab_open_screen);
@ -194,6 +199,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
fabSeed.setOnClickListener(this);
fabImport.setOnClickListener(this);
fabLedger.setOnClickListener(this);
fabSidekick.setOnClickListener(this);
fabScreen.setOnClickListener(this);
RecyclerView recyclerView = view.findViewById(R.id.list);
@ -202,6 +208,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
recyclerView.setAdapter(adapter);
ViewGroup llNotice = view.findViewById(R.id.llNotice);
Notice.showAll(llNotice, ".*_login");
view.findViewById(R.id.llNode).setOnClickListener(v -> startNodePrefs());
@ -276,7 +283,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
}
// remove information of non-existent wallet
Set<String> removedWallets = getActivity()
Set<String> removedWallets = requireActivity()
.getSharedPreferences(KeyStoreHelper.SecurityConstants.WALLET_PASS_PREFS_NAME, Context.MODE_PRIVATE)
.getAll().keySet();
for (WalletManager.WalletInfo s : walletList) {
@ -304,17 +311,14 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
super.onCreateOptionsMenu(menu, inflater);
}
@Getter
private boolean fabOpen = false;
private FloatingActionButton fab, fabNew, fabView, fabKey, fabSeed, fabImport, fabLedger;
private FloatingActionButton fab, fabNew, fabView, fabKey, fabSeed, fabImport, fabLedger, fabSidekick;
private RelativeLayout fabScreen;
private RelativeLayout fabNewL, fabViewL, fabKeyL, fabSeedL, fabImportL, fabLedgerL;
private RelativeLayout fabNewL, fabViewL, fabKeyL, fabSeedL, fabImportL, fabLedgerL, fabSidekickL;
private Animation fab_open, fab_close, rotate_forward, rotate_backward, fab_open_screen, fab_close_screen;
private Animation fab_pulse;
public boolean isFabOpen() {
return fabOpen;
}
private void setFabOpen(boolean value) {
fabOpen = value;
onBackPressedCallback.setEnabled(value);
@ -328,6 +332,9 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
if (fabLedgerL.getVisibility() == View.VISIBLE) {
fabLedgerL.startAnimation(fab_close);
fabLedger.setClickable(false);
} else if (fabSidekickL.getVisibility() == View.VISIBLE) {
fabSidekickL.startAnimation(fab_close);
fabSidekick.setClickable(false);
} else {
fabNewL.startAnimation(fab_close);
fabNew.setClickable(false);
@ -345,17 +352,25 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
fabScreen.setClickable(true);
fabScreen.startAnimation(fab_open_screen);
fab.startAnimation(rotate_forward);
if (activityCallback.hasLedger()) {
fabLedgerL.setVisibility(View.VISIBLE);
if ((activityCallback.hasDevice(Wallet.Device.Ledger)
|| activityCallback.hasDevice(Wallet.Device.Sidekick))) {
fabNewL.setVisibility(View.GONE);
fabViewL.setVisibility(View.GONE);
fabKeyL.setVisibility(View.GONE);
fabSeedL.setVisibility(View.GONE);
fabImportL.setVisibility(View.GONE);
fabLedgerL.startAnimation(fab_open);
fabLedger.setClickable(true);
if (activityCallback.hasDevice(Wallet.Device.Ledger)) {
fabLedgerL.setVisibility(View.VISIBLE);
fabLedgerL.startAnimation(fab_open);
fabLedger.setClickable(true);
} else { // Sidekick
fabSidekickL.setVisibility(View.VISIBLE);
fabSidekickL.startAnimation(fab_open);
fabSidekick.setClickable(true);
}
} else {
fabSidekickL.setVisibility(View.GONE);
fabLedgerL.setVisibility(View.GONE);
fabNewL.setVisibility(View.VISIBLE);
fabViewL.setVisibility(View.VISIBLE);
@ -404,6 +419,10 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
Timber.d("FAB_LEDGER");
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_LEDGER);
} else if (id == R.id.fabSidekick) {
Timber.d("FAB_SIDEKICK");
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_SIDEKICK);
} else if (id == R.id.fabScreen) {
animateFAB();
}
@ -426,7 +445,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
}
private void setSubtext(String status) {
final Context ctx = getContext();
final Context ctx = requireContext();
final Spanned text = Html.fromHtml(ctx.getString(R.string.status,
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
Integer.toHexString(ThemeHelper.getThemedColor(ctx, android.R.attr.colorBackground) & 0xFFFFFF),

View File

@ -32,6 +32,7 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
@ -68,7 +69,7 @@ public class NodeFragment extends Fragment
static private int NODES_TO_FIND = 10;
static private NumberFormat FORMATTER = NumberFormat.getInstance();
static private final NumberFormat FORMATTER = NumberFormat.getInstance();
private SwipeRefreshLayout pullToRefresh;
private TextView tvPull;
@ -104,7 +105,7 @@ public class NodeFragment extends Fragment
}
@Override
public void onAttach(Context context) {
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof Listener) {
this.activityCallback = (Listener) context;
@ -146,7 +147,7 @@ public class NodeFragment extends Fragment
}
}
private OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
@Override
public void handleOnBackPressed() {
Toast.makeText(requireActivity(), getString(R.string.node_refresh_wait), Toast.LENGTH_LONG).show();
@ -210,7 +211,7 @@ public class NodeFragment extends Fragment
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.node_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@ -289,8 +290,7 @@ public class NodeFragment extends Fragment
} else if (params[0] == SCAN) {
// otherwise scan the network
Timber.d("scanning");
Set<NodeInfo> seedList = new HashSet<>();
seedList.addAll(nodeList);
Set<NodeInfo> seedList = new HashSet<>(nodeList);
nodeList.clear();
Timber.d("seed %d", seedList.size());
Dispatcher d = new Dispatcher(info -> publishProgress(info));
@ -455,7 +455,7 @@ public class NodeFragment extends Fragment
private void closeDialog() {
if (editDialog == null) throw new IllegalStateException();
Helper.hideKeyboardAlways(getActivity());
Helper.hideKeyboardAlways(requireActivity());
editDialog.dismiss();
editDialog = null;
NodeFragment.this.editDialog = null;

View File

@ -32,12 +32,10 @@ import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
@ -84,8 +82,6 @@ public class ReceiveFragment extends Fragment {
private ImageView ivQrCode;
private ImageView ivQrCodeFull;
private EditText etDummy;
private ImageButton bCopyAddress;
private MenuItem shareItem;
private Wallet wallet = null;
private boolean isMyWallet = false;
@ -116,11 +112,10 @@ public class ReceiveFragment extends Fragment {
tvQrCode = view.findViewById(R.id.tvQrCode);
ivQrCodeFull = view.findViewById(R.id.qrCodeFull);
etDummy = view.findViewById(R.id.etDummy);
bCopyAddress = view.findViewById(R.id.bCopyAddress);
etDummy.setRawInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
bCopyAddress.setOnClickListener(v -> copyAddress());
view.findViewById(R.id.bCopyAddress).setOnClickListener(v -> copyAddress());
evAmount.setOnNewAmountListener(xmr -> {
Timber.d("new amount = %s", xmr);
@ -211,8 +206,7 @@ public class ReceiveFragment extends Fragment {
inflater.inflate(R.menu.receive_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
shareItem = menu.findItem(R.id.menu_item_share);
shareItem.setOnMenuItemClickListener(item -> {
menu.findItem(R.id.menu_item_share).setOnMenuItemClickListener(item -> {
if (shareRequested) return true;
shareRequested = true;
if (!qrValid) {
@ -238,7 +232,7 @@ public class ReceiveFragment extends Fragment {
private boolean saveQrCode() {
if (!qrValid) throw new IllegalStateException("trying to save null qr code!");
File cachePath = new File(getActivity().getCacheDir(), "images");
File cachePath = new File(requireActivity().getCacheDir(), "images");
if (!cachePath.exists())
if (!cachePath.mkdirs()) throw new IllegalStateException("cannot create images folder");
File png = new File(cachePath, "QR.png");
@ -452,7 +446,7 @@ public class ReceiveFragment extends Fragment {
.withEndAction(resetSize).start();
}
subaddress = newSubaddress;
final Context context = getContext();
final Context context = requireContext();
Spanned label = Html.fromHtml(context.getString(R.string.receive_subaddress,
Integer.toHexString(ThemeHelper.getThemedColor(context, R.attr.positiveColor) & 0xFFFFFF),
Integer.toHexString(ThemeHelper.getThemedColor(context, android.R.attr.colorBackground) & 0xFFFFFF),

View File

@ -24,6 +24,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import com.google.zxing.BarcodeFormat;
@ -43,7 +44,7 @@ public class ScannerFragment extends Fragment implements ZXingScannerView.Result
private ZXingScannerView mScannerView;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Timber.d("onCreateView");
mScannerView = new ZXingScannerView(getActivity());
return mScannerView;
@ -85,7 +86,7 @@ public class ScannerFragment extends Fragment implements ZXingScannerView.Result
}
@Override
public void onAttach(Context context) {
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof OnScannedListener) {
this.onScannedListener = (OnScannedListener) context;

View File

@ -5,6 +5,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.StyleRes;
import androidx.preference.ListPreference;
import androidx.preference.PreferenceFragmentCompat;
@ -60,7 +61,7 @@ public class SettingsFragment extends PreferenceFragmentCompat
private SettingsFragment.Listener activity;
@Override
public void onAttach(Context context) {
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof SettingsFragment.Listener) {
activity = (SettingsFragment.Listener) context;

View File

@ -0,0 +1,188 @@
/*
* Copyright (c) 2018 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.
*/
package com.m2049r.xmrwallet;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.m2049r.xmrwallet.data.BluetoothInfo;
import com.m2049r.xmrwallet.layout.BluetoothInfoAdapter;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.util.ArrayList;
import java.util.List;
import timber.log.Timber;
public class SidekickConnectFragment extends Fragment
implements BluetoothInfoAdapter.OnInteractionListener {
private BluetoothAdapter bluetoothAdapter;
private SwipeRefreshLayout pullToRefresh;
private BluetoothInfoAdapter infoAdapter;
private Listener activityCallback;
public interface Listener {
void setToolbarButton(int type);
void setSubtitle(String title);
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof Listener) {
this.activityCallback = (Listener) context;
} else {
throw new ClassCastException(context + " must implement Listener");
}
}
@Override
public void onPause() {
Timber.d("onPause()");
if (ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(requireContext(), "Bluetooth permission not granted", Toast.LENGTH_LONG).show();
} else {
if (bluetoothAdapter.isDiscovering()) {
bluetoothAdapter.cancelDiscovery();
}
}
super.onPause();
}
@Override
public void onResume() {
super.onResume();
Timber.d("onResume()");
activityCallback.setSubtitle(getString(R.string.label_bluetooth));
activityCallback.setToolbarButton(Toolbar.BUTTON_BACK);
final BluetoothFragment btFragment = (BluetoothFragment) getChildFragmentManager().findFragmentById(R.id.bt_fragment);
assert btFragment != null;
btFragment.start();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView");
View view = inflater.inflate(R.layout.fragment_sidekick_connect, container, false);
RecyclerView recyclerView = view.findViewById(R.id.list);
infoAdapter = new BluetoothInfoAdapter(this);
recyclerView.setAdapter(infoAdapter);
pullToRefresh = view.findViewById(R.id.pullToRefresh);
pullToRefresh.setOnRefreshListener(() -> {
populateList();
pullToRefresh.setRefreshing(false);
});
return view;
}
private void populateList() {
List<BluetoothInfo> items = new ArrayList<>();
if (ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(requireContext(), "Bluetooth permission not granted", Toast.LENGTH_LONG).show();
return;
}
for (BluetoothDevice device : bluetoothAdapter.getBondedDevices()) {
final int deviceCLass = device.getBluetoothClass().getDeviceClass();
switch (deviceCLass) {
case BluetoothClass.Device.PHONE_SMART:
//TODO verify these are correct
case BluetoothClass.Device.COMPUTER_HANDHELD_PC_PDA:
case BluetoothClass.Device.COMPUTER_PALM_SIZE_PC_PDA:
items.add(new BluetoothInfo(device));
}
}
infoAdapter.setItems(items);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
Helper.hideKeyboard(getActivity());
// Get the local Bluetooth adapter
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
populateList();
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.sidekick_connect_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public void onDestroy() {
super.onDestroy();
// Make sure we're not doing discovery anymore
if (bluetoothAdapter != null) {
if (ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(requireContext(), "Bluetooth permission not granted", Toast.LENGTH_LONG).show();
return;
}
bluetoothAdapter.cancelDiscovery();
}
}
@Override
public void onInteraction(final View view, final BluetoothInfo item) {
Timber.d("onInteraction %s", item);
if (ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED)
throw new IllegalStateException("Bluetooth permission not granted");
bluetoothAdapter.cancelDiscovery();
final BluetoothFragment btFragment = (BluetoothFragment) getChildFragmentManager().findFragmentById(R.id.bt_fragment);
assert btFragment != null;
btFragment.connectDevice(item.getAddress());
}
public void allowClick() {
infoAdapter.allowClick(true);
}
}

View File

@ -195,7 +195,7 @@ public class SubaddressFragment extends Fragment implements SubaddressInfoAdapte
@Override
protected void onPreExecute() {
super.onPreExecute();
if ((wallet.getDeviceType() == Wallet.Device.Device_Ledger) && (progressCallback != null)) {
if ((wallet.getDeviceType() == Wallet.Device.Ledger) && (progressCallback != null)) {
progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_SUBADDRESS);
dialogOpened = true;
}
@ -226,7 +226,7 @@ public class SubaddressFragment extends Fragment implements SubaddressInfoAdapte
// Callbacks from SubaddressInfoAdapter
@Override
public void onInteraction(final View view, final Subaddress subaddress) {
public void onInteraction(final View view, @NonNull final Subaddress subaddress) {
if (managerMode)
activityCallback.showSubaddress(view, subaddress.getAddressIndex());
else

View File

@ -52,7 +52,6 @@ public class SubaddressInfoFragment extends Fragment
private Subaddress subaddress;
private TextInputLayout etName;
private TextView tvAddress;
private TextView tvTxLabel;
@Override
@ -61,7 +60,6 @@ public class SubaddressInfoFragment extends Fragment
View view = inflater.inflate(R.layout.fragment_subaddressinfo, container, false);
etName = view.findViewById(R.id.etName);
tvAddress = view.findViewById(R.id.tvAddress);
tvTxLabel = view.findViewById(R.id.tvTxLabel);
final RecyclerView list = view.findViewById(R.id.list);
@ -71,11 +69,13 @@ public class SubaddressInfoFragment extends Fragment
final Wallet wallet = activityCallback.getWallet();
Bundle b = getArguments();
assert b != null;
final int subaddressIndex = b.getInt("subaddressIndex");
subaddress = wallet.getSubaddressObject(subaddressIndex);
etName.getEditText().setText(subaddress.getDisplayLabel());
tvAddress.setText(getContext().getString(R.string.subbaddress_info_subtitle,
final TextView tvAddress = view.findViewById(R.id.tvAddress);
tvAddress.setText(requireContext().getString(R.string.subbaddress_info_subtitle,
subaddress.getAddressIndex(), subaddress.getAddress()));
etName.getEditText().setOnFocusChangeListener((v, hasFocus) -> {

View File

@ -20,7 +20,6 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.text.InputType;
@ -34,6 +33,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.transition.Transition;
@ -44,6 +44,8 @@ import com.m2049r.xmrwallet.data.UserNotes;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.model.Transfer;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.ShiftApi;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.ThemeHelper;
import com.m2049r.xmrwallet.widget.Toolbar;
@ -61,6 +63,7 @@ public class TxFragment extends Fragment {
static public final String ARG_INFO = "info";
@SuppressLint("SimpleDateFormat")
private final SimpleDateFormat TS_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
public TxFragment() {
@ -106,6 +109,7 @@ public class TxFragment extends Fragment {
tvTxAmountBtc = view.findViewById(R.id.tvTxAmountBtc);
tvXmrToSupport = view.findViewById(R.id.tvXmrToSupport);
tvXmrToKeyLabel = view.findViewById(R.id.tvXmrToKeyLabel);
tvXmrToLogo = view.findViewById(R.id.tvXmrToLogo);
tvAccount = view.findViewById(R.id.tvAccount);
@ -127,18 +131,20 @@ public class TxFragment extends Fragment {
tvWarning = view.findViewById(R.id.tvWarning);
tvTxXmrToKey.setOnClickListener(v -> {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
});
info = getArguments().getParcelable(ARG_INFO);
final Bundle args = getArguments();
assert args != null;
info = args.getParcelable(ARG_INFO);
show();
return view;
}
void shareTxInfo() {
if (this.info == null) return;
StringBuffer sb = new StringBuffer();
StringBuilder sb = new StringBuilder();
sb.append(getString(R.string.tx_timestamp)).append(":\n");
sb.append(TS_FORMATTER.format(new Date(info.timestamp * 1000))).append("\n\n");
@ -216,7 +222,7 @@ public class TxFragment extends Fragment {
private void showSubaddressLabel() {
final Subaddress subaddress = activityCallback.getWalletSubaddress(info.accountIndex, info.addressIndex);
final Context ctx = getContext();
final Context ctx = requireContext();
Spanned label = Html.fromHtml(ctx.getString(R.string.tx_account_formatted,
info.accountIndex, info.addressIndex,
Integer.toHexString(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor) & 0xFFFFFF),
@ -264,16 +270,17 @@ public class TxFragment extends Fragment {
tvTxFee.setVisibility(View.GONE);
}
final Context ctx = requireContext();
if (info.isFailed) {
tvTxAmount.setText(getString(R.string.tx_list_amount_failed, Wallet.getDisplayAmount(info.amount)));
tvTxFee.setText(getString(R.string.tx_list_failed_text));
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor));
setTxColour(ThemeHelper.getThemedColor(ctx, R.attr.neutralColor));
} else if (info.isPending) {
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor));
setTxColour(ThemeHelper.getThemedColor(ctx, R.attr.neutralColor));
} else if (info.direction == TransactionInfo.Direction.Direction_In) {
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.positiveColor));
setTxColour(ThemeHelper.getThemedColor(ctx, R.attr.positiveColor));
} else {
setTxColour(ThemeHelper.getThemedColor(getContext(), R.attr.negativeColor));
setTxColour(ThemeHelper.getThemedColor(ctx, R.attr.negativeColor));
}
Set<String> destinations = new HashSet<>();
StringBuilder sb = new StringBuilder();
@ -328,31 +335,29 @@ public class TxFragment extends Fragment {
void showBtcInfo() {
if (userNotes.xmrtoKey != null) {
cvXmrTo.setVisibility(View.VISIBLE);
String key = userNotes.xmrtoKey;
if ("xmrto".equals(userNotes.xmrtoTag)) { // legacy xmr.to service :(
key = "xmrto-" + key;
}
tvTxXmrToKey.setText(key);
ShiftService service = ShiftService.findWithTag(userNotes.xmrtoTag);
tvXmrToKeyLabel.setText(getString(R.string.label_send_btc_xmrto_key_lb, service.getLabel()));
if (service.getIconId() == 0)
tvXmrToLogo.setVisibility(View.GONE);
else
tvXmrToLogo.setImageResource(service.getLogoId());
tvTxXmrToKey.setText(userNotes.xmrtoKey);
tvDestinationBtc.setText(userNotes.xmrtoDestination);
tvTxAmountBtc.setText(userNotes.xmrtoAmount + " " + userNotes.xmrtoCurrency);
switch (userNotes.xmrtoTag) {
case "xmrto":
tvXmrToSupport.setVisibility(View.GONE);
tvXmrToKeyLabel.setVisibility(View.INVISIBLE);
tvXmrToLogo.setImageResource(R.drawable.ic_xmrto_logo);
break;
case "side": // defaults in layout - just add underline
tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
tvXmrToSupport.setOnClickListener(v -> {
Uri uri = Uri.parse("https://sideshift.ai/orders/" + userNotes.xmrtoKey);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);
});
break;
default:
tvXmrToSupport.setVisibility(View.GONE);
tvXmrToKeyLabel.setVisibility(View.INVISIBLE);
tvXmrToLogo.setVisibility(View.GONE);
ShiftApi shiftApi = service.getShiftApi();
if (shiftApi != null) {
tvXmrToSupport.setText(getString(R.string.label_send_btc_xmrto_info, service.getLabel()));
tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
tvXmrToSupport.setOnClickListener(v -> {
startActivity(new Intent(Intent.ACTION_VIEW, shiftApi.getQueryOrderUri(userNotes.xmrtoKey)));
});
} else {
tvXmrToSupport.setVisibility(View.GONE);
tvXmrToKeyLabel.setVisibility(View.INVISIBLE);
}
} else {
cvXmrTo.setVisibility(View.GONE);
@ -369,7 +374,7 @@ public class TxFragment extends Fragment {
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.tx_info_menu, menu);
super.onCreateOptionsMenu(menu, inflater);
}
@ -397,7 +402,7 @@ public class TxFragment extends Fragment {
}
@Override
public void onAttach(Context context) {
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof TxFragment.Listener) {
this.activityCallback = (TxFragment.Listener) context;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2017 m2049r
* Copyright (c) 2017-2024 m2049r
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -38,13 +38,13 @@ import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.PreferenceManager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@ -92,6 +92,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
public static final String REQUEST_FINGERPRINT_USED = "fingerprint";
public static final String REQUEST_STREETMODE = "streetmode";
public static final String REQUEST_URI = "uri";
public static final String REQUEST_SIDEKICK = "sidekick";
private NavigationView accountsView;
private DrawerLayout drawer;
@ -202,19 +203,9 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
return getWallet().getSubaddress(major, minor);
}
private void startWalletService() {
Bundle extras = getIntent().getExtras();
if (extras != null) {
acquireWakeLock();
String walletId = extras.getString(REQUEST_ID);
// we can set the streetmode height AFTER opening the wallet
requestStreetMode = extras.getBoolean(REQUEST_STREETMODE);
password = extras.getString(REQUEST_PW);
uri = extras.getString(REQUEST_URI);
connectWalletService(walletId, password);
} else {
finish();
}
private void startWalletService(String walletId) {
acquireWakeLock();
connectWalletService(walletId, password);
}
private void stopWalletService() {
@ -330,7 +321,6 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
});
}
public void onWalletChangePassword() {
try {
GenerateReviewFragment detailsFragment = (GenerateReviewFragment) getCurrentFragment();
@ -356,33 +346,42 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
return;
}
Bundle extras = getIntent().getExtras();
if (extras == null) {
finish(); // we need extras!
return;
}
String walletId = extras.getString(REQUEST_ID);
requestStreetMode = extras.getBoolean(REQUEST_STREETMODE);
password = extras.getString(REQUEST_PW);
uri = extras.getString(REQUEST_URI);
boolean sidekick = extras.getBoolean(REQUEST_SIDEKICK);
setContentView(R.layout.activity_wallet);
toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayShowTitleEnabled(false);
toolbar.setOnButtonListener(new Toolbar.OnButtonListener() {
@Override
public void onButton(int type) {
switch (type) {
case Toolbar.BUTTON_BACK:
onDisposeRequest();
getOnBackPressedDispatcher().onBackPressed();
break;
case Toolbar.BUTTON_CANCEL:
onDisposeRequest();
Helper.hideKeyboard(WalletActivity.this);
getOnBackPressedDispatcher().onBackPressed();
break;
case Toolbar.BUTTON_CLOSE:
finish();
break;
case Toolbar.BUTTON_SETTINGS:
Toast.makeText(WalletActivity.this, getString(R.string.label_credits), Toast.LENGTH_SHORT).show();
case Toolbar.BUTTON_NONE:
default:
Timber.e("Button " + type + "pressed - how can this be?");
}
toolbar.setOnButtonListener(type -> {
switch (type) {
case Toolbar.BUTTON_BACK:
onDisposeRequest();
getOnBackPressedDispatcher().onBackPressed();
break;
case Toolbar.BUTTON_CANCEL:
onDisposeRequest();
Helper.hideKeyboard(WalletActivity.this);
getOnBackPressedDispatcher().onBackPressed();
break;
case Toolbar.BUTTON_CLOSE:
finish();
break;
case Toolbar.BUTTON_SETTINGS:
Toast.makeText(WalletActivity.this, getString(R.string.label_credits), Toast.LENGTH_SHORT).show();
case Toolbar.BUTTON_NONE:
default:
Timber.e("Button " + type + "pressed - how can this be?");
}
});
@ -398,11 +397,16 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
showNet();
Fragment walletFragment = new WalletFragment();
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, walletFragment, WalletFragment.class.getName()).commit();
Timber.d("fragment added");
final FragmentTransaction tx =
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, walletFragment, WalletFragment.class.getName());
if (sidekick) {
tx.add(R.id.fragment_bluetooth, new BluetoothFragment(BluetoothFragment.Mode.CLIENT), BluetoothFragment.class.getName());
}
tx.commit();
Timber.d("fragments added");
startWalletService();
if (!sidekick) startWalletService(walletId);
Timber.d("onCreate() done.");
}
@ -616,7 +620,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
@Override
public void onWalletOpen(final Wallet.Device device) {
if (device == Wallet.Device.Device_Ledger) {
if (device == Wallet.Device.Ledger) {
runOnUiThread(() -> showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE));
}
}
@ -766,7 +770,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
intent.putExtra(WalletService.REQUEST_CMD_TX_TAG, tag);
startService(intent);
Timber.d("CREATE TX request sent");
if (getWallet().getDeviceType() == Wallet.Device.Device_Ledger)
if (getWallet().getDeviceType() == Wallet.Device.Ledger)
showLedgerProgressDialog(LedgerProgressDialog.TYPE_SEND);
} else {
Timber.e("Service not bound");
@ -1132,11 +1136,12 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
protected void onPreExecute() {
super.onPreExecute();
switch (getWallet().getDeviceType()) {
case Device_Ledger:
case Ledger:
showLedgerProgressDialog(LedgerProgressDialog.TYPE_ACCOUNT);
dialogOpened = true;
break;
case Device_Software:
case Software:
case Sidekick:
showProgressDialog(R.string.accounts_progress_new);
dialogOpened = true;
break;
@ -1176,7 +1181,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste
}
@Override
public void onSubaddressSelected(@Nullable final Subaddress subaddress) {
public void onSubaddressSelected(@NonNull final Subaddress subaddress) {
selectedSubaddressIndex = subaddress.getAddressIndex();
getOnBackPressedDispatcher().onBackPressed();
}

View File

@ -112,7 +112,7 @@ public class WalletFragment extends Fragment
flExchange = view.findViewById(R.id.flExchange);
((ProgressBar) view.findViewById(R.id.pbExchange)).getIndeterminateDrawable().
setColorFilter(
ThemeHelper.getThemedColor(getContext(), com.google.android.material.R.attr.colorPrimaryVariant),
ThemeHelper.getThemedColor(requireContext(), com.google.android.material.R.attr.colorPrimaryVariant),
android.graphics.PorterDuff.Mode.MULTIPLY);
tvProgress = view.findViewById(R.id.tvProgress);

View File

@ -22,27 +22,19 @@ import android.content.res.Configuration;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStateManagerControl;
import com.m2049r.xmrwallet.model.NetworkType;
import com.m2049r.xmrwallet.util.LocaleHelper;
import com.m2049r.xmrwallet.util.NetCipherHelper;
import com.m2049r.xmrwallet.util.NightmodeHelper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import java.util.Arrays;
import timber.log.Timber;
public class XmrWalletApplication extends Application {
@Override
@OptIn(markerClass = FragmentStateManagerControl.class)
public void onCreate() {
super.onCreate();
FragmentManager.enableNewStateManager(false);
if (BuildConfig.DEBUG) {
Timber.plant(new Timber.DebugTree());
}

View File

@ -24,43 +24,38 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.Getter;
import lombok.ToString;
import timber.log.Timber;
@Getter
@ToString
public class BarcodeData {
public enum Security {
NORMAL,
OA_NO_DNSSEC,
OA_DNSSEC
}
final public Crypto asset;
final public List<Crypto> ambiguousAssets;
final public String address;
final public String addressName;
final public String amount;
final public String description;
final public Security security;
final private Set<Crypto> possibleAssets = new HashSet<>();
private String address = null;
private String addressName = null;
private String amount = null;
private String description = null;
private Security security = null;
public BarcodeData(List<Crypto> assets, String address) {
if (assets.isEmpty())
throw new IllegalArgumentException("no assets specified");
this.addressName = null;
this.description = null;
this.amount = null;
this.security = Security.NORMAL;
security = Security.NORMAL;
this.address = address;
if (assets.size() == 1) {
this.asset = assets.get(0);
this.ambiguousAssets = null;
} else {
this.asset = null;
this.ambiguousAssets = assets;
}
possibleAssets.addAll(assets);
}
public BarcodeData(Crypto asset, String address, String description, String amount) {
@ -68,8 +63,7 @@ public class BarcodeData {
}
public BarcodeData(Crypto asset, String address, String addressName, String description, String amount, Security security) {
this.ambiguousAssets = null;
this.asset = asset;
possibleAssets.add(asset);
this.address = address;
this.addressName = addressName;
this.description = description;
@ -82,7 +76,7 @@ public class BarcodeData {
}
public String getUriString() {
if (asset != Crypto.XMR) throw new IllegalStateException("We can only do XMR stuff!");
if (getAsset() != Crypto.XMR) throw new IllegalStateException("We can only do XMR stuff!");
StringBuilder sb = new StringBuilder();
sb.append(Crypto.XMR.getUriScheme())
.append(':')
@ -227,6 +221,20 @@ public class BarcodeData {
}
public boolean isAmbiguous() {
return ambiguousAssets != null;
return possibleAssets.size() > 1;
}
public Crypto getAsset() {
if (possibleAssets.size() == 1) {
return possibleAssets.iterator().next();
} else {
return null;
}
}
// return true if we still have possible assets
public boolean filter(Set<Crypto> assets) {
possibleAssets.retainAll(assets);
return !possibleAssets.isEmpty();
}
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) 2018 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.
*/
package com.m2049r.xmrwallet.data;
import android.bluetooth.BluetoothDevice;
import java.util.Comparator;
import lombok.Data;
import lombok.Getter;
@Data
public class BluetoothInfo {
@Getter
final private String name;
@Getter
final private String address;
@Getter
private boolean bonded;
public BluetoothInfo(BluetoothDevice device) {
name = device.getName().trim();
address = device.getAddress();
bonded = device.getBondState() == BluetoothDevice.BOND_BONDED;
}
static public Comparator<BluetoothInfo> NameComparator = (o1, o2) -> o1.name.compareTo(o2.name);
}

View File

@ -1,3 +1,19 @@
/*
* Copyright (c) 2024 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.
*/
package com.m2049r.xmrwallet.data;
import androidx.annotation.NonNull;
@ -5,35 +21,30 @@ import androidx.annotation.Nullable;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.validator.BitcoinAddressType;
import com.m2049r.xmrwallet.util.validator.BitcoinAddressValidator;
import com.m2049r.xmrwallet.util.validator.EthAddressValidator;
import java.util.regex.Pattern;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum Crypto {
XMR("XMR", true, "monero:tx_amount:recipient_name:tx_description", R.id.ibXMR, R.drawable.ic_monero, R.drawable.ic_monero_bw, Wallet::isAddressValid),
BTC("BTC", true, "bitcoin:amount:label:message", R.id.ibBTC, R.drawable.ic_xmrto_btc, R.drawable.ic_xmrto_btc_off, address -> {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.BTC);
}),
DASH("DASH", true, "dash:amount:label:message", R.id.ibDASH, R.drawable.ic_xmrto_dash, R.drawable.ic_xmrto_dash_off, address -> {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.DASH);
}),
DOGE("DOGE", true, "dogecoin:amount:label:message", R.id.ibDOGE, R.drawable.ic_xmrto_doge, R.drawable.ic_xmrto_doge_off, address -> {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.DOGE);
}),
ETH("ETH", false, "ethereum:amount:label:message", R.id.ibETH, R.drawable.ic_xmrto_eth, R.drawable.ic_xmrto_eth_off, EthAddressValidator::validate),
LTC("LTC", true, "litecoin:amount:label:message", R.id.ibLTC, R.drawable.ic_xmrto_ltc, R.drawable.ic_xmrto_ltc_off, address -> {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.LTC);
});
XMR("XMR", "XMR", "XMR", "monero:tx_amount:recipient_name:tx_description", R.id.ibXMR, R.drawable.ic_monero, R.drawable.ic_monero_bw, Pattern.compile("^[48][a-zA-Z|\\d]{94}([a-zA-Z|\\d]{11})?$")),
BTC("BTC", "BTC", "BTC", "bitcoin:amount:label:message", R.id.ibBTC, R.drawable.ic_xmrto_btc, R.drawable.ic_xmrto_btc_off, Pattern.compile("^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$|^(bc1q)|(bc1p)[0-9A-Za-z]{37,62}$")),
LTC("LTC", "LTC", "LTC", "litecoin:amount:label:message", R.id.ibLTC, R.drawable.ic_xmrto_ltc, R.drawable.ic_xmrto_ltc_off, Pattern.compile("^([LM3])[A-Za-z0-9]{33}$|^(ltc1)[0-9A-Za-z]{39}$")),
ETH("ETH", "ETH", "ETH", "ethereum:amount:label:message", R.id.ibETH, R.drawable.ic_xmrto_eth, R.drawable.ic_xmrto_eth_off, Pattern.compile("^(0x)[0-9A-Fa-f]{40}$")),
USDT("USDT", "TRX", "USDT(TRC20)", "usdt:amount:label:message", R.id.ibUSDT, R.drawable.ic_xmrto_usdt_trc20, R.drawable.ic_xmrto_usdt_trc20_off, Pattern.compile("^T[1-9A-HJ-NP-Za-km-z]{33}$")),
SOLANA("SOL", "SOL", "SOL", "solana:amount:label:message", R.id.ibSOL, R.drawable.ic_xmrto_sol, R.drawable.ic_xmrto_sol_off, Pattern.compile("^[1-9A-HJ-NP-Za-km-z]{32,44}$"));
@Getter
@NonNull
private final String symbol;
@Getter
private final boolean casefull;
@NonNull
private final String network;
@Getter
@NonNull
private final String label;
@NonNull
private final String uriSpec;
@Getter
@ -43,7 +54,7 @@ public enum Crypto {
@Getter
private final int iconDisabledId;
@NonNull
private final Validator validator;
private final Pattern regex;
@Nullable
public static Crypto withScheme(@NonNull String scheme) {
@ -62,10 +73,6 @@ public enum Crypto {
return null;
}
interface Validator {
boolean validate(String address);
}
// TODO maybe cache these segments
String getUriScheme() {
return uriSpec.split(":")[0];
@ -83,7 +90,8 @@ public enum Crypto {
return uriSpec.split(":")[3];
}
boolean validate(String address) {
return validator.validate(address);
public boolean validate(String address) {
if (this == XMR) return Wallet.isAddressValid(address);
return regex.matcher(address).find();
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (c) 2024 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.
*/
package com.m2049r.xmrwallet.data;
import lombok.Value;
@Value
public class CryptoAmount {
Crypto crypto;
double amount;
public CryptoAmount newWithAmount(double amount) {
return new CryptoAmount(this.crypto, amount);
}
}

View File

@ -19,23 +19,26 @@ package com.m2049r.xmrwallet.data;
import lombok.AllArgsConstructor;
import lombok.Getter;
// Nodes stolen from https://moneroworld.com/#nodes
@Getter
@AllArgsConstructor
public enum DefaultNodes {
MONERUJO("nodex.monerujo.io:18081"),
XMRTO("node.xmr.to:18081"),
SUPPORTXMR("node.supportxmr.com:18081"),
HASHVAULT("nodes.hashvault.pro:18081"),
MONEROWORLD("node.moneroworld.com:18089"),
XMRTW("opennode.xmr-tw.org:18089"),
ds_jetzt("monero.ds-jetzt.de:18089"),
MONERUJO_ONION("monerujods7mbghwe6cobdr6ujih6c22zu5rl7zshmizz2udf7v7fsad.onion:18081/mainnet/monerujo.onion"),
Criminales78("56wl7y2ebhamkkiza4b7il4mrzwtyvpdym7bm2bkg3jrei2je646k3qd.onion:18089/mainnet/Criminales78.onion"),
xmrfail("mxcd4577fldb3ppzy7obmmhnu3tf57gbcbd4qhwr2kxyjj2qi3dnbfqd.onion:18081/mainnet/xmrfail.onion"),
AGORIST("xmr.agor.ist:18089/mainnet/agor.ist"),
BOLDSUCK("xmr-de.boldsuck.org:18080/mainnet/boldsuck.org"),
boldsuck("6dsdenp6vjkvqzy4wzsnzn6wixkdzihx3khiumyzieauxuxslmcaeiad.onion:18081/mainnet/boldsuck.onion"),
ds_jetzt_onion("qvlr4w7yhnjrdg3txa72jwtpnjn4ezsrivzvocbnvpfbdo342fahhoad.onion:18089/mainnet/ds-jetzt.onion");
CAKE("xmr-node.cakewallet.com:18081/mainnet/cakewallet.com"),
DS_JETZT("monero.ds-jetzt.de:18089/mainnet/ds-jetzt.de"),
ds_jetzt("qvlr4w7yhnjrdg3txa72jwtpnjn4ezsrivzvocbnvpfbdo342fahhoad.onion:18089/mainnet/ds-jetzt.onion"),
MONERODEVS("node.monerodevs.org:18089/mainnet/monerodevs.org"),
MONERUJO("nodex.monerujo.io:18081/mainnet/monerujo.io"),
monerujo("monerujods7mbghwe6cobdr6ujih6c22zu5rl7zshmizz2udf7v7fsad.onion:18081/mainnet/monerujo.onion"),
SETH("node.sethforprivacy.com:18089/mainnet/sethforprivacy.com"),
seth("sfpp2p7wnfjv3lrvfan4jmmkvhnbsbimpa3cqyuf7nt6zd24xhcqcsyd.onion/mainnet/sethforprivacy.onion"),
STACK("monero.stackwallet.com:18081/mainnet/stackwallet.com"),
STORMYCLOUD("xmr.stormycloud.org:18089/mainnet/stormycloud.org"),
TENZ("monero.10z.com.ar:18089/mainnet/10z.com.ar"),
XMRROCKS("node.xmr.rocks:18089/mainnet/xmr.rocks"),
xmrrocks("xqnnz2xmlmtpy2p4cm4cphg2elkwu5oob7b7so5v4wwgt44p6vbx5ryd.onion/mainnet/xmr.rocks.onion"),
XMRTW("opennode.xmr-tw.org:18089/mainnet/xmr-tw.org");
@Getter
private final String uri;
}

View File

@ -144,7 +144,7 @@ public class Node {
if ((nodeString == null) || nodeString.isEmpty())
throw new IllegalArgumentException("daemon is empty");
String daemonAddress;
String a[] = nodeString.split("@");
String[] a = nodeString.split("@");
if (a.length == 1) { // no credentials
daemonAddress = a[0];
username = "";
@ -169,7 +169,7 @@ public class Node {
throw new IllegalArgumentException("Too many '/' or too few");
daemonAddress = daParts[0];
String da[] = daemonAddress.split(":");
String[] da = daemonAddress.split(":");
if ((da.length > 2) || (da.length < 1))
throw new IllegalArgumentException("Too many ':' or too few");
String host = da[0];

View File

@ -201,8 +201,13 @@ public class NodeInfo extends Node {
.port(port)
.addPathSegment("json_rpc")
.build();
final String json = "{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getlastblockheader\"}";
return new Request(url, json, getUsername(), getPassword());
try {
final JSONObject json = new JSONObject("{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getlastblockheader\"}");
return new Request(url, json, getUsername(), getPassword());
} catch (JSONException ex) {
throw new IllegalStateException(ex);
}
}
private boolean testRpcService(int port) {

View File

@ -20,22 +20,21 @@ import android.os.Parcel;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class TxDataBtc extends TxData {
@Getter
@Setter
private ShiftService shiftService;
private String btcSymbol; // the actual non-XMR thing we're sending
@Getter
@Setter
private String xmrtoOrderId; // shown in success screen
@Getter
@Setter
private String btcAddress;
@Getter
@Setter
private double btcAmount;
private CryptoAmount shiftAmount; // what we want to send
private String xmrtoQueryOrderToken; // used for queryOrder API
public TxDataBtc() {
super();
@ -47,7 +46,9 @@ public class TxDataBtc extends TxData {
out.writeString(btcSymbol);
out.writeString(xmrtoOrderId);
out.writeString(btcAddress);
out.writeDouble(btcAmount);
out.writeString(shiftAmount.getCrypto().name());
out.writeDouble(shiftAmount.getAmount());
out.writeString(xmrtoQueryOrderToken);
}
// this is used to regenerate your object. All Parcelables must have a CREATOR that implements these two methods
@ -66,7 +67,8 @@ public class TxDataBtc extends TxData {
btcSymbol = in.readString();
xmrtoOrderId = in.readString();
btcAddress = in.readString();
btcAmount = in.readDouble();
shiftAmount = new CryptoAmount(Crypto.valueOf(in.readString()), in.readDouble());
xmrtoQueryOrderToken = in.readString();
}
@NonNull
@ -79,19 +81,33 @@ public class TxDataBtc extends TxData {
sb.append(btcSymbol);
sb.append(",btcAddress:");
sb.append(btcAddress);
sb.append(",btcAmount:");
sb.append(btcAmount);
sb.append(",amount:");
sb.append(shiftAmount);
sb.append(",xmrtoQueryOrderToken:");
sb.append(xmrtoQueryOrderToken);
return sb.toString();
}
public boolean validateAddress(@NonNull String address) {
if ((btcSymbol == null) || (btcAddress == null)) return false;
final Crypto crypto = Crypto.withSymbol(btcSymbol);
if (crypto == null) return false;
if (crypto.isCasefull()) { // compare as-is
return address.equals(btcAddress);
} else { // normalize & compare (e.g. ETH with and without checksum capitals
return address.toLowerCase().equals(btcAddress.toLowerCase());
return address.equalsIgnoreCase(btcAddress);
}
public double getBtcAmount() {
return (shiftAmount.getCrypto() == Crypto.XMR) ? 0 : shiftAmount.getAmount();
}
public double getXmrAmount() {
return (shiftAmount.getCrypto() == Crypto.XMR) ? shiftAmount.getAmount() : 0;
}
public boolean validate(RequestQuote quote) {
if (shiftAmount.getCrypto() == Crypto.XMR) {
return (quote.getXmrAmount() == getXmrAmount());
} else {
return (quote.getBtcAmount() == getBtcAmount());
}
}
}

View File

@ -16,7 +16,7 @@
package com.m2049r.xmrwallet.data;
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
import com.m2049r.xmrwallet.util.Helper;
import java.util.regex.Matcher;
@ -61,7 +61,7 @@ public class UserNotes {
public void setXmrtoOrder(CreateOrder order) {
if (order != null) {
xmrtoTag = order.TAG;
xmrtoTag = order.getTag();
xmrtoKey = order.getOrderId();
xmrtoAmount = Helper.getDisplayAmount(order.getBtcAmount());
xmrtoCurrency = order.getBtcCurrency();

View File

@ -20,10 +20,10 @@ import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@ -57,13 +57,14 @@ public class AboutFragment extends DialogFragment {
AboutFragment.newInstance().show(ft, TAG);
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_about, null);
final View view = getLayoutInflater().inflate(R.layout.fragment_about, null);
((TextView) view.findViewById(R.id.tvHelp)).setText(Html.fromHtml(getLicencesHtml()));
((TextView) view.findViewById(R.id.tvVersion)).setText(getString(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity())
.setView(view)
.setNegativeButton(R.string.about_close,
new DialogInterface.OnClickListener() {
@ -77,7 +78,7 @@ public class AboutFragment extends DialogFragment {
private String getLicencesHtml() {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(getContext().getAssets().open("licenses.html"), StandardCharsets.UTF_8))) {
new InputStreamReader(requireContext().getAssets().open("licenses.html"), StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null)

View File

@ -20,10 +20,10 @@ import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@ -49,13 +49,14 @@ public class CreditsFragment extends DialogFragment {
CreditsFragment.newInstance().show(ft, TAG);
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_credits, null);
final View view = getLayoutInflater().inflate(R.layout.fragment_credits, null);
((TextView) view.findViewById(R.id.tvCredits)).setText(Html.fromHtml(getString(R.string.credits_text)));
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity())
.setView(view)
.setNegativeButton(R.string.about_close,
new DialogInterface.OnClickListener() {

View File

@ -16,13 +16,13 @@
package com.m2049r.xmrwallet.dialog;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.text.Html;
import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
@ -65,9 +65,10 @@ public class HelpFragment extends DialogFragment {
private Spanned getHtml(String html, double textSize) {
final Html.ImageGetter imageGetter = source -> {
final int imageId = getResources().getIdentifier(source.replace("/", ""), "drawable", requireActivity().getPackageName());
@SuppressLint("DiscouragedApi") final int imageId = getResources().getIdentifier(source.replace("/", ""), "drawable", requireActivity().getPackageName());
// Don't die if we don't find the image - use a heart instead
final Drawable drawable = ContextCompat.getDrawable(requireActivity(), imageId > 0 ? imageId : R.drawable.ic_favorite_24dp);
assert drawable != null;
final double f = textSize / drawable.getIntrinsicHeight();
drawable.setBounds(0, 0, (int) (f * drawable.getIntrinsicWidth()), (int) textSize);
return drawable;
@ -82,7 +83,7 @@ public class HelpFragment extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_help, null);
final View view = getLayoutInflater().inflate(R.layout.fragment_help, null);
int helpId = 0;
boolean torButton = false;
@ -100,7 +101,7 @@ public class HelpFragment extends DialogFragment {
.setView(view);
if (torButton) {
builder.setNegativeButton(R.string.help_nok,
(dialog, id) -> dialog.dismiss())
(dialog, id) -> dialog.dismiss())
.setPositiveButton(R.string.help_getorbot,
(dialog, id) -> {
dialog.dismiss();

View File

@ -18,7 +18,6 @@ package com.m2049r.xmrwallet.dialog;
import android.app.Dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
@ -66,7 +65,7 @@ public class PocketChangeFragment extends DialogFragment implements Slider.OnCha
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_pocketchange_setting, null);
final View view = getLayoutInflater().inflate(R.layout.fragment_pocketchange_setting, null);
boolean enabled = false;
int progress = 0;
Bundle arguments = getArguments();

View File

@ -20,10 +20,10 @@ import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@ -49,13 +49,14 @@ public class PrivacyFragment extends DialogFragment {
PrivacyFragment.newInstance().show(ft, TAG);
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getActivity()).inflate(R.layout.fragment_privacy_policy, null);
final View view = getLayoutInflater().inflate(R.layout.fragment_privacy_policy, null);
((TextView) view.findViewById(R.id.tvCredits)).setText(Html.fromHtml(getString(R.string.privacy_policy)));
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getActivity())
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireActivity())
.setView(view)
.setNegativeButton(R.string.about_close,
new DialogInterface.OnClickListener() {

View File

@ -17,6 +17,7 @@ package com.m2049r.xmrwallet.dialog;
* limitations under the License.
*/
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
@ -31,6 +32,7 @@ import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.util.Helper;
import java.util.Locale;
import java.util.Objects;
import timber.log.Timber;
@ -56,7 +58,7 @@ public class ProgressDialog extends AlertDialog {
@Override
protected void onCreate(Bundle savedInstanceState) {
final View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_ledger_progress, null);
@SuppressLint("InflateParams") final View view = LayoutInflater.from(getContext()).inflate(R.layout.dialog_ledger_progress, null);
pbCircle = view.findViewById(R.id.pbCircle);
tvMessage = view.findViewById(R.id.tvMessage);
rlProgressBar = view.findViewById(R.id.rlProgressBar);
@ -78,7 +80,7 @@ public class ProgressDialog extends AlertDialog {
super.onCreate(savedInstanceState);
if (Helper.preventScreenshot()) {
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
Objects.requireNonNull(getWindow()).setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
}
}

View File

@ -0,0 +1,16 @@
package com.m2049r.xmrwallet.fragment.send;
import com.m2049r.xmrwallet.data.CryptoAmount;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
public interface PreShifter {
CryptoAmount getAmount();
void onOrderParametersError(final Exception ex);
void onOrderParametersReceived(final QueryOrderParameters orderParameters);
boolean isActive();
void showProgress();
}

View File

@ -44,12 +44,9 @@ import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.data.UserNotes;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.OpenAliasHelper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import com.m2049r.xmrwallet.util.validator.BitcoinAddressType;
import com.m2049r.xmrwallet.util.validator.BitcoinAddressValidator;
import com.m2049r.xmrwallet.util.validator.EthAddressValidator;
import java.util.HashMap;
import java.util.HashSet;
@ -64,15 +61,11 @@ public class SendAddressWizardFragment extends SendWizardFragment {
public static SendAddressWizardFragment newInstance(Listener listener) {
SendAddressWizardFragment instance = new SendAddressWizardFragment();
instance.setSendListener(listener);
instance.sendListener = listener;
return instance;
}
Listener sendListener;
public void setSendListener(Listener listener) {
this.sendListener = listener;
}
private Listener sendListener;
public interface Listener {
void setBarcodeData(BarcodeData data);
@ -103,13 +96,6 @@ public class SendAddressWizardFragment extends SendWizardFragment {
void onScan();
}
private Crypto getCryptoForButton(ImageButton button) {
for (Map.Entry<Crypto, ImageButton> entry : ibCrypto.entrySet()) {
if (entry.getValue() == button) return entry.getKey();
}
return null;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@ -122,7 +108,9 @@ public class SendAddressWizardFragment extends SendWizardFragment {
ibCrypto = new HashMap<>();
for (Crypto crypto : Crypto.values()) {
final ImageButton button = view.findViewById(crypto.getButtonId());
if (Helper.ALLOW_SHIFT || (crypto == Crypto.XMR)) {
if ((crypto == Crypto.XMR)
|| (Helper.ALLOW_SHIFT && ShiftService.isAssetSupported(crypto))) {
button.setVisibility(View.VISIBLE);
ibCrypto.put(crypto, button);
button.setOnClickListener(v -> {
if (possibleCryptos.contains(crypto)) {
@ -131,9 +119,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
} else {
// show help what to do:
if (button.getId() != R.id.ibXMR) {
final String name = getResources().getStringArray(R.array.cryptos)[crypto.ordinal()];
final String symbol = getCryptoForButton(button).getSymbol();
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help, name, symbol)));
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help, crypto.getNetwork(), crypto.getLabel(), ShiftService.DEFAULT.getLabel())));
tvXmrTo.setVisibility(View.VISIBLE);
} else {
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_help_xmr)));
@ -143,9 +129,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
}
});
} else {
button.setImageResource(crypto.getIconDisabledId());
button.setImageAlpha(128);
button.setEnabled(false);
button.setVisibility(View.INVISIBLE);
}
}
if (!Helper.ALLOW_SHIFT) {
@ -175,49 +159,26 @@ public class SendAddressWizardFragment extends SendWizardFragment {
@Override
public void afterTextChanged(Editable editable) {
Timber.d("AFTER: %s", editable.toString());
etAddress.setError(null);
BarcodeData bc = sendListener.getBarcodeData();
if (bc == null) {
final String address = etAddress.getEditText().getText().toString();
bc = BarcodeData.fromString(address);
}
sendListener.setBarcodeData(null); // it's used up now
possibleCryptos.clear();
selectedCrypto = null;
final String address = etAddress.getEditText().getText().toString();
if (isIntegratedAddress(address)) {
Timber.d("isIntegratedAddress");
possibleCryptos.add(Crypto.XMR);
selectedCrypto = Crypto.XMR;
etAddress.setError(getString(R.string.info_paymentid_integrated));
sendListener.setMode(SendFragment.Mode.XMR);
} else if (isStandardAddress(address)) {
Timber.d("isStandardAddress");
possibleCryptos.add(Crypto.XMR);
selectedCrypto = Crypto.XMR;
sendListener.setMode(SendFragment.Mode.XMR);
}
if (!Helper.ALLOW_SHIFT) return;
if ((selectedCrypto == null) && isEthAddress(address)) {
Timber.d("isEthAddress");
possibleCryptos.add(Crypto.ETH);
selectedCrypto = Crypto.ETH;
tvXmrTo.setVisibility(View.VISIBLE);
sendListener.setMode(SendFragment.Mode.BTC);
}
if (possibleCryptos.isEmpty()) {
Timber.d("isBitcoinAddress");
for (BitcoinAddressType type : BitcoinAddressType.values()) {
if (BitcoinAddressValidator.validate(address, type)) {
possibleCryptos.add(Crypto.valueOf(type.name()));
}
}
if (!possibleCryptos.isEmpty()) // found something in need of shifting!
sendListener.setMode(SendFragment.Mode.BTC);
if (possibleCryptos.size() == 1) {
selectedCrypto = (Crypto) possibleCryptos.toArray()[0];
if ((bc != null) && (bc.filter(ShiftService.getPossibleAssets()))) {
possibleCryptos.clear();
possibleCryptos.addAll(bc.getPossibleAssets());
selectedCrypto = bc.getAsset();
if (checkAddress()) {
if (bc.getSecurity() == BarcodeData.Security.OA_NO_DNSSEC)
etAddress.setError(getString(R.string.send_address_no_dnssec));
else if (bc.getSecurity() == BarcodeData.Security.OA_DNSSEC)
etAddress.setError(getString(R.string.send_address_openalias));
}
}
if (possibleCryptos.isEmpty()) {
Timber.d("other");
tvXmrTo.setVisibility(View.INVISIBLE);
sendListener.setMode(SendFragment.Mode.XMR);
}
updateCryptoButtons(address.isEmpty());
updateCryptoButtons(false);
}
@Override
@ -231,7 +192,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
final ImageButton bPasteAddress = view.findViewById(R.id.bPasteAddress);
bPasteAddress.setOnClickListener(v -> {
final String clip = Helper.getClipBoardText(getActivity());
final String clip = Helper.getClipBoardText(requireActivity());
if (clip == null) return;
// clean it up
final String address = clip.replaceAll("( +)|(\\r?\\n?)", "");
@ -248,16 +209,14 @@ public class SendAddressWizardFragment extends SendWizardFragment {
etNotes = view.findViewById(R.id.etNotes);
etNotes.getEditText().setRawInputType(InputType.TYPE_CLASS_TEXT);
etNotes.getEditText().
setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
etDummy.requestFocus();
return true;
}
return false;
});
etNotes.getEditText().setOnEditorActionListener((v, actionId, event) -> {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN))
|| (actionId == EditorInfo.IME_ACTION_DONE)) {
etDummy.requestFocus();
return true;
}
return false;
});
final View cvScan = view.findViewById(R.id.bScan);
cvScan.setOnClickListener(v -> onScanListener.onScan());
@ -271,13 +230,19 @@ public class SendAddressWizardFragment extends SendWizardFragment {
private void selectedCrypto(Crypto crypto) {
final ImageButton button = ibCrypto.get(crypto);
assert button != null;
button.setImageResource(crypto.getIconEnabledId());
button.setImageAlpha(255);
button.setEnabled(true);
if (selectedCrypto == Crypto.XMR)
sendListener.setMode(SendFragment.Mode.XMR);
else
sendListener.setMode(SendFragment.Mode.BTC);
}
private void possibleCrypto(Crypto crypto) {
final ImageButton button = ibCrypto.get(crypto);
assert button != null;
button.setImageResource(crypto.getIconDisabledId());
button.setImageAlpha(255);
button.setEnabled(true);
@ -285,6 +250,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
private void impossibleCrypto(Crypto crypto) {
final ImageButton button = ibCrypto.get(crypto);
if (button == null) return; // not all buttons exist for all providers
button.setImageResource(crypto.getIconDisabledId());
button.setImageAlpha(128);
button.setEnabled(true);
@ -302,7 +268,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
}
}
if ((selectedCrypto != null) && (selectedCrypto != Crypto.XMR)) {
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto, selectedCrypto.getSymbol())));
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto, selectedCrypto.getNetwork(), selectedCrypto.getLabel(), ShiftService.DEFAULT.getLabel())));
tvXmrTo.setVisibility(View.VISIBLE);
} else if ((selectedCrypto == null) && (possibleCryptos.size() > 1)) {
tvXmrTo.setText(Html.fromHtml(getString(R.string.info_xmrto_ambiguous)));
@ -328,7 +294,7 @@ public class SendAddressWizardFragment extends SendWizardFragment {
BarcodeData barcodeData = dataMap.get(Crypto.XMR);
if (barcodeData == null) barcodeData = dataMap.get(Crypto.BTC);
if (barcodeData != null) {
Timber.d("Security=%s, %s", barcodeData.security.toString(), barcodeData.address);
Timber.d("Security=%s, %s", barcodeData.getSecurity().toString(), barcodeData.getAddress());
processScannedData(barcodeData);
} else {
etAddress.setError(getString(R.string.send_address_not_openalias));
@ -369,18 +335,6 @@ public class SendAddressWizardFragment extends SendWizardFragment {
&& Wallet.isAddressValid(address);
}
private boolean isBitcoinishAddress(String address) {
return BitcoinAddressValidator.validate(address, BitcoinAddressType.BTC)
||
BitcoinAddressValidator.validate(address, BitcoinAddressType.LTC)
||
BitcoinAddressValidator.validate(address, BitcoinAddressType.DASH);
}
private boolean isEthAddress(String address) {
return EthAddressValidator.validate(address);
}
private void shakeAddress() {
if (possibleCryptos.size() > 1) { // address ambiguous
for (Crypto crypto : Crypto.values()) {
@ -412,10 +366,10 @@ public class SendAddressWizardFragment extends SendWizardFragment {
((TxDataBtc) txData).setBtcAddress(etAddress.getEditText().getText().toString());
((TxDataBtc) txData).setBtcSymbol(selectedCrypto.getSymbol());
txData.setDestination(null);
ServiceHelper.ASSET = selectedCrypto.getSymbol().toLowerCase();
ShiftService.ASSET = selectedCrypto;
} else {
txData.setDestination(etAddress.getEditText().getText().toString());
ServiceHelper.ASSET = null;
ShiftService.ASSET = null;
}
txData.setUserNotes(new UserNotes(etNotes.getEditText().getText().toString()));
txData.setPriority(PendingTransaction.Priority.Priority_Default);
@ -445,49 +399,33 @@ public class SendAddressWizardFragment extends SendWizardFragment {
}
public void processScannedData(BarcodeData barcodeData) {
barcodeData.filter(ShiftService.getPossibleAssets());
sendListener.setBarcodeData(barcodeData);
if (isResumed())
processScannedData();
}
public void processScannedData() {
BarcodeData barcodeData = sendListener.getBarcodeData();
final BarcodeData barcodeData = sendListener.getBarcodeData();
if (barcodeData != null) {
Timber.d("GOT DATA");
if (!Helper.ALLOW_SHIFT && (barcodeData.asset != Crypto.XMR)) {
if (!Helper.ALLOW_SHIFT && (barcodeData.getAsset() != Crypto.XMR)) {
Timber.d("BUT ONLY XMR SUPPORTED");
barcodeData = null;
sendListener.setBarcodeData(barcodeData);
sendListener.setBarcodeData(null);
return;
}
if (barcodeData.address != null) {
etAddress.getEditText().setText(barcodeData.address);
possibleCryptos.clear();
selectedCrypto = null;
if (barcodeData.isAmbiguous()) {
possibleCryptos.addAll(barcodeData.ambiguousAssets);
} else {
possibleCryptos.add(barcodeData.asset);
selectedCrypto = barcodeData.asset;
}
if (Helper.ALLOW_SHIFT)
updateCryptoButtons(false);
if (checkAddress()) {
if (barcodeData.security == BarcodeData.Security.OA_NO_DNSSEC)
etAddress.setError(getString(R.string.send_address_no_dnssec));
else if (barcodeData.security == BarcodeData.Security.OA_DNSSEC)
etAddress.setError(getString(R.string.send_address_openalias));
}
if (barcodeData.getAddress() != null) {
etAddress.getEditText().setText(barcodeData.getAddress());
} else {
etAddress.getEditText().getText().clear();
etAddress.setError(null);
}
String scannedNotes = barcodeData.addressName;
String scannedNotes = barcodeData.getAddressName();
if (scannedNotes == null) {
scannedNotes = barcodeData.description;
} else if (barcodeData.description != null) {
scannedNotes = scannedNotes + ": " + barcodeData.description;
scannedNotes = barcodeData.getDescription();
} else if (barcodeData.getDescription() != null) {
scannedNotes = scannedNotes + ": " + barcodeData.getDescription();
}
if (scannedNotes != null) {
etNotes.getEditText().setText(scannedNotes);

View File

@ -23,8 +23,6 @@ import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.TextView;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.android.material.switchmaterial.SwitchMaterial;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.data.TxData;
@ -38,17 +36,13 @@ public class SendAmountWizardFragment extends SendWizardFragment {
public static SendAmountWizardFragment newInstance(Listener listener) {
SendAmountWizardFragment instance = new SendAmountWizardFragment();
instance.setSendListener(listener);
instance.sendListener = listener;
return instance;
}
Listener sendListener;
private Listener sendListener;
public void setSendListener(Listener listener) {
this.sendListener = listener;
}
interface Listener {
public interface Listener {
SendFragment.Listener getActivityCallback();
TxData getTxData();
@ -139,7 +133,7 @@ public class SendAmountWizardFragment extends SendWizardFragment {
public void onResumeFragment() {
super.onResumeFragment();
Timber.d("onResumeFragment()");
Helper.showKeyboard(getActivity());
Helper.showKeyboard(requireActivity());
final long funds = getTotalFunds();
maxFunds = 1.0 * funds / Helper.ONE_XMR;
if (!sendListener.getActivityCallback().isStreetMode()) {
@ -150,8 +144,8 @@ public class SendAmountWizardFragment extends SendWizardFragment {
getString(R.string.unknown_amount)));
}
final BarcodeData data = sendListener.popBarcodeData();
if ((data != null) && (data.amount != null)) {
etAmount.setAmount(data.amount);
if ((data != null) && (data.getAmount() != null)) {
etAmount.setAmount(data.getAmount());
}
}

View File

@ -17,48 +17,49 @@
package com.m2049r.xmrwallet.fragment.send;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.Html;
import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.BarcodeData;
import com.m2049r.xmrwallet.data.CryptoAmount;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftError;
import com.m2049r.xmrwallet.service.shift.ShiftException;
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderParameters;
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
import com.m2049r.xmrwallet.service.shift.process.PreShiftProcess;
import com.m2049r.xmrwallet.util.AmountHelper;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import com.m2049r.xmrwallet.widget.ExchangeOtherEditText;
import com.m2049r.xmrwallet.widget.SendProgressView;
import java.text.NumberFormat;
import java.util.Locale;
import timber.log.Timber;
public class SendBtcAmountWizardFragment extends SendWizardFragment {
public class SendBtcAmountWizardFragment extends SendWizardFragment implements PreShifter, ExchangeOtherEditText.Listener {
public static SendBtcAmountWizardFragment newInstance(SendAmountWizardFragment.Listener listener) {
SendBtcAmountWizardFragment instance = new SendBtcAmountWizardFragment();
instance.setSendListener(listener);
return instance;
return new SendBtcAmountWizardFragment(listener);
}
SendAmountWizardFragment.Listener sendListener;
public SendBtcAmountWizardFragment setSendListener(SendAmountWizardFragment.Listener listener) {
this.sendListener = listener;
return this;
private SendBtcAmountWizardFragment(@NonNull SendAmountWizardFragment.Listener listener) {
super();
sendListener = listener;
}
private final SendAmountWizardFragment.Listener sendListener;
private TextView tvFunds;
private ExchangeOtherEditText etAmount;
@ -72,8 +73,6 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
sendListener = (SendAmountWizardFragment.Listener) getParentFragment();
View view = inflater.inflate(R.layout.fragment_send_btc_amount, container, false);
tvFunds = view.findViewById(R.id.tvFunds);
@ -83,6 +82,8 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
tvXmrToParms = view.findViewById(R.id.tvXmrToParms);
((ImageView) view.findViewById(R.id.shiftIcon)).setImageResource(service.getIconId());
etAmount = view.findViewById(R.id.etAmount);
etAmount.requestFocus();
@ -98,32 +99,19 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
if (orderParameters == null) {
return false; // this should never happen
}
if (sendListener != null) {
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
String btcString = etAmount.getNativeAmount();
if (btcString != null) {
try {
double btc = Double.parseDouble(btcString);
Timber.d("setBtcAmount %f", btc);
txDataBtc.setBtcAmount(btc);
txDataBtc.setAmount(btc / orderParameters.getPrice());
} catch (NumberFormatException ex) {
Timber.d(ex.getLocalizedMessage());
txDataBtc.setBtcAmount(0);
}
} else {
txDataBtc.setBtcAmount(0);
}
}
final TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
txDataBtc.setShiftAmount(etAmount.getPrimaryAmount());
return true;
}
double maxBtc = 0;
double minBtc = 0;
private double maxBtc = 0;
private double minBtc = 0;
@Override
public void onPauseFragment() {
super.onPauseFragment();
llXmrToParms.setVisibility(View.INVISIBLE);
etAmount.setListener(null);
}
@Override
@ -131,8 +119,8 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
super.onResumeFragment();
Timber.d("onResumeFragment()");
final String btcSymbol = ((TxDataBtc) sendListener.getTxData()).getBtcSymbol();
if (!btcSymbol.toLowerCase().equals(ServiceHelper.ASSET))
throw new IllegalStateException("Asset Symbol is wrong!");
if (!btcSymbol.equalsIgnoreCase(ShiftService.ASSET.getSymbol()))
throw new IllegalStateException("Asset Symbol is wrong (" + btcSymbol + "!=" + ShiftService.ASSET.getSymbol() + ")");
final long funds = getTotalFunds();
if (!sendListener.getActivityCallback().isStreetMode()) {
tvFunds.setText(getString(R.string.send_available,
@ -143,48 +131,131 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
getString(R.string.unknown_amount)));
}
etAmount.setAmount("");
etAmount.setListener(this);
final BarcodeData data = sendListener.popBarcodeData();
if (data != null) {
if (data.amount != null) {
etAmount.setAmount(data.amount);
if (data.getAmount() != null) {
etAmount.setAmount(data.getAmount());
}
}
etAmount.setBaseCurrency(btcSymbol);
callXmrTo();
updateShift();
}
long getTotalFunds() {
return sendListener.getActivityCallback().getTotalFunds();
}
private final ShiftService service = ShiftService.DEFAULT;
private final PreShiftProcess preShiftProcess = service.createPreProcess(this);
private QueryOrderParameters orderParameters = null;
private void processOrderParms(final QueryOrderParameters orderParameters) {
private void reset() {
orderParameters = null;
maxBtc = 0;
minBtc = 0;
etAmount.setExchangeRate(0);
}
private void updateShift() {
reset();
getTxData().setShiftService(service);
llXmrToParms.setVisibility(View.INVISIBLE);
preShiftProcess.run();
}
private TxDataBtc getTxData() {
final TxData txData = sendListener.getTxData();
if (txData instanceof TxDataBtc) {
return (TxDataBtc) txData;
} else {
throw new IllegalStateException("TxData not BTC");
}
}
private boolean isValid() {
return orderParameters != null;
}
@Override
public CryptoAmount getAmount() { // of BTC
return etAmount.getPrimaryAmount();
}
public double getLowerLimit() {
if (!isValid()) throw new IllegalStateException();
return orderParameters.getLowerLimit();
}
public double getPrice() {
if (!isValid()) throw new IllegalStateException();
return orderParameters.getPrice();
}
public double getUpperLimit() {
if (!isValid()) throw new IllegalStateException();
return orderParameters.getUpperLimit();
}
@Override
public void onOrderParametersError(final Exception ex) {
reset();
Timber.e(ex);
requireView().post(() -> {
if (ex instanceof ShiftException) {
ShiftException xmrEx = (ShiftException) ex;
ShiftError xmrErr = xmrEx.getError();
if (xmrErr != null) {
if (xmrErr.isRetryable()) {
evParams.showMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_retry));
evParams.setOnClickListener(v -> {
evParams.setOnClickListener(null);
updateShift();
});
} else {
evParams.showMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
} else {
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
} else {
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
ex.getLocalizedMessage(),
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
});
}
@Override
public void onOrderParametersReceived(QueryOrderParameters orderParameters) {
final double price = orderParameters.getPrice();
maxBtc = price * orderParameters.getUpperLimit();
minBtc = price * orderParameters.getLowerLimit();
this.orderParameters = orderParameters;
getView().post(() -> {
final double price = orderParameters.getPrice();
requireView().post(() -> {
etAmount.setExchangeRate(1 / price);
maxBtc = price * orderParameters.getUpperLimit();
minBtc = price * orderParameters.getLowerLimit();
Timber.d("minBtc=%f / maxBtc=%f", minBtc, maxBtc);
NumberFormat df = NumberFormat.getInstance(Locale.US);
df.setMaximumFractionDigits(6);
String min = df.format(minBtc);
String max = df.format(maxBtc);
String rate = df.format(price);
final String min = AmountHelper.format_6(minBtc);
final String max = AmountHelper.format_6(maxBtc);
final String rate = AmountHelper.format_6(price);
final TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
Spanned xmrParmText = Html.fromHtml(getString(R.string.info_send_xmrto_parms,
min, max, rate, txDataBtc.getBtcSymbol()));
final Spanned xmrParmText = Html.fromHtml(getString(R.string.info_send_xmrto_parms,
min, max, rate, txDataBtc.getBtcSymbol(), service.getLabel()));
tvXmrToParms.setText(xmrParmText);
final long funds = getTotalFunds();
double availableXmr = 1.0 * funds / Helper.ONE_XMR;
final double availableXmr = 1.0 * funds / Helper.ONE_XMR;
String availBtcString;
String availXmrString;
if (!sendListener.getActivityCallback().isStreetMode()) {
availBtcString = df.format(availableXmr * price);
availXmrString = df.format(availableXmr);
availBtcString = AmountHelper.format_6(availableXmr * price);
availXmrString = AmountHelper.format_6(availableXmr);
} else {
availBtcString = getString(R.string.unknown_amount);
availXmrString = availBtcString;
@ -198,66 +269,28 @@ public class SendBtcAmountWizardFragment extends SendWizardFragment {
});
}
private void processOrderParmsError(final Exception ex) {
etAmount.setExchangeRate(0);
orderParameters = null;
maxBtc = 0;
minBtc = 0;
Timber.e(ex);
getView().post(() -> {
if (ex instanceof ShiftException) {
ShiftException xmrEx = (ShiftException) ex;
ShiftError xmrErr = xmrEx.getError();
if (xmrErr != null) {
if (xmrErr.isRetryable()) {
evParams.showMessage(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_retry));
evParams.setOnClickListener(v -> {
evParams.setOnClickListener(null);
callXmrTo();
});
} else {
evParams.showMessage(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry));
}
} else {
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
getString(R.string.text_noretry));
}
} else {
evParams.showMessage(getString(R.string.label_generic_xmrto_error),
ex.getLocalizedMessage(),
getString(R.string.text_noretry));
}
});
}
@Override
public boolean isActive() {
return true;
} // TODO Test what happens if we swtich away while querying
private void callXmrTo() {
@Override
public void showProgress() {
evParams.showProgress(getString(R.string.label_send_progress_queryparms));
getXmrToApi().queryOrderParameters(new ShiftCallback<QueryOrderParameters>() {
@Override
public void onSuccess(final QueryOrderParameters orderParameters) {
processOrderParms(orderParameters);
}
@Override
public void onError(final Exception e) {
processOrderParmsError(e);
}
});
}
private SideShiftApi xmrToApi = null;
long lastRequest = 0;
final static long EXCHANGE_TIME = 750; //ms
final Handler handler = new Handler(Looper.getMainLooper());
private SideShiftApi getXmrToApi() {
if (xmrToApi == null) {
synchronized (this) {
if (xmrToApi == null) {
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
}
@Override
public void onExchangeRequested() {
final long now = System.currentTimeMillis();
lastRequest = now;
handler.postDelayed(() -> {
if (now == lastRequest) { // otherwise we are superseded
updateShift();
}
}
return xmrToApi;
}, EXCHANGE_TIME);
}
}

View File

@ -24,39 +24,36 @@ import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.model.PendingTransaction;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftError;
import com.m2049r.xmrwallet.service.shift.ShiftException;
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.sideshift.api.RequestQuote;
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
import com.m2049r.xmrwallet.service.shift.process.ShiftProcess;
import com.m2049r.xmrwallet.util.AmountHelper;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import com.m2049r.xmrwallet.widget.SendProgressView;
import java.text.NumberFormat;
import java.util.Locale;
import timber.log.Timber;
public class SendBtcConfirmWizardFragment extends SendWizardFragment implements SendConfirm {
public class SendBtcConfirmWizardFragment extends SendWizardFragment implements SendConfirm, Shifter {
public static SendBtcConfirmWizardFragment newInstance(SendConfirmWizardFragment.Listener listener) {
SendBtcConfirmWizardFragment instance = new SendBtcConfirmWizardFragment();
instance.setSendListener(listener);
instance.sendListener = listener;
return instance;
}
SendConfirmWizardFragment.Listener sendListener;
public void setSendListener(SendConfirmWizardFragment.Listener listener) {
this.sendListener = listener;
}
private SendConfirmWizardFragment.Listener sendListener;
private View llStageA;
private SendProgressView evStageA;
@ -77,11 +74,12 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
private TextView tvTxChange;
private View llPocketChange;
private TextView tvTxXmrToKeyLabel;
private TextView tvTxXmrToInfo;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Timber.d("onCreateView(%s)", (String.valueOf(savedInstanceState)));
View view = inflater.inflate(
@ -93,6 +91,10 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
tvTxBtcRate = view.findViewById(R.id.tvTxBtcRate);
tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey);
tvTxXmrToKeyLabel = view.findViewById(R.id.tvTxXmrToKeyLabel);
tvTxXmrToInfo = view.findViewById(R.id.tvTxXmrToInfo);
tvTxFee = view.findViewById(R.id.tvTxFee);
tvTxTotal = view.findViewById(R.id.tvTxTotal);
tvTxChange = view.findViewById(R.id.tvTxChange);
@ -106,7 +108,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
evStageC = view.findViewById(R.id.evStageC);
tvTxXmrToKey.setOnClickListener(v -> {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
});
@ -125,68 +127,74 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
return view;
}
int inProgress = 0;
final static int STAGE_X = 0;
final static int STAGE_A = 1;
final static int STAGE_B = 2;
final static int STAGE_C = 3;
@NonNull
Shifter.Stage inProgress = Stage.X;
private void showProgress(int stage, String progressText) {
Timber.d("showProgress(%d)", stage);
inProgress = stage;
switch (stage) {
case STAGE_A:
evStageA.showProgress(progressText);
break;
case STAGE_B:
evStageB.showProgress(progressText);
break;
case STAGE_C:
evStageC.showProgress(progressText);
break;
default:
throw new IllegalStateException("unknown stage " + stage);
}
@Override
public void showProgress(@NonNull Shifter.Stage stage) {
Timber.d("showProgress(%s)", stage);
requireView().post(() -> {
switch (stage) {
case A:
evStageA.showProgress(getString(R.string.label_send_progress_xmrto_create));
break;
case B:
evStageB.showProgress(getString(R.string.label_send_progress_xmrto_query));
break;
case C:
evStageC.showProgress(getString(R.string.label_send_progress_create_tx));
break;
default:
throw new IllegalArgumentException("invalid stage " + stage);
}
inProgress = stage;
});
}
public void hideProgress() {
Timber.d("hideProgress(%d)", inProgress);
Timber.d("hideProgress(%s)", inProgress);
switch (inProgress) {
case STAGE_A:
case A:
evStageA.hideProgress();
llStageA.setVisibility(View.VISIBLE);
break;
case STAGE_B:
case B:
evStageB.hideProgress();
llStageA.setVisibility(View.VISIBLE); // show Stage A info when B is ready
llStageB.setVisibility(View.VISIBLE);
break;
case STAGE_C:
case C:
evStageC.hideProgress();
llStageC.setVisibility(View.VISIBLE);
break;
default:
throw new IllegalStateException("unknown stage " + inProgress);
}
inProgress = STAGE_X;
inProgress = Stage.X;
}
public void showStageError(String code, String message, String solution) {
private void showErrorMessage(String code, String message, String solution) {
switch (inProgress) {
case STAGE_A:
case A:
evStageA.showMessage(code, message, solution);
break;
case STAGE_B:
case B:
evStageB.showMessage(code, message, solution);
break;
case STAGE_C:
case C:
evStageC.showMessage(code, message, solution);
break;
default:
throw new IllegalStateException("unknown stage");
throw new IllegalStateException("invalid stage");
}
inProgress = STAGE_X;
inProgress = Stage.X;
}
public void showQuoteError() {
showErrorMessage(ShiftError.Type.SERVICE.toString(),
getString(R.string.shift_noquote),
getString(R.string.shift_checkamount));
}
PendingTransaction pendingTransaction = null;
void send() {
@ -196,11 +204,8 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
Toast.makeText(getContext(), getString(R.string.send_xmrto_timeout), Toast.LENGTH_SHORT).show();
return;
}
sendListener.getTxData().getUserNotes().setXmrtoOrder(xmrtoOrder); // note the transaction in the TX notes
((TxDataBtc) sendListener.getTxData()).setXmrtoOrderId(xmrtoOrder.getOrderId()); // remember the order id for later
// TODO make method in TxDataBtc to set both of the above in one go
sendListener.commitTransaction();
getActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
requireActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
}
@Override
@ -209,15 +214,18 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
Toast.makeText(getContext(), getString(R.string.status_transaction_failed, error), Toast.LENGTH_LONG).show();
}
private String orderId = null;
@Override
// callback from wallet when PendingTransaction created (started by prepareSend() here
public void transactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
if (isResumed
&& (inProgress == STAGE_C)
&& (xmrtoOrder != null)
&& (xmrtoOrder.getOrderId().equals(txTag))) {
&& (inProgress == Stage.C)
&& (orderId != null)
&& (orderId.equals(txTag))) {
this.pendingTransaction = pendingTransaction;
getView().post(() -> {
requireView().post(() -> {
Timber.d("transactionCreated");
hideProgress();
tvTxFee.setText(Wallet.getDisplayAmount(pendingTransaction.getFee()));
tvTxTotal.setText(Wallet.getDisplayAmount(
@ -243,29 +251,35 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
if (pendingTransaction != null) {
throw new IllegalStateException("pendingTransaction is not null");
}
showStageError(getString(R.string.send_create_tx_error_title),
showErrorMessage(getString(R.string.send_create_tx_error_title),
errorText,
getString(R.string.text_noretry_monero));
}
@Override
public boolean onValidateFields() {
return true;
}
private boolean isResumed = false;
@Override
public void onPauseFragment() {
isResumed = false;
shiftProcess = null;
stopSendTimer();
sendListener.disposeTransaction();
pendingTransaction = null;
inProgress = STAGE_X;
inProgress = Stage.X;
//TODO: maybe reset the progress messages
updateSendButton();
super.onPauseFragment();
}
private TxDataBtc getTxData() {
final TxData txData = sendListener.getTxData();
if (txData instanceof TxDataBtc) {
return (TxDataBtc) txData;
} else {
throw new IllegalStateException("TxData not BTC");
}
}
@Override
public void onResumeFragment() {
super.onResumeFragment();
@ -273,8 +287,9 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
if (sendListener.getMode() != SendFragment.Mode.BTC) {
throw new IllegalStateException("Mode is not BTC!");
}
if (!((TxDataBtc) sendListener.getTxData()).getBtcSymbol().toLowerCase().equals(ServiceHelper.ASSET))
throw new IllegalStateException("Asset Symbol is wrong!");
final String btcSymbol = getTxData().getBtcSymbol();
if (!btcSymbol.equalsIgnoreCase(ShiftService.ASSET.getSymbol()))
throw new IllegalStateException("Asset Symbol is wrong (" + btcSymbol + "!=" + ShiftService.ASSET.getSymbol() + ")");
Helper.hideKeyboard(getActivity());
llStageA.setVisibility(View.INVISIBLE);
evStageA.hideProgress();
@ -282,9 +297,17 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
evStageB.hideProgress();
llStageC.setVisibility(View.INVISIBLE);
evStageC.hideProgress();
if (shiftProcess != null) throw new IllegalStateException("shiftProcess not null");
shiftProcess = getTxData().getShiftService().createProcess(this);
tvTxXmrToKeyLabel.setText(getString(R.string.label_send_btc_xmrto_key, shiftProcess.getService().getLabel()));
tvTxXmrToKeyLabel.setCompoundDrawablesRelativeWithIntrinsicBounds(shiftProcess.getService().getIconId(), 0, 0, 0);
tvTxXmrToInfo.setText(getString(R.string.label_send_btc_xmrto_info, shiftProcess.getService().getLabel()));
isResumed = true;
if ((pendingTransaction == null) && (inProgress == STAGE_X)) {
stageA();
if ((pendingTransaction == null) && (inProgress == Stage.X)) {
Timber.d("Starting ShiftProcess");
shiftProcess.run(getTxData());
} // otherwise just sit there blank
// TODO: don't sit there blank - can this happen? should we just die?
}
@ -310,19 +333,19 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
}
int minutes = sendCountdown / 60;
int seconds = sendCountdown % 60;
String t = String.format("%d:%02d", minutes, seconds);
String t = String.format(Locale.US, "%d:%02d", minutes, seconds);
bSend.setText(getString(R.string.send_send_timed_label, t));
if (sendCountdown > 0) {
sendCountdown -= XMRTO_COUNTDOWN_STEP;
getView().postDelayed(this, XMRTO_COUNTDOWN_STEP * 1000);
requireView().postDelayed(this, XMRTO_COUNTDOWN_STEP * 1000);
}
}
};
getView().post(updateRunnable);
requireView().post(updateRunnable);
}
void stopSendTimer() {
getView().removeCallbacks(updateRunnable);
requireView().removeCallbacks(updateRunnable);
}
void updateSendButton() {
@ -344,7 +367,7 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
}
public void fail(String walletName) {
getActivity().runOnUiThread(() -> {
requireActivity().runOnUiThread(() -> {
bSend.setEnabled(sendCountdown > 0); // allow to try again
});
}
@ -353,211 +376,127 @@ public class SendBtcConfirmWizardFragment extends SendWizardFragment implements
// creates a pending transaction and calls us back with transactionCreated()
// or createTransactionFailed()
void prepareSend() {
public void prepareSend(CreateOrder order) {
if (!isResumed) return;
if ((xmrtoOrder == null)) {
throw new IllegalStateException("xmrtoOrder is null");
if ((order == null)) {
throw new IllegalStateException("order is null");
}
showProgress(3, getString(R.string.label_send_progress_create_tx));
final TxData txData = sendListener.getTxData();
txData.setDestination(xmrtoOrder.getXmrAddress());
txData.setAmount(xmrtoOrder.getXmrAmount());
getActivityCallback().onPrepareSend(xmrtoOrder.getOrderId(), txData);
showProgress(Stage.C);
orderId = order.getOrderId();
final TxDataBtc txData = getTxData();
txData.setDestination(order.getXmrAddress());
txData.setAmount(order.getXmrAmount());
txData.getUserNotes().setXmrtoOrder(order); // note the transaction in the TX notes
txData.setXmrtoOrderId(order.getOrderId()); // remember the order id for later
txData.setXmrtoQueryOrderToken(order.getQueryOrderId()); // remember the order id for later
getActivityCallback().onPrepareSend(order.getOrderId(), txData);
}
SendFragment.Listener getActivityCallback() {
return sendListener.getActivityCallback();
}
private RequestQuote xmrtoQuote = null;
private ShiftProcess shiftProcess;
private void processStageA(final RequestQuote requestQuote) {
Timber.d("processCreateOrder %s", requestQuote.getId());
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
// verify the BTC amount is correct
if (requestQuote.getBtcAmount() != txDataBtc.getBtcAmount()) {
Timber.d("Failed to get quote");
getView().post(() -> showStageError(ShiftError.Error.SERVICE.toString(),
getString(R.string.shift_noquote),
getString(R.string.shift_checkamount)));
return; // just stop for now
}
xmrtoQuote = requestQuote;
txDataBtc.setAmount(xmrtoQuote.getXmrAmount());
getView().post(() -> {
// show data from the actual quote as that is what is used to
NumberFormat df = NumberFormat.getInstance(Locale.US);
df.setMaximumFractionDigits(12);
final String btcAmount = df.format(xmrtoQuote.getBtcAmount());
final String xmrAmountTotal = df.format(xmrtoQuote.getXmrAmount());
tvTxBtcAmount.setText(getString(R.string.text_send_btc_amount,
btcAmount, xmrAmountTotal, txDataBtc.getBtcSymbol()));
final String xmrPriceBtc = df.format(xmrtoQuote.getPrice());
tvTxBtcRate.setText(getString(R.string.text_send_btc_rate, xmrPriceBtc, txDataBtc.getBtcSymbol()));
hideProgress();
});
stageB(requestQuote.getId());
public void showQuote(double btcAmount, double xmrAmount, double price) {
final String symbol = getTxData().getBtcSymbol();
tvTxBtcAmount.setText(getString(R.string.text_send_btc_amount,
AmountHelper.format(btcAmount), AmountHelper.format(xmrAmount), symbol));
tvTxBtcRate.setText(getString(R.string.text_send_btc_rate, AmountHelper.format(price), symbol));
}
private void processStageAError(final Exception ex) {
// Shifter
public void onQuoteReceived(RequestQuote quote) {
requireView().post(() -> {
Timber.d("onQuoteReceived");
showQuote(quote.getBtcAmount(), quote.getXmrAmount(), quote.getPrice());
hideProgress();
});
}
public void onQuoteError(final Exception ex) {
Timber.e("processStageAError %s", ex.getLocalizedMessage());
getView().post(() -> {
requireView().post(() -> {
if (ex instanceof ShiftException) {
ShiftException xmrEx = (ShiftException) ex;
ShiftError xmrErr = xmrEx.getError();
if (xmrErr != null) {
if (xmrErr.isRetryable()) {
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_retry));
evStageA.setOnClickListener(v -> {
evStageA.setOnClickListener(null);
stageA();
shiftProcess.restart();
});
} else {
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry));
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
} else {
showStageError(getString(R.string.label_generic_xmrto_error),
showErrorMessage(getString(R.string.label_generic_xmrto_error),
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
getString(R.string.text_noretry));
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
} else {
evStageA.showMessage(getString(R.string.label_generic_xmrto_error),
ex.getLocalizedMessage(),
getString(R.string.text_noretry));
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
});
}
private void stageA() {
if (!isResumed) return;
Timber.d("Request Quote");
xmrtoQuote = null;
xmrtoOrder = null;
showProgress(1, getString(R.string.label_send_progress_xmrto_create));
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
ShiftCallback<RequestQuote> callback = new ShiftCallback<RequestQuote>() {
@Override
public void onSuccess(RequestQuote requestQuote) {
if (!isResumed) return;
if (xmrtoQuote != null) {
Timber.w("another ongoing request quote request");
return;
}
processStageA(requestQuote);
}
@Override
public void onError(Exception ex) {
if (!isResumed) return;
if (xmrtoQuote != null) {
Timber.w("another ongoing request quote request");
return;
}
processStageAError(ex);
}
};
getXmrToApi().requestQuote(txDataBtc.getBtcAmount(), callback);
@Override
public boolean isActive() {
return isResumed;
}
private CreateOrder xmrtoOrder = null;
private void processStageB(final CreateOrder order) {
Timber.d("processCreateOrder %s for %s", order.getOrderId(), order.getQuoteId());
TxDataBtc txDataBtc = (TxDataBtc) sendListener.getTxData();
// verify amount & destination
if ((order.getBtcAmount() != txDataBtc.getBtcAmount())
|| (!txDataBtc.validateAddress(order.getBtcAddress()))) {
throw new IllegalStateException("Order does not fulfill quote!"); // something is terribly wrong - die
}
xmrtoOrder = order;
getView().post(() -> {
public void onOrderCreated(CreateOrder order) {
requireView().post(() -> {
showQuote(order.getBtcAmount(), order.getXmrAmount(), order.getBtcAmount() / order.getXmrAmount());
tvTxXmrToKey.setText(order.getOrderId());
tvTxBtcAddress.setText(order.getBtcAddress());
tvTxBtcAddressLabel.setText(getString(R.string.label_send_btc_address, txDataBtc.getBtcSymbol()));
tvTxBtcAddressLabel.setText(getString(R.string.label_send_btc_address, order.getBtcCurrency()));
Timber.d("onOrderCreated");
hideProgress();
Timber.d("Expires @ %s", order.getExpiresAt().toString());
final int timeout = (int) (order.getExpiresAt().getTime() - order.getCreatedAt().getTime()) / 1000 - 60; // -1 minute buffer
startSendTimer(timeout);
prepareSend();
prepareSend(order);
});
}
private void processStageBError(final Exception ex) {
Timber.e("processCreateOrderError %s", ex.getLocalizedMessage());
getView().post(() -> {
public void onOrderError(final Exception ex) {
Timber.e("onOrderError %s", ex.getLocalizedMessage());
requireView().post(() -> {
if (ex instanceof ShiftException) {
ShiftException xmrEx = (ShiftException) ex;
ShiftError xmrErr = xmrEx.getError();
if (xmrErr != null) {
if (xmrErr.isRetryable()) {
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_retry));
evStageB.setOnClickListener(v -> {
evStageB.setOnClickListener(null);
stageB(xmrtoOrder.getOrderId());
shiftProcess.retryCreateOrder();
});
} else {
showStageError(xmrErr.getErrorType().toString(), xmrErr.getErrorMsg(),
getString(R.string.text_noretry));
showErrorMessage(xmrErr.getType().toString(), xmrErr.getErrorMsg(), null);
}
} else {
showStageError(getString(R.string.label_generic_xmrto_error),
showErrorMessage(getString(R.string.label_generic_xmrto_error),
getString(R.string.text_generic_xmrto_error, xmrEx.getCode()),
getString(R.string.text_noretry));
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
} else {
evStageB.showMessage(getString(R.string.label_generic_xmrto_error),
ex.getLocalizedMessage(),
getString(R.string.text_noretry));
getString(R.string.text_noretry, getTxData().getShiftService().getLabel()));
}
});
}
private void stageB(final String quoteId) {
Timber.d("createOrder(%s)", quoteId);
if (!isResumed) return;
final String btcAddress = ((TxDataBtc) sendListener.getTxData()).getBtcAddress();
getView().post(() -> {
xmrtoOrder = null;
showProgress(2, getString(R.string.label_send_progress_xmrto_query));
getXmrToApi().createOrder(quoteId, btcAddress, new ShiftCallback<CreateOrder>() {
@Override
public void onSuccess(CreateOrder order) {
if (!isResumed) return;
if (xmrtoQuote == null) return;
if (!order.getQuoteId().equals(xmrtoQuote.getId())) {
Timber.d("Quote ID does not match");
// ignore (we got a response to a stale request)
return;
}
if (xmrtoOrder != null)
throw new IllegalStateException("xmrtoOrder must be null here!");
processStageB(order);
}
@Override
public void onError(Exception ex) {
if (!isResumed) return;
processStageBError(ex);
}
});
});
}
private SideShiftApi xmrToApi = null;
private SideShiftApi getXmrToApi() {
if (xmrToApi == null) {
synchronized (this) {
if (xmrToApi == null) {
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
}
}
}
return xmrToApi;
@Override
public void invalidateShift() {
orderId = null;
}
}

View File

@ -18,7 +18,6 @@ package com.m2049r.xmrwallet.fragment.send;
import android.content.Intent;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@ -32,16 +31,17 @@ import android.widget.Toast;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.data.PendingTx;
import com.m2049r.xmrwallet.data.TxData;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftException;
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus;
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderStatus;
import com.m2049r.xmrwallet.service.shift.api.ShiftApi;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import com.m2049r.xmrwallet.util.ThemeHelper;
import java.net.SocketTimeoutException;
import java.text.NumberFormat;
import java.util.Locale;
@ -51,15 +51,11 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
public static SendBtcSuccessWizardFragment newInstance(SendSuccessWizardFragment.Listener listener) {
SendBtcSuccessWizardFragment instance = new SendBtcSuccessWizardFragment();
instance.setSendListener(listener);
instance.sendListener = listener;
return instance;
}
SendSuccessWizardFragment.Listener sendListener;
public void setSendListener(SendSuccessWizardFragment.Listener listener) {
this.sendListener = listener;
}
private SendSuccessWizardFragment.Listener sendListener;
ImageButton bCopyTxId;
private TextView tvTxId;
@ -80,6 +76,7 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
Bundle savedInstanceState) {
Timber.d("onCreateView() %s", (String.valueOf(savedInstanceState)));
final ShiftService shiftService = getTxData().getShiftService();
View view = inflater.inflate(
R.layout.fragment_send_btc_success, container, false);
@ -87,7 +84,7 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
bCopyTxId = view.findViewById(R.id.bCopyTxId);
bCopyTxId.setEnabled(false);
bCopyTxId.setOnClickListener(v -> {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_txid), Toast.LENGTH_SHORT).show();
});
@ -105,23 +102,22 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
pbXmrto = view.findViewById(R.id.pbXmrto);
pbXmrto.getIndeterminateDrawable().setColorFilter(0x61000000, android.graphics.PorterDuff.Mode.MULTIPLY);
final TextView tvXmrToLabel = view.findViewById(R.id.tvXmrToLabel);
tvXmrToLabel.setText(getString(R.string.info_send_xmrto_success_order_label, shiftService.getLabel()));
tvTxXmrToKey = view.findViewById(R.id.tvTxXmrToKey);
tvTxXmrToKey.setOnClickListener(v -> {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
});
tvXmrToSupport = view.findViewById(R.id.tvXmrToSupport);
tvXmrToSupport.setPaintFlags(tvXmrToSupport.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
tvXmrToSupport.setText(getString(R.string.label_send_btc_xmrto_info, shiftService.getLabel()));
return view;
}
@Override
public boolean onValidateFields() {
return true;
}
private boolean isResumed = false;
@Override
@ -130,7 +126,15 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
super.onPauseFragment();
}
TxDataBtc btcData = null;
private TxDataBtc getTxData() {
final TxData txData = sendListener.getTxData();
if (txData == null) throw new IllegalStateException("TxDataBtc is null");
if (txData instanceof TxDataBtc) {
return (TxDataBtc) txData;
} else {
throw new IllegalStateException("TxData not BTC");
}
}
@Override
public void onResumeFragment() {
@ -139,8 +143,8 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
Helper.hideKeyboard(getActivity());
isResumed = true;
btcData = (TxDataBtc) sendListener.getTxData();
tvTxAddress.setText(btcData.getDestination());
final TxDataBtc txData = getTxData();
tvTxAddress.setText(txData.getDestination());
final PendingTx committedTx = sendListener.getCommittedTx();
if (committedTx != null) {
@ -148,45 +152,41 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
bCopyTxId.setEnabled(true);
tvTxAmount.setText(getString(R.string.send_amount, Helper.getDisplayAmount(committedTx.amount)));
tvTxFee.setText(getString(R.string.send_fee, Helper.getDisplayAmount(committedTx.fee)));
if (btcData != null) {
NumberFormat df = NumberFormat.getInstance(Locale.US);
df.setMaximumFractionDigits(12);
String btcAmount = df.format(btcData.getBtcAmount());
tvXmrToAmount.setText(getString(R.string.info_send_xmrto_success_btc, btcAmount, btcData.getBtcSymbol()));
//TODO btcData.getBtcAddress();
tvTxXmrToKey.setText(btcData.getXmrtoOrderId());
final Crypto crypto = Crypto.withSymbol(btcData.getBtcSymbol());
ivXmrToIcon.setImageResource(crypto.getIconEnabledId());
tvXmrToSupport.setOnClickListener(v -> {
Uri orderUri = getXmrToApi().getQueryOrderUri(btcData.getXmrtoOrderId());
Intent intent = new Intent(Intent.ACTION_VIEW, orderUri);
startActivity(intent);
});
queryOrder();
} else {
throw new IllegalStateException("btcData is null");
}
NumberFormat df = NumberFormat.getInstance(Locale.US);
df.setMaximumFractionDigits(12);
String btcAmount = df.format(txData.getBtcAmount());
tvXmrToAmount.setText(getString(R.string.info_send_xmrto_success_btc, btcAmount, txData.getBtcSymbol()));
//TODO btcData.getBtcAddress();
tvTxXmrToKey.setText(txData.getXmrtoOrderId());
final Crypto crypto = Crypto.withSymbol(txData.getBtcSymbol());
assert crypto != null;
ivXmrToIcon.setImageResource(crypto.getIconEnabledId());
tvXmrToSupport.setOnClickListener(v -> {
startActivity(new Intent(Intent.ACTION_VIEW, txData.getShiftService().getShiftApi().getQueryOrderUri(txData.getXmrtoOrderId())));
});
queryOrder();
}
sendListener.enableDone();
}
private void processQueryOrder(final QueryOrderStatus status) {
Timber.d("processQueryOrder %s for %s", status.getState().toString(), status.getOrderId());
if (!btcData.getXmrtoOrderId().equals(status.getOrderId()))
Timber.d("processQueryOrder %s for %s", status.getStatus().toString(), status.getOrderId());
if (!getTxData().getXmrtoOrderId().equals(status.getOrderId()))
throw new IllegalStateException("UUIDs do not match!");
if (isResumed && (getView() != null))
getView().post(() -> {
showXmrToStatus(status);
if (!status.isTerminal()) {
getView().postDelayed(this::queryOrder, SideShiftApi.QUERY_INTERVAL);
getView().postDelayed(this::queryOrder, ShiftApi.QUERY_INTERVAL);
}
});
}
private void queryOrder() {
final TxDataBtc btcData = getTxData();
Timber.d("queryOrder(%s)", btcData.getXmrtoOrderId());
if (!isResumed) return;
getXmrToApi().queryOrderStatus(btcData.getXmrtoOrderId(), new ShiftCallback<QueryOrderStatus>() {
btcData.getShiftService().getShiftApi().queryOrderStatus(btcData.getXmrtoQueryOrderToken(), new ShiftCallback<QueryOrderStatus>() {
@Override
public void onSuccess(QueryOrderStatus status) {
if (!isAdded()) return;
@ -197,45 +197,54 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
public void onError(final Exception ex) {
if (!isResumed) return;
Timber.w(ex);
getActivity().runOnUiThread(() -> {
if (ex instanceof ShiftException) {
Toast.makeText(getActivity(), ((ShiftException) ex).getError().getErrorMsg(), Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getActivity(), ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
}
});
if (ex instanceof SocketTimeoutException) {
// try again
if (isResumed && (getView() != null))
getView().post(() -> {
getView().postDelayed(SendBtcSuccessWizardFragment.this::queryOrder, ShiftApi.QUERY_INTERVAL);
});
} else {
requireActivity().runOnUiThread(() -> {
if (ex instanceof ShiftException) {
Toast.makeText(getActivity(), ((ShiftException) ex).getErrorMessage(), Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getActivity(), ex.getLocalizedMessage(), Toast.LENGTH_LONG).show();
}
});
}
}
});
}
void showXmrToStatus(final QueryOrderStatus status) {
int statusResource = 0;
final TxDataBtc txData = getTxData();
if (status.isError()) {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_error, status.toString()));
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_error, txData.getShiftService().getLabel(), status.toString()));
statusResource = R.drawable.ic_error_red_24dp;
pbXmrto.getIndeterminateDrawable().setColorFilter(
ThemeHelper.getThemedColor(getContext(), android.R.attr.colorError),
ThemeHelper.getThemedColor(requireContext(), R.attr.negativeColor),
android.graphics.PorterDuff.Mode.MULTIPLY);
} else if (status.isSent() || status.isPaid()) {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_sent, btcData.getBtcSymbol()));
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_sent, txData.getBtcSymbol()));
statusResource = R.drawable.ic_success;
pbXmrto.getIndeterminateDrawable().setColorFilter(
ThemeHelper.getThemedColor(getContext(), R.attr.positiveColor),
ThemeHelper.getThemedColor(requireContext(), R.attr.positiveColor),
android.graphics.PorterDuff.Mode.MULTIPLY);
} else if (status.isWaiting()) {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_unpaid));
statusResource = R.drawable.ic_pending;
pbXmrto.getIndeterminateDrawable().setColorFilter(
ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor),
ThemeHelper.getThemedColor(requireContext(), R.attr.neutralColor),
android.graphics.PorterDuff.Mode.MULTIPLY);
} else if (status.isPending()) {
tvXmrToStatus.setText(getString(R.string.info_send_xmrto_paid));
statusResource = R.drawable.ic_pending;
pbXmrto.getIndeterminateDrawable().setColorFilter(
ThemeHelper.getThemedColor(getContext(), R.attr.neutralColor),
ThemeHelper.getThemedColor(requireContext(), R.attr.neutralColor),
android.graphics.PorterDuff.Mode.MULTIPLY);
} else {
throw new IllegalStateException("status is broken: " + status.toString());
throw new IllegalStateException("status is broken: " + status);
}
ivXmrToStatus.setImageResource(statusResource);
if (status.isTerminal()) {
@ -246,17 +255,4 @@ public class SendBtcSuccessWizardFragment extends SendWizardFragment {
ivXmrToStatusBig.setVisibility(View.VISIBLE);
}
}
private SideShiftApi xmrToApi = null;
private SideShiftApi getXmrToApi() {
if (xmrToApi == null) {
synchronized (this) {
if (xmrToApi == null) {
xmrToApi = new SideShiftApiImpl(ServiceHelper.getXmrToBaseUrl());
}
}
}
return xmrToApi;
}
}

View File

@ -38,18 +38,13 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
public static SendConfirmWizardFragment newInstance(Listener listener) {
SendConfirmWizardFragment instance = new SendConfirmWizardFragment();
instance.setSendListener(listener);
instance.sendListener = listener;
return instance;
}
Listener sendListener;
private Listener sendListener;
public SendConfirmWizardFragment setSendListener(Listener listener) {
this.sendListener = listener;
return this;
}
interface Listener {
public interface Listener {
SendFragment.Listener getActivityCallback();
TxData getTxData();
@ -137,7 +132,7 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
void send() {
sendListener.commitTransaction();
getActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
requireActivity().runOnUiThread(() -> pbProgressSend.setVisibility(View.VISIBLE));
}
@Override
@ -153,7 +148,7 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
}
private void showAlert(String title, String message) {
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(getActivity());
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireActivity());
builder.setCancelable(true).
setTitle(title).
setMessage(message).
@ -235,7 +230,7 @@ public class SendConfirmWizardFragment extends SendWizardFragment implements Sen
}
public void fail(String walletName) {
getActivity().runOnUiThread(() -> {
requireActivity().runOnUiThread(() -> {
bSend.setEnabled(true); // allow to try again
});
}

View File

@ -56,6 +56,7 @@ import com.m2049r.xmrwallet.widget.Toolbar;
import java.lang.ref.WeakReference;
import lombok.Getter;
import timber.log.Timber;
public class SendFragment extends Fragment
@ -67,6 +68,7 @@ public class SendFragment extends Fragment
final static public int MIXIN = 0;
@Getter
private Listener activityCallback;
public interface Listener {
@ -273,7 +275,7 @@ public class SendFragment extends Fragment
return false;
}
enum Mode {
public enum Mode {
XMR, BTC
}
@ -293,7 +295,7 @@ public class SendFragment extends Fragment
default:
throw new IllegalArgumentException("Mode " + String.valueOf(aMode) + " unknown!");
}
getView().post(() -> pagerAdapter.notifyDataSetChanged());
requireView().post(() -> pagerAdapter.notifyDataSetChanged());
Timber.d("New Mode = %s", mode.toString());
}
}
@ -478,11 +480,6 @@ public class SendFragment extends Fragment
bDone.setVisibility(View.VISIBLE);
}
public Listener getActivityCallback() {
return activityCallback;
}
// callbacks from send service
public void onTransactionCreated(final String txTag, final PendingTransaction pendingTransaction) {
@ -502,12 +499,9 @@ public class SendFragment extends Fragment
activityCallback.onDisposeRequest();
}
@Getter
PendingTx pendingTx;
public PendingTx getPendingTx() {
return pendingTx;
}
public void onCreateTransactionFailed(String errorText) {
final SendConfirm confirm = getSendConfirm();
if (confirm != null) {

View File

@ -36,18 +36,13 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
public static SendSuccessWizardFragment newInstance(Listener listener) {
SendSuccessWizardFragment instance = new SendSuccessWizardFragment();
instance.setSendListener(listener);
instance.sendListener = listener;
return instance;
}
Listener sendListener;
private Listener sendListener;
public SendSuccessWizardFragment setSendListener(Listener listener) {
this.sendListener = listener;
return this;
}
interface Listener {
public interface Listener {
TxData getTxData();
PendingTx getCommittedTx();
@ -62,7 +57,6 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
ImageButton bCopyTxId;
private TextView tvTxId;
private TextView tvTxAddress;
private TextView tvTxPaymentId;
private TextView tvTxAmount;
private TextView tvTxFee;
@ -78,13 +72,12 @@ public class SendSuccessWizardFragment extends SendWizardFragment {
bCopyTxId = view.findViewById(R.id.bCopyTxId);
bCopyTxId.setEnabled(false);
bCopyTxId.setOnClickListener(v -> {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
Helper.clipBoardCopy(requireActivity(), getString(R.string.label_send_txid), tvTxId.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_txid), Toast.LENGTH_SHORT).show();
});
tvTxId = view.findViewById(R.id.tvTxId);
tvTxAddress = view.findViewById(R.id.tvTxAddress);
tvTxPaymentId = view.findViewById(R.id.tvTxPaymentId);
tvTxAmount = view.findViewById(R.id.tvTxAmount);
tvTxFee = view.findViewById(R.id.tvTxFee);

View File

@ -0,0 +1,26 @@
package com.m2049r.xmrwallet.fragment.send;
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
public interface Shifter {
void invalidateShift();
void onQuoteError(final Exception ex);
void showQuoteError();
void onQuoteReceived(RequestQuote quote);
void onOrderCreated(CreateOrder order);
void onOrderError(final Exception ex);
boolean isActive();
void showProgress(Shifter.Stage stage);
enum Stage {
X, A, B, C
}
}

View File

@ -0,0 +1,149 @@
/*
* Copyright (c) 2018 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.
*/
package com.m2049r.xmrwallet.layout;
import android.annotation.SuppressLint;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.BluetoothInfo;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class BluetoothInfoAdapter extends RecyclerView.Adapter<BluetoothInfoAdapter.ViewHolder> {
public interface OnInteractionListener {
void onInteraction(View view, BluetoothInfo item);
}
private final List<BluetoothInfo> items = new ArrayList<>();
private final OnInteractionListener listener;
public BluetoothInfoAdapter(OnInteractionListener listener) {
this.listener = listener;
}
private static class BluetoothInfoDiff extends DiffCallback<BluetoothInfo> {
public BluetoothInfoDiff(List<BluetoothInfo> oldList, List<BluetoothInfo> newList) {
super(oldList, newList);
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return mOldList.get(oldItemPosition).getAddress().equals(mNewList.get(newItemPosition).getAddress());
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
final BluetoothInfo oldItem = mOldList.get(oldItemPosition);
final BluetoothInfo newItem = mNewList.get(newItemPosition);
return oldItem.equals(newItem);
}
}
@Override
public @NonNull
ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_bluetooth, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(final @NonNull ViewHolder holder, int position) {
holder.bind(position);
}
@Override
public int getItemCount() {
return items.size();
}
public void add(BluetoothInfo item) {
if (item == null) return;
List<BluetoothInfo> newItems = new ArrayList<>(items);
if (!items.contains(item))
newItems.add(item);
setItems(newItems); // in case the nodeinfo has changed
}
public void setItems(Collection<BluetoothInfo> newItemsCollection) {
List<BluetoothInfo> newItems;
if (newItemsCollection != null) {
newItems = new ArrayList<>(newItemsCollection);
Collections.sort(newItems, BluetoothInfo.NameComparator);
} else {
newItems = new ArrayList<>();
}
final BluetoothInfoAdapter.BluetoothInfoDiff diffCallback = new BluetoothInfoAdapter.BluetoothInfoDiff(items, newItems);
final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback);
items.clear();
items.addAll(newItems);
diffResult.dispatchUpdatesTo(this);
}
private boolean itemsClickable = true;
@SuppressLint("NotifyDataSetChanged")
public void allowClick(boolean clickable) {
itemsClickable = clickable;
notifyDataSetChanged();
}
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
final TextView tvName;
final TextView tvAddress;
BluetoothInfo item;
ViewHolder(View itemView) {
super(itemView);
tvName = itemView.findViewById(R.id.tvName);
tvAddress = itemView.findViewById(R.id.tvAddress);
itemView.setOnClickListener(this);
}
void bind(int position) {
item = items.get(position);
tvName.setText(item.getName());
tvAddress.setText(item.getAddress());
itemView.setClickable(itemsClickable);
itemView.setEnabled(itemsClickable);
}
@Override
public void onClick(View view) {
if (listener != null) {
int position = getBindingAdapterPosition(); // gets item position
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
final BluetoothInfo node = items.get(position);
allowClick(false);
listener.onInteraction(view, node);
}
}
}
}
}

View File

@ -16,6 +16,7 @@
package com.m2049r.xmrwallet.layout;
import android.annotation.SuppressLint;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
@ -131,12 +132,13 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
private boolean itemsClickable = true;
@SuppressLint("NotifyDataSetChanged")
public void allowClick(boolean clickable) {
itemsClickable = clickable;
notifyDataSetChanged();
}
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
final ImageButton ibBookmark;
final View pbBookmark;
final TextView tvName;
@ -195,7 +197,7 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
@Override
public void onClick(View view) {
if (listener != null) {
int position = getAdapterPosition(); // gets item position
int position = getBindingAdapterPosition(); // gets item position
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
final NodeInfo node = nodeItems.get(position);
if (node.isOnion()) {
@ -218,7 +220,7 @@ public class NodeInfoAdapter extends RecyclerView.Adapter<NodeInfoAdapter.ViewHo
@Override
public boolean onLongClick(View view) {
if (listener != null) {
int position = getAdapterPosition(); // gets item position
int position = getBindingAdapterPosition(); // gets item position
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
return listener.onLongInteraction(view, nodeItems.get(position));
}

View File

@ -89,6 +89,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
return items.size();
}
@NonNull
public Subaddress getItem(int position) {
return items.get(position);
}
@ -108,7 +109,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
diffResult.dispatchUpdatesTo(this);
}
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener {
final TextView tvName;
final TextView tvAddress;
final TextView tvAmount;
@ -143,7 +144,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
@Override
public void onClick(View view) {
if (listener != null) {
int position = getAdapterPosition(); // gets item position
int position = getBindingAdapterPosition(); // gets item position
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
listener.onInteraction(view, getItem(position));
}
@ -153,7 +154,7 @@ public class SubaddressInfoAdapter extends RecyclerView.Adapter<SubaddressInfoAd
@Override
public boolean onLongClick(View view) {
if (listener != null) {
int position = getAdapterPosition(); // gets item position
int position = getBindingAdapterPosition(); // gets item position
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
return listener.onLongInteraction(view, getItem(position));
}

View File

@ -16,6 +16,7 @@
package com.m2049r.xmrwallet.layout;
import android.annotation.SuppressLint;
import android.content.Context;
import android.text.Html;
import android.text.Spanned;
@ -49,6 +50,7 @@ import java.util.TimeZone;
import timber.log.Timber;
public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfoAdapter.ViewHolder> {
@SuppressLint("SimpleDateFormat")
private final static SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
private final int outboundColour;
@ -86,7 +88,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
}
public boolean needsTransactionUpdateOnNewBlock() {
return (infoItems.size() > 0) && !infoItems.get(0).isConfirmed();
return (!infoItems.isEmpty()) && !infoItems.get(0).isConfirmed();
}
private static class TransactionInfoDiff extends DiffCallback<TransactionInfo> {
@ -156,7 +158,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
return infoItems.get(position);
}
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
final ImageView ivTxType;
final TextView tvAmount;
final TextView tvFailed;
@ -291,7 +293,7 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
@Override
public void onClick(View view) {
if (listener != null) {
int position = getAdapterPosition(); // gets item position
int position = getBindingAdapterPosition(); // gets item position
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
listener.onInteraction(view, infoItems.get(position));
}

View File

@ -16,6 +16,7 @@
package com.m2049r.xmrwallet.layout;
import android.annotation.SuppressLint;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.MenuItem;
@ -44,6 +45,7 @@ import timber.log.Timber;
public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.ViewHolder> {
@SuppressLint("SimpleDateFormat")
private final SimpleDateFormat DATETIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm");
public interface OnInteractionListener {
@ -120,7 +122,7 @@ public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.Vi
diffResult.dispatchUpdatesTo(this);
}
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
final TextView tvName;
final ImageButton ibOptions;
WalletManager.WalletInfo infoItem;
@ -164,7 +166,7 @@ public class WalletInfoAdapter extends RecyclerView.Adapter<WalletInfoAdapter.Vi
@Override
public void onClick(View view) {
if (listener != null) {
int position = getAdapterPosition(); // gets item position
int position = getBindingAdapterPosition(); // gets item position
if (position != RecyclerView.NO_POSITION) { // Check if an item was deleted, but the user clicked it before the UI removed it
listener.onInteraction(view, infoItems.get(position));
}

View File

@ -0,0 +1,4 @@
package com.m2049r.xmrwallet.ledger;
public interface Hardware {
}

View File

@ -35,12 +35,11 @@ import java.nio.charset.StandardCharsets;
import timber.log.Timber;
public class Ledger {
public class Ledger implements Hardware {
static final public boolean ENABLED = true;
// 5:20 is same as wallet2.cpp::restore()
static public final int LOOKAHEAD_ACCOUNTS = 5;
static public final int LOOKAHEAD_SUBADDRESSES = 20;
static public final String SUBADDRESS_LOOKAHEAD = LOOKAHEAD_ACCOUNTS + ":" + LOOKAHEAD_SUBADDRESSES;
private static final byte PROTOCOL_VERSION = 0x03;
public static final int SW_OK = 0x9000;

View File

@ -111,7 +111,11 @@ public class Wallet {
@RequiredArgsConstructor
@Getter
public enum Device {
Device_Undefined(0, 0), Device_Software(50, 200), Device_Ledger(5, 20);
Undefined(0, 0),
Software(50, 200),
Ledger(5, 20),
Trezor(5, 20),
Sidekick(5, 20);
private final int accountLookahead;
private final int subaddressLookahead;
}

View File

@ -18,7 +18,6 @@ package com.m2049r.xmrwallet.model;
import com.m2049r.xmrwallet.XmrWalletApplication;
import com.m2049r.xmrwallet.data.Node;
import com.m2049r.xmrwallet.ledger.Ledger;
import com.m2049r.xmrwallet.util.RestoreHeight;
import java.io.File;
@ -161,10 +160,12 @@ public class WalletManager {
String spendKeyString);
public Wallet createWalletFromDevice(File aFile, String password, long restoreHeight,
String deviceName) {
Wallet.Device device) {
final String lookahead = device.getAccountLookahead() + ":" + device.getSubaddressLookahead();
Timber.d("Creating from %s with %s lookahead", device, lookahead);
long walletHandle = createWalletFromDeviceJ(aFile.getAbsolutePath(), password,
getNetworkType().getValue(), deviceName, restoreHeight,
Ledger.SUBADDRESS_LOOKAHEAD);
getNetworkType().getValue(), device.name(), restoreHeight,
lookahead);
Wallet wallet = new Wallet(walletHandle);
manageWallet(wallet);
return wallet;

View File

@ -19,6 +19,8 @@ package com.m2049r.xmrwallet.onboarding;
import android.content.Context;
import android.content.SharedPreferences;
import com.m2049r.xmrwallet.util.Helper;
import java.util.Date;
import timber.log.Timber;

View File

@ -0,0 +1,662 @@
/*
* Copyright (C) 2021 m2049r@monerujo.io
* Copyright (C) 2014 The Android Open Source Project
*
* 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.
*/
// mostly from BluetoothChatService https://github.com/android/connectivity-samples
package com.m2049r.xmrwallet.service;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.os.Handler;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import lombok.Getter;
import lombok.Setter;
import timber.log.Timber;
/**
* This class does all the work for setting up and managing Bluetooth
* connections with other devices. It has a thread that listens for
* incoming connections, a thread for connecting with a device, and a
* thread for performing data transmissions when connected.
*/
public class BluetoothService {
final static private byte[] MAGIC = "SIDEKICK".getBytes(StandardCharsets.US_ASCII);
public interface MessageType {
int STATE_CHANGE = 1;
int READ = 2;
int WRITE = 3;
int DEVICE_NAME = 4;
int TOAST = 5;
int READ_CMD = 6;
int CODE = 42;
}
//TODO refactor this using an enum with resource ids for messages and stuff
public interface Toasts {
String CONNECT_FAILED = "Unable to connect device";
String CONNECTION_LOST = "Device connection was lost";
int READ = 2;
int WRITE = 3;
int DEVICE_NAME = 4;
int TOAST = 5;
}
// Constants that indicate the current connection state
public interface State {
int NONE = 0; // we're doing nothing
int LISTEN = 1; // now listening for incoming connections
int CONNECTING = 2; // now initiating an outgoing connection
int CONNECTED = 3; // now connected to a remote device
}
// Name for the SDP record when creating server socket
private static final String SDP_NAME = "Monerujo";
// Unique UUID for this application
private static final UUID SDP_UUID = UUID.fromString("2150154b-58ce-4c58-99e3-ccfdd14bed3b");
static final BluetoothService Instance = new BluetoothService();
public static BluetoothService GetInstance() {
return Instance;
}
public static boolean IsConnected() {
return Instance.isConnected();
}
public static void Stop() {
Instance.stop();
}
// Member fields
private final BluetoothAdapter bluetoothAdapter;
@Setter
private Handler uiHandler;
@Setter
private Handler commHandler;
private AcceptThread acceptThread;
private ConnectThread connectThread;
private ConnectedThread connectedThread;
@Getter
private int state = State.NONE;
@Getter
private String connectedName;
@Getter
private String connectedCode;
public boolean isStarted() {
return state != State.NONE;
}
public boolean isConnected() {
return state == State.CONNECTED;
}
public BluetoothService() {
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
}
/**
* Notify UI of state changes
*/
private synchronized void onStateChanged() {
Timber.d("onStateChanged() -> %s", state);
if (uiHandler != null)
uiHandler.obtainMessage(MessageType.STATE_CHANGE, state, -1).sendToTarget();
}
/**
* Notify UI that we've connected
*/
private synchronized void onConnected(String remoteName) {
Timber.d("onConnected() -> %s", remoteName);
connectedName = remoteName;
if (uiHandler != null)
uiHandler.obtainMessage(MessageType.DEVICE_NAME, remoteName).sendToTarget();
}
/**
* Start the service: Start AcceptThread to begin a session in listening (server) mode.
*/
public synchronized void start() {
Timber.d("start");
halt = false;
// Cancel any thread attempting to make a connection
if (connectThread != null) {
connectThread.cancel();
connectThread = null;
}
// Cancel any thread currently running a connection
if (connectedThread != null) {
connectedThread.cancel();
connectedThread = null;
}
// Start the thread to listen on a BluetoothServerSocket (or let it run if already running)
if (acceptThread == null) {
acceptThread = new AcceptThread();
acceptThread.start();
}
onStateChanged();
}
/**
* Start the ConnectThread to initiate a connection to a remote device.
*
* @param device The BluetoothDevice to connect
*/
public synchronized void connect(BluetoothDevice device) {
Timber.d("connect to: %s", device);
// Cancel any thread attempting to make a connection
if (state == State.CONNECTING) {
if (connectThread != null) {
connectThread.cancel();
connectThread = null;
}
}
// Cancel any thread currently running a connection
if (connectedThread != null) {
reconnect = true;
connectedThread.cancel();
connectedThread = null;
}
// Start the thread to connect with the given device
connectThread = new ConnectThread(device);
connectThread.start();
onStateChanged();
}
boolean reconnect = false;
boolean halt = false;
/**
* Start the ConnectedThread to begin managing a Bluetooth connection
*
* @param socket The BluetoothSocket on which the connection was made
* @param device The BluetoothDevice that has been connected
*/
public synchronized void startConnected(BluetoothSocket socket, BluetoothDevice device) {
Timber.d("startConnected");
// Cancel the thread that completed the connection
if (connectThread != null) {
connectThread.cancel();
connectThread = null;
}
// Cancel any thread currently running a connection
if (connectedThread != null) {
connectedThread.cancel();
connectedThread = null;
}
// Cancel the accept thread because we only want to connect to one device
if (acceptThread != null) {
acceptThread.cancel();
acceptThread = null;
}
// Start the thread to manage the connection and perform transmissions
connectedThread = new ConnectedThread(socket);
connectedThread.start();
onConnected(device.getName());
onStateChanged();
}
/**
* Stop all threads
*/
public synchronized void stop() {
Timber.d("stop");
halt = true;
if (connectThread != null) {
connectThread.cancel();
connectThread = null;
}
if (connectedThread != null) {
connectedThread.cancel();
connectedThread = null;
}
if (acceptThread != null) {
acceptThread.cancel();
acceptThread = null;
}
state = State.NONE;
onStateChanged();
setUiHandler(null);
}
/**
* Write to the ConnectedThread in an unsynchronized manner
*
* @param out The bytes to write
* @see ConnectedThread#write(byte[])
*/
public boolean write(byte[] out) {
// Synchronize a copy of the ConnectedThread
ConnectedThread connectedThread;
synchronized (this) {
if (state != State.CONNECTED) return false;
connectedThread = this.connectedThread;
}
// Perform the write itself unsynchronized
return connectedThread.write(out);
}
public boolean write(int code) {
// Synchronize a copy of the ConnectedThread
ConnectedThread connectedThread;
synchronized (this) {
if (state != State.CONNECTED) return false;
connectedThread = this.connectedThread;
}
final byte[] buffer = new byte[2];
buffer[0] = (byte) (code >> 8);
buffer[1] = (byte) (code & 0xff);
connectedCode = String.format(Locale.US, "%04d", code);
if (uiHandler != null)
uiHandler.obtainMessage(MessageType.CODE, connectedCode).sendToTarget();
return connectedThread.write(buffer);
}
public byte[] exchange(byte[] buffer) {
// Synchronize a copy of the ConnectedThread
ConnectedThread connectedThread;
synchronized (this) {
if (state != State.CONNECTED) return null; //TODO maybe exception?
connectedThread = this.connectedThread;
}
CountDownLatch signal = new CountDownLatch(1);
connectedThread.setReadSignal(signal);
connectedThread.write(buffer);
try {
signal.await(); //TODO what happens when the reader is canceled?
return connectedThread.getReadBuffer();
} catch (InterruptedException ex) {
Timber.d(ex);
return null;
}
}
/**
* Indicate that the connection attempt failed and notify
*/
private void onConnectFailed() {
Timber.d("onConnectFailed()");
if (uiHandler != null)
uiHandler.obtainMessage(MessageType.TOAST, Toasts.CONNECT_FAILED).sendToTarget();
state = State.NONE;
// don't notify as start() notifies immediately afterwards
// onStateChanged();
// Start the service over to restart listening mode
if (!halt) start();
}
/**
* Indicate that the connection was lost
*/
private void onConnectionLost() {
Timber.d("onConnectionLost()");
connectedName = null;
connectedCode = null;
if (reconnect) return;
state = State.NONE;
if (halt) return;
// don't notify as start() notifies immediately afterwards
// onStateChanged();
// Start the service over to restart listening mode
start();
}
/**
* This thread runs while listening for incoming connections. It behaves
* like a server-side client. It runs until a connection is accepted
* (or until cancelled).
*/
private class AcceptThread extends Thread {
private final BluetoothServerSocket serverSocket;
public AcceptThread() {
// Create a new listening server socket
try {
serverSocket = bluetoothAdapter.listenUsingRfcommWithServiceRecord(SDP_NAME, SDP_UUID);
state = BluetoothService.State.LISTEN;
} catch (IOException ex) {
Timber.d(ex, "listen() failed");
throw new IllegalStateException();
}
}
public void run() {
Timber.d("BEGIN AcceptThread %s", this);
BluetoothSocket socket;
// Listen to the server socket if we're not connected
while (state != BluetoothService.State.CONNECTED) {
try {
// This is a blocking call and will only return on a
// successful connection or an exception
socket = serverSocket.accept();
} catch (IOException ex) {
Timber.d(ex, "accept() failed"); // this also happens on socket.close()
break;
}
// If a connection was accepted
if (socket != null) {
synchronized (BluetoothService.this) {
switch (state) {
case BluetoothService.State.LISTEN:
case BluetoothService.State.CONNECTING:
// Situation normal. Start the ConnectedThread.
startConnected(socket, socket.getRemoteDevice());
break;
case BluetoothService.State.NONE:
case BluetoothService.State.CONNECTED:
// Either not ready or already connected. Terminate new socket.
try {
socket.close();
} catch (IOException ex) {
Timber.d(ex, "Could not close unwanted socket");
}
break;
}
}
}
}
Timber.d("END AcceptThread %s", this);
}
public void cancel() {
Timber.d("cancel() %s", this);
try {
serverSocket.close();
} catch (IOException ex) {
Timber.d(ex, "close() of server failed");
}
}
}
/**
* This thread runs while attempting to make an outgoing connection
* with a device. It runs straight through; the connection either
* succeeds or fails.
*/
private class ConnectThread extends Thread {
private final BluetoothSocket socket;
private final BluetoothDevice device;
public ConnectThread(BluetoothDevice device) {
this.device = device;
// Create a BluetoothSocket
try {
socket = device.createRfcommSocketToServiceRecord(SDP_UUID);
state = BluetoothService.State.CONNECTING;
} catch (IOException ex) {
Timber.d(ex, "create() failed");
throw new IllegalStateException(); //TODO really die here?
}
}
public void run() {
Timber.d("BEGIN ConnectThread");
// Always cancel discovery because it will slow down a connection
bluetoothAdapter.cancelDiscovery(); //TODO show & remember discovery state?
// Make a connection to the BluetoothSocket
try {
// This is a blocking call and will only return on a
// successful connection or an exception
socket.connect(); // sometimes this fails - why?
} catch (IOException ex) {
try {
socket.close();
} catch (IOException exClose) {
Timber.d(exClose, "unable to close() socket during connection failure");
}
onConnectFailed();
return;
} finally {
// Reset the ConnectThread because we're done
synchronized (BluetoothService.this) {
connectThread = null;
}
}
// Start the ConnectedThread
startConnected(socket, device);
}
public void cancel() {
try {
socket.close();
} catch (IOException ex) {
Timber.d(ex, "close() of connect socket failed");
}
}
}
/**
* This thread runs during a connection with a remote device.
* It handles all incoming and outgoing transmissions.
*/
private class ConnectedThread extends Thread {
private final BluetoothSocket socket;
private final InputStream in;
private final OutputStream out;
private final ByteArrayOutputStream bytesIn = new ByteArrayOutputStream();
private final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
private CountDownLatch readSignal = null; // TODO this needs to be a Map with correlationIds
public void setReadSignal(CountDownLatch signal) { //TODO see above
readSignal = signal;
readBuffer = null;
}
@Getter
private byte[] readBuffer = null;
public ConnectedThread(BluetoothSocket bluetoothSocket) {
Timber.d("ConnectedThread()");
socket = bluetoothSocket;
InputStream tmpIn = null;
OutputStream tmpOut;
// Get the BluetoothSocket input and output streams
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException ex) {
Timber.d(ex, "temp sockets not created");
if (tmpIn != null)
try {
tmpIn.close();
} catch (IOException exIn) {
Timber.d(exIn);
}
throw new IllegalStateException();
}
in = tmpIn;
out = tmpOut;
state = BluetoothService.State.CONNECTED;
}
public void run() {
Timber.d("BEGIN ConnectedThread %s", this);
final byte[] buffer = new byte[4096];
int bytesRead;
// Keep listening to the InputStream while connected
while (state == BluetoothService.State.CONNECTED) {
// Protocol: "SIDEKICK"|1-byte:reserved|2-bytes:length|buffer
try {
// protocol header
bytesRead = in.read(buffer, 0, MAGIC.length + 3);
int a = in.available() + bytesRead;
if (bytesRead != MAGIC.length + 3)
throw new IllegalStateException("message too short");
for (int i = 0; i < MAGIC.length; i++) {
if (buffer[i] != MAGIC[i]) throw new IllegalStateException("no MAGIC");
}
final int options = buffer[MAGIC.length]; // 0 regular message, else CODE instead of payload length
final int payloadLength = ((0xff & buffer[MAGIC.length + 1]) << 8) + (0xff & buffer[MAGIC.length + 2]);
Timber.d("READ options %d, payloadLength=%d, available=%d", options, payloadLength, a);
if ((options & 0x01) != 0) { // CODE
connectedCode = String.format(Locale.US, "%04d", payloadLength);
if (uiHandler != null)
uiHandler.obtainMessage(MessageType.CODE, connectedCode).sendToTarget();
continue;
}
int remainingBytes = payloadLength;
bytesIn.reset();
while (remainingBytes > 0) {
bytesRead = in.read(buffer, 0, Math.min(remainingBytes, buffer.length));
remainingBytes -= bytesRead;
bytesIn.write(buffer, 0, bytesRead);
}
readBuffer = bytesIn.toByteArray();
if (readSignal != null) { // someone is awaiting this
readSignal.countDown();
} else if (commHandler != null) { // we are the counterparty
final int command = readBuffer[0];
commHandler.obtainMessage(command, readBuffer).sendToTarget();
if (uiHandler != null) {
uiHandler.obtainMessage(MessageType.READ_CMD, readBuffer.length, command).sendToTarget();
}
} else {
throw new IllegalStateException("would drop a message");
}
} catch (IOException ex) {
Timber.d(ex, "disconnected");
if (readSignal != null) readSignal.countDown(); // readBudder is still null
onConnectionLost();
reconnect = false;
break;
}
}
Timber.d("END ConnectedThread %s", this);
}
/**
* Write to the connected OutStream.
* <p>
* Protocol: "SIDEKICK"|1-byte:reserved|2-bytes:length|buffer
*
* @param buffer The bytes to write
*/
public boolean write(byte[] buffer) {
boolean sendCode = buffer.length == 2; // TODO undo this hack
try {
final int len = buffer.length;
if (len > 65535) {
Timber.w("buffer too long %d", len);
return false;
}
bytesOut.reset();
bytesOut.write(MAGIC);
if (sendCode) {
bytesOut.write(0x01); // options bit 0 is CODE
} else {
bytesOut.write(0);
bytesOut.write(len >> 8);
bytesOut.write(len & 0xff);
}
bytesOut.write(buffer);
out.write(bytesOut.toByteArray());
if (uiHandler != null) {
uiHandler.obtainMessage(MessageType.WRITE, buffer.length, -1).sendToTarget();
}
} catch (IOException ex) {
Timber.d(ex, "Exception during write");
return false;
//TODO probably kill the connection if this happens?
// but the read operation probably throws it as well, and that takes care of that!
}
return true;
}
public void cancel() {
try {
socket.close();
} catch (IOException ex) {
Timber.d(ex, "close() of connect socket failed");
}
}
}
// for direct communication from JNI
// this should block
static public byte[] Exchange(byte[] request) {
Timber.d("EXCHANGE req = %d bytes", request.length);
final byte[] response = Instance.exchange(request);
Timber.d("EXCHANGE resp = %d bytes", response.length);
return response;
}
static public boolean Write(byte[] request) {
return Instance.write(request);
}
}

View File

@ -26,7 +26,7 @@ import android.os.Process;
/**
* Handy class for starting a new thread that has a looper. The looper can then be
* used to create handler classes. Note that start() must still be called.
* The started Thread has a stck size of STACK_SIZE (=5MB)
* The started Thread has a stack size of STACK_SIZE (=5MB)
*/
public class MoneroHandlerThread extends Thread {
// from src/cryptonote_config.h

View File

@ -16,6 +16,8 @@
package com.m2049r.xmrwallet.service;
import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@ -581,7 +583,11 @@ public class WalletService extends Service {
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.setContentIntent(pendingIntent)
.build();
startForeground(NOTIFICATION_ID, notification);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(NOTIFICATION_ID, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
} else {
startForeground(NOTIFICATION_ID, notification);
}
}
@RequiresApi(Build.VERSION_CODES.O)

View File

@ -142,9 +142,13 @@ public class ExchangeApiImpl implements ExchangeApi {
} catch (ParserConfigurationException | SAXException ex) {
Timber.w(ex);
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
} finally {
response.close();
}
} else {
callback.onError(new ExchangeException(response.code(), response.message()));
final ExchangeException ex = new ExchangeException(response.code(), response.message());
response.close();
callback.onError(ex);
}
}
});

View File

@ -89,12 +89,12 @@ public class ExchangeApiImpl implements ExchangeApi {
final NetCipherHelper.Request httpRequest = new NetCipherHelper.Request(url);
httpRequest.enqueue(new okhttp3.Callback() {
@Override
public void onFailure(final Call call, final IOException ex) {
public void onFailure(@NonNull final Call call, @NonNull final IOException ex) {
callback.onError(ex);
}
@Override
public void onResponse(final Call call, final Response response) throws IOException {
public void onResponse(@NonNull final Call call, @NonNull final Response response) throws IOException {
if (response.isSuccessful()) {
try {
final JSONObject json = new JSONObject(response.body().string());
@ -108,9 +108,13 @@ public class ExchangeApiImpl implements ExchangeApi {
}
} catch (JSONException ex) {
callback.onError(new ExchangeException(ex.getLocalizedMessage()));
} finally {
response.close();
}
} else {
callback.onError(new ExchangeException(response.code(), response.message()));
final ExchangeException ex = new ExchangeException(response.code(), response.message());
response.close();
callback.onError(ex);
}
}
});

View File

@ -95,9 +95,13 @@ public class ExchangeApiImpl implements ExchangeApi {
callback.onSuccess(new ExchangeRateImpl(quoteCurrency, rate, timestamp));
} catch (JSONException ex) {
callback.onError(ex);
} finally {
response.close();
}
} else {
callback.onError(new ExchangeException(response.code(), response.message()));
final ExchangeException ex = new ExchangeException(response.code(), response.message());
response.close();
callback.onError(ex);
}
}
});

View File

@ -22,6 +22,5 @@ public interface NetworkCallback {
void onSuccess(JSONObject jsonObject);
void onError(Exception ex);
void onError(Exception ex, JSONObject json);
}

View File

@ -21,8 +21,7 @@ import androidx.annotation.NonNull;
import org.json.JSONObject;
public interface ShiftApiCall {
void get(@NonNull final String path, final String parameters, @NonNull final NetworkCallback callback);
void call(@NonNull final String path, @NonNull final NetworkCallback callback);
void call(@NonNull final String path, final JSONObject request, @NonNull final NetworkCallback callback);
void post(@NonNull final String path, final JSONObject data, @NonNull final NetworkCallback callback);
}

View File

@ -18,32 +18,23 @@ package com.m2049r.xmrwallet.service.shift;
import androidx.annotation.NonNull;
import org.json.JSONException;
import org.json.JSONObject;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class ShiftError {
@Getter
private final Error errorType;
private final Type type;
@Getter
private final String errorMsg;
public enum Error {
public enum Type {
SERVICE,
INFRASTRUCTURE
}
public boolean isRetryable() {
return errorType == Error.INFRASTRUCTURE;
}
public ShiftError(final JSONObject jsonObject) throws JSONException {
final JSONObject errorObject = jsonObject.getJSONObject("error");
errorType = Error.SERVICE;
errorMsg = errorObject.getString("message");
return type == Type.INFRASTRUCTURE;
}
@Override

View File

@ -16,6 +16,8 @@
package com.m2049r.xmrwallet.service.shift;
import androidx.annotation.Nullable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@ -24,10 +26,14 @@ public class ShiftException extends Exception {
@Getter
private final int code;
@Getter
@Nullable
private final ShiftError error;
public ShiftException(int code) {
this.code = code;
this.error = null;
this(code, null);
}
public String getErrorMessage() {
return (error != null) ? error.getErrorMsg() : ("HTTP:" + code);
}
}

View File

@ -0,0 +1,100 @@
package com.m2049r.xmrwallet.service.shift;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.R;
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.fragment.send.PreShifter;
import com.m2049r.xmrwallet.fragment.send.Shifter;
import com.m2049r.xmrwallet.service.shift.api.ShiftApi;
import com.m2049r.xmrwallet.service.shift.process.PreProcess;
import com.m2049r.xmrwallet.service.shift.process.PreShiftProcess;
import com.m2049r.xmrwallet.service.shift.process.Process;
import com.m2049r.xmrwallet.service.shift.process.ShiftProcess;
import com.m2049r.xmrwallet.service.shift.provider.exolix.ExolixApiImpl;
import java.util.HashSet;
import java.util.Set;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public enum ShiftService {
XMRTO(false, "xmr.to", "xmrto", null, null, 0, R.drawable.ic_xmrto_logo, ""),
SIDESHIFT(false, "SideShift.ai", "side", null, null, R.drawable.ic_sideshift_icon, R.drawable.ic_sideshift_wide, ""),
EXOLIX(true, "EXOLIX", "exolix", new ExolixApiImpl(), Type.ONESTEP, R.drawable.ic_exolix_icon, R.drawable.ic_exolix_wide, "XMR:BTC:LTC:ETH:USDT:SOL"),
UNKNOWN(false, "", null, null, null, 0, 0, "");
static final public ShiftService DEFAULT = EXOLIX;
final private boolean enabled;
final private String label;
final private String tag;
final private ShiftApi shiftApi;
final private Type type;
final private int iconId;
final private int logoId;
final private String assets;
@NonNull
static public ShiftService findWithTag(String tag) {
if (tag == null) return UNKNOWN;
for (ShiftService service : values()) {
if (tag.equals(service.tag)) return service;
}
return UNKNOWN;
}
@Getter
static private final Set<Crypto> possibleAssets = new HashSet<>();
static {
assert DEFAULT.enabled;
for (ShiftService service : values()) {
if (!service.enabled) continue;
final String[] assets = service.getAssets().split(":");
for (String anAsset : assets) {
possibleAssets.add(Crypto.withSymbol(anAsset));
}
}
}
public static boolean isAssetSupported(@NonNull Crypto crypto) {
return possibleAssets.contains(crypto);
}
public static boolean isAssetSupported(@NonNull String symbol) {
final Crypto crypto = Crypto.withSymbol(symbol);
if (crypto != null) {
return isAssetSupported(crypto);
}
return false;
}
public boolean supportsAsset(@NonNull Crypto crypto) {
return assets.contains(crypto.getSymbol());
}
public boolean supportsAsset(@NonNull String symbol) {
final Crypto crypto = Crypto.withSymbol(symbol);
if (crypto != null) {
return supportsAsset(crypto);
}
return false;
}
public ShiftProcess createProcess(Shifter shifter) {
return new Process(this, shifter);
}
public PreShiftProcess createPreProcess(PreShifter shifter) {
return new PreProcess(this, shifter);
}
public enum Type {
ONESTEP, TWOSTEP;
}
public static Crypto ASSET = null; // keep asset to exchange globally
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) 2024 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.
*/
package com.m2049r.xmrwallet.service.shift;
public enum ShiftType {
FIXED, FLOAT;
}

View File

@ -14,12 +14,14 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet.service.shift.sideshift.api;
package com.m2049r.xmrwallet.service.shift.api;
import com.m2049r.xmrwallet.service.shift.ShiftType;
import java.util.Date;
public interface CreateOrder {
String TAG = "side";
String getTag();
String getBtcCurrency();
@ -35,8 +37,11 @@ public interface CreateOrder {
String getXmrAddress();
Date getCreatedAt(); // createdAt
Date getCreatedAt();
Date getExpiresAt(); // expiresAt
Date getExpiresAt();
String getQueryOrderId();
ShiftType getType();
}

View File

@ -14,14 +14,14 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet.service.shift.sideshift.api;
package com.m2049r.xmrwallet.service.shift.api;
public interface QueryOrderParameters {
double getLowerLimit();
double getLowerLimit(); // XMR
double getPrice();
double getPrice(); // BTC/XMR
double getUpperLimit();
double getUpperLimit(); // XMR
}

View File

@ -0,0 +1,106 @@
/*
* Copyright (c) 2017-2021 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.shift.api;
import androidx.annotation.Nullable;
import java.util.Date;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class QueryOrderStatus {
final String orderId;
final Status status;
final String btcCurrency;
final double btcAmount;
final String btcAddress;
final double xmrAmount;
final String xmrAddress;
@Nullable
final Date createdAt;
@Nullable
final Date expiresAt;
public double getPrice() {
return btcAmount / xmrAmount;
}
public boolean isError() {
return status.isError();
}
public boolean isTerminal() {
return status.isTerminal();
}
public boolean isWaiting() {
return status.isWaiting();
}
public boolean isPending() {
return status.isPending();
}
public boolean isSent() {
return status.isSent();
}
public boolean isPaid() {
return status.isPaid();
}
public enum Status {
WAITING, // Waiting for mempool
PENDING, // Detected (waiting for confirmations)
SETTLING, // Settlement in progress
SETTLED, // Settlement completed
// no refunding in monerujo so these are ignored:
// REFUND, // Queued for refund
// REFUNDING, // Refund in progress
// REFUNDED // Refund completed
UNDEFINED,
EXPIRED,
ERROR; // Something went wrong and the user needs to interact with the provider
public boolean isError() {
return this == Status.UNDEFINED || this == Status.ERROR || this == Status.EXPIRED;
}
public boolean isTerminal() {
return (this == Status.SETTLED) || isError();
}
public boolean isWaiting() {
return this == Status.WAITING;
}
public boolean isPending() {
return this == Status.PENDING;
}
public boolean isSent() {
return this == Status.SETTLING;
}
public boolean isPaid() {
return this == Status.SETTLED;
}
}
}

View File

@ -14,13 +14,17 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet.service.shift.sideshift.api;
package com.m2049r.xmrwallet.service.shift.api;
import com.m2049r.xmrwallet.service.shift.ShiftType;
import java.util.Date;
public interface RequestQuote {
double getBtcAmount(); // settleAmount
double getBtcAmount(); // what we want to receive
double getXmrAmount(); // the XMR we need to send
String getId(); // id
@ -28,7 +32,7 @@ public interface RequestQuote {
Date getExpiresAt(); // expiresAt
double getXmrAmount(); // depositAmount
double getPrice(); // rate
ShiftType getType();
}

View File

@ -14,15 +14,17 @@
* limitations under the License.
*/
package com.m2049r.xmrwallet.service.shift.sideshift.api;
package com.m2049r.xmrwallet.service.shift.api;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.m2049r.xmrwallet.data.CryptoAmount;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
public interface SideShiftApi {
public interface ShiftApi {
int QUERY_INTERVAL = 5000; // ms
/**
@ -30,22 +32,23 @@ public interface SideShiftApi {
*
* @param callback the callback with the OrderParameter object
*/
void queryOrderParameters(@NonNull final ShiftCallback<QueryOrderParameters> callback);
void queryOrderParameters(@NonNull final CryptoAmount btcAmount, @NonNull final ShiftCallback<QueryOrderParameters> callback);
/**
* Creates an order
*
* @param xmrAmount the desired XMR amount
* @param btcAddress destination
* @param btcAmount the desired amount to send
*/
void requestQuote(final double xmrAmount, @NonNull final ShiftCallback<RequestQuote> callback);
void requestQuote(@Nullable final String btcAddress, @NonNull final CryptoAmount btcAmount, @NonNull final ShiftCallback<RequestQuote> callback);
/**
* Creates an order
*
* @param quoteId the desired XMR amount
* @param quote the quote from {@link #requestQuote(String, CryptoAmount, ShiftCallback)}
* @param btcAddress the target bitcoin address
*/
void createOrder(final String quoteId, @NonNull final String btcAddress, @NonNull final ShiftCallback<CreateOrder> callback);
void createOrder(final RequestQuote quote, @NonNull final String btcAddress, @NonNull final ShiftCallback<CreateOrder> callback);
/**
* Queries the order status for given current order

View File

@ -0,0 +1,53 @@
package com.m2049r.xmrwallet.service.shift.process;
import com.m2049r.xmrwallet.data.CryptoAmount;
import com.m2049r.xmrwallet.fragment.send.PreShifter;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import timber.log.Timber;
@RequiredArgsConstructor
public class PreProcess implements PreShiftProcess {
@Getter
final private ShiftService service;
final private PreShifter preshifter;
@Override
public void run() {
getOrderParameters();
}
@Override
public void restart() {
run();
}
private void getOrderParameters() {
Timber.d("getOrderParameters");
if (!preshifter.isActive()) return;
preshifter.showProgress();
final CryptoAmount btcAmount = preshifter.getAmount();
service.getShiftApi().queryOrderParameters(btcAmount, new ShiftCallback<QueryOrderParameters>() {
@Override
public void onSuccess(final QueryOrderParameters orderParameters) {
if (!preshifter.isActive()) return;
onOrderParametersReceived(orderParameters);
}
@Override
public void onError(final Exception ex) {
if (!preshifter.isActive()) return;
preshifter.onOrderParametersError(ex);
}
});
}
private void onOrderParametersReceived(final QueryOrderParameters orderParameters) {
Timber.d("onOrderParmsReceived %f", orderParameters.getPrice());
preshifter.onOrderParametersReceived(orderParameters);
}
}

View File

@ -0,0 +1,11 @@
package com.m2049r.xmrwallet.service.shift.process;
import com.m2049r.xmrwallet.service.shift.ShiftService;
public interface PreShiftProcess {
ShiftService getService();
void run();
void restart();
}

View File

@ -0,0 +1,178 @@
package com.m2049r.xmrwallet.service.shift.process;
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.fragment.send.Shifter;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.ShiftType;
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
import java.util.Date;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import timber.log.Timber;
@RequiredArgsConstructor
public class Process implements ShiftProcess {
@Getter
final private ShiftService service;
final private Shifter shifter;
private TxDataBtc txDataBtc;
private RequestQuote quote = null;
private CreateOrder order = null;
@Override
public void run(TxDataBtc txData) {
txDataBtc = txData;
switch (service.getType()) {
case TWOSTEP:
getQuote();
break;
case ONESTEP:
quote = new RequestQuote() {
@Override
public double getBtcAmount() {
return txDataBtc.getBtcAmount();
}
@Override
public double getXmrAmount() {
return txDataBtc.getXmrAmount();
}
@Override
public String getId() {
return null;
}
@Override
public Date getCreatedAt() {
return null;
}
@Override
public Date getExpiresAt() {
return null;
}
@Override
public double getPrice() {
return 0;
}
@Override
public ShiftType getType() {
return (txDataBtc.getShiftAmount().getCrypto() == Crypto.XMR) ? ShiftType.FLOAT : ShiftType.FIXED;
}
};
createOrder();
break;
default:
throw new IllegalStateException();
}
}
@Override
public void restart() {
run(txDataBtc);
}
private void getQuote() {
shifter.invalidateShift();
if (!shifter.isActive()) return;
Timber.d("Request Quote");
quote = null;
order = null;
shifter.showProgress(Shifter.Stage.A);
ShiftCallback<RequestQuote> callback = new ShiftCallback<RequestQuote>() {
@Override
public void onSuccess(RequestQuote requestQuote) {
if (!shifter.isActive()) return;
if (quote != null) {
Timber.w("another ongoing request quote request");
return;
}
onQuoteReceived(requestQuote);
}
@Override
public void onError(Exception ex) {
if (!shifter.isActive()) return;
if (quote != null) {
Timber.w("another ongoing request quote request");
return;
}
shifter.onQuoteError(ex);
}
};
service.getShiftApi().requestQuote(txDataBtc.getBtcAddress(), txDataBtc.getShiftAmount(), callback);
}
private void onQuoteReceived(final RequestQuote quote) {
Timber.d("onQuoteReceived %s", quote.getId());
// verify the shift is correct
if (!txDataBtc.validate(quote)) {
Timber.d("Failed to get quote");
shifter.showQuoteError();
return; // just stop for now
}
this.quote = quote;
txDataBtc.setAmount(this.quote.getXmrAmount());
shifter.onQuoteReceived(this.quote);
createOrder();
}
@Override
public void retryCreateOrder() {
createOrder();
}
private void createOrder() {
Timber.d("createOrder(%s)", quote.getId());
if (!shifter.isActive()) return;
final String btcAddress = txDataBtc.getBtcAddress();
order = null;
shifter.showProgress(Shifter.Stage.B);
service.getShiftApi().createOrder(quote, btcAddress, new ShiftCallback<CreateOrder>() {
@Override
public void onSuccess(final CreateOrder order) {
if (!shifter.isActive()) return;
if (quote == null) return;
if ((quote.getId() != null) && !order.getQuoteId().equals(quote.getId())) {
Timber.d("Quote ID does not match");
// ignore (we got a response to a stale request)
return;
}
if (Process.this.order != null)
throw new IllegalStateException("order must be null here!");
onOrderReceived(order);
}
@Override
public void onError(final Exception ex) {
if (!shifter.isActive()) return;
shifter.onOrderError(ex);
}
});
}
private void onOrderReceived(final CreateOrder order) {
Timber.d("onOrderReceived %s for %s", order.getOrderId(), order.getQuoteId());
// verify amount & destination
if (!order.getBtcCurrency().equalsIgnoreCase(txDataBtc.getBtcSymbol()))
throw new IllegalStateException("Destination Currency is wrong: " + order.getBtcCurrency()); // something is terribly wrong - die
if ((order.getType() == ShiftType.FIXED) && (order.getBtcAmount() != txDataBtc.getShiftAmount().getAmount()))
throw new IllegalStateException("Destination Amount is wrong: " + order.getBtcAmount()); // something is terribly wrong - die
if ((order.getType() == ShiftType.FLOAT) && (order.getXmrAmount() != txDataBtc.getShiftAmount().getAmount()))
throw new IllegalStateException("Source Amount is wrong: " + order.getXmrAmount()); // something is terribly wrong - die
if (!txDataBtc.validateAddress(order.getBtcAddress())) {
throw new IllegalStateException("Destination address is wrong: " + order.getBtcAddress()); // something is terribly wrong - die
}
this.order = order;
shifter.onOrderCreated(order);
}
}

View File

@ -0,0 +1,14 @@
package com.m2049r.xmrwallet.service.shift.process;
import com.m2049r.xmrwallet.data.TxDataBtc;
import com.m2049r.xmrwallet.service.shift.ShiftService;
public interface ShiftProcess {
ShiftService getService();
void run(TxDataBtc txData);
void restart();
void retryCreateOrder();
}

View File

@ -0,0 +1,165 @@
/*
* Copyright (c) 2017-2021 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.shift.provider;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.m2049r.xmrwallet.data.CryptoAmount;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftError;
import com.m2049r.xmrwallet.service.shift.ShiftException;
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderStatus;
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
import com.m2049r.xmrwallet.service.shift.api.ShiftApi;
import com.m2049r.xmrwallet.util.NetCipherHelper;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.Response;
import timber.log.Timber;
abstract public class ShiftApiImpl implements ShiftApi, ShiftApiCall {
protected abstract String getBaseUrl();
protected abstract String getApiUrl();
private final HttpUrl api;
public ShiftApiImpl() {
api = HttpUrl.parse(getApiUrl());
}
@Override
public void queryOrderParameters(CryptoAmount btcAmount, @NonNull final ShiftCallback<QueryOrderParameters> callback) {
throw new UnsupportedOperationException();
}
@Override
public void requestQuote(@Nullable final String btcAddress, @NonNull final CryptoAmount btcAmount, @NonNull final ShiftCallback<RequestQuote> callback) {
throw new UnsupportedOperationException();
}
@Override
public void createOrder(@NonNull final RequestQuote quote, @NonNull final String btcAddress,
@NonNull final ShiftCallback<CreateOrder> callback) {
throw new UnsupportedOperationException();
}
@Override
public void queryOrderStatus(@NonNull final String uuid,
@NonNull final ShiftCallback<QueryOrderStatus> callback) {
throw new UnsupportedOperationException();
}
@Override
public Uri getQueryOrderUri(String orderId) {
throw new UnsupportedOperationException();
}
// void post(@NonNull final String path, final JSONObject data, @NonNull final NetworkCallback callback);
@Override
public void get(@NonNull final String path, final String parameters, @NonNull final NetworkCallback callback) {
Timber.d("GET parameters=%s", parameters);
final HttpUrl.Builder builder = api.newBuilder().addPathSegments(path);
if (parameters != null)
for (String parm : parameters.split("&")) {
String[] p = parm.split("=");
builder.addQueryParameter(p[0], p[1]);
}
NetCipherHelper.Request request = new NetCipherHelper.Request(builder.build());
augment(request, null);
enqueue(request, callback);
}
@Override
public void post(@NonNull final String path, final JSONObject data, @NonNull final NetworkCallback callback) {
Timber.d("data=%s", data);
final HttpUrl url = api.newBuilder().addPathSegments(path).build();
final NetCipherHelper.Request request = new NetCipherHelper.Request(url, data);
augment(request, data);
enqueue(request, callback);
}
protected void augment(@NonNull final NetCipherHelper.Request request, @Nullable final JSONObject data) {
}
private void enqueue(NetCipherHelper.Request request, @NonNull final NetworkCallback callback) {
Timber.d("REQ: %s", request);
request.enqueue(new okhttp3.Callback() {
@Override
public void onFailure(@NonNull final Call call, @NonNull final IOException ex) {
callback.onError(ex, null);
}
@Override
public void onResponse(@NonNull final Call call, @NonNull final Response response) throws IOException {
Timber.d("onResponse code=%d", response.code());
try {
if (response.body() == null) {
callback.onError(new IllegalStateException("Empty response from service"), null);
return;
}
final String body = response.body().string();
if ((response.code() >= 200) && (response.code() <= 499)) {
try {
Timber.d(" SUCCESS %s", body);
final JSONObject json = new JSONObject(body);
final ShiftError error = createShiftError(ShiftError.Type.SERVICE, json);
if (error != null) {
callback.onError(new ShiftException(response.code(), error), json);
} else {
callback.onSuccess(json);
}
} catch (JSONException ex) {
callback.onError(ex, null);
}
} else {
try {
Timber.d("!SUCCESS %s", body);
final JSONObject json = new JSONObject(body);
Timber.d(json.toString(2));
final ShiftError error = createShiftError(ShiftError.Type.INFRASTRUCTURE, json);
Timber.d("%s says %d/%s", getBaseUrl(), response.code(), error);
callback.onError(new ShiftException(response.code(), error), json);
} catch (JSONException ex) {
callback.onError(new ShiftException(response.code()), null);
}
}
} finally {
response.close();
}
}
});
}
abstract protected ShiftError createShiftError(ShiftError.Type type, final JSONObject jsonObject);
}

View File

@ -0,0 +1,137 @@
/*
* Copyright (c) 2017-2021 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.shift.provider.exolix;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.ShiftType;
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
import com.m2049r.xmrwallet.util.DateHelper;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.ParseException;
import java.util.Date;
import lombok.Getter;
@Getter
class CreateOrderImpl implements CreateOrder {
private final static long EXPIRE = 10 * 60 * 1000; // 10 minutes
private final String btcCurrency;
private final double btcAmount;
private final String btcAddress;
private final String orderId;
private final double xmrAmount;
private final String xmrAddress;
private final Date createdAt;
private final Date expiresAt;
private final ShiftType type;
@Override
public String getQueryOrderId() {
return orderId;
}
@Override
public String getQuoteId() {
return null;
}
CreateOrderImpl(final JSONObject jsonObject) throws JSONException {
final JSONObject coinFrom = jsonObject.getJSONObject("coinFrom");
final JSONObject coinTo = jsonObject.getJSONObject("coinTo");
// sanity checks
final String depositMethod = coinFrom.getString("coinCode");
final String settleMethod = coinTo.getString("coinCode");
if (!"xmr".equalsIgnoreCase(depositMethod)
|| !ShiftService.ASSET.getSymbol().equalsIgnoreCase(settleMethod))
throw new IllegalStateException();
btcCurrency = settleMethod.toUpperCase();
btcAmount = jsonObject.getDouble("amountTo");
btcAddress = jsonObject.getString("withdrawalAddress");
xmrAmount = jsonObject.getDouble("amount");
xmrAddress = jsonObject.getString("depositAddress");
orderId = jsonObject.getString("id");
try {
final String created = jsonObject.getString("createdAt");
createdAt = DateHelper.parse(created);
expiresAt = new Date(createdAt.getTime() + EXPIRE);
} catch (ParseException ex) {
throw new JSONException(ex.getLocalizedMessage());
}
type = jsonObject.getString("rateType").equals("float") ? ShiftType.FLOAT : ShiftType.FIXED;
}
public static void call(@NonNull final ShiftApiCall api,
@NonNull final String btcAddress,
@NonNull final RequestQuote quote,
@NonNull final ShiftCallback<CreateOrder> callback) {
try {
final JSONObject request = createRequest(btcAddress, quote);
api.post("transactions", request, new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(new CreateOrderImpl(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex, JSONObject json) {
callback.onError(ex);
}
});
} catch (JSONException ex) {
callback.onError(ex);
}
}
static JSONObject createRequest(@NonNull final String btcAddress, @NonNull final RequestQuote quote) throws JSONException {
final JSONObject jsonObject = new JSONObject();
if (quote.getType() == ShiftType.FLOAT) {
jsonObject.put("rateType", "float");
jsonObject.put("amount", quote.getXmrAmount());
} else { // default is FIXED
jsonObject.put("rateType", "fixed");
jsonObject.put("withdrawalAmount", quote.getBtcAmount());
}
jsonObject.put("coinFrom", "XMR");
jsonObject.put("coinTo", ShiftService.ASSET.getSymbol());
jsonObject.put("networkTo", ShiftService.ASSET.getNetwork());
jsonObject.put("withdrawalAddress", btcAddress);
return jsonObject;
}
@Override
public String getTag() {
return ShiftService.EXOLIX.getTag();
}
}

View File

@ -0,0 +1,87 @@
/*
* Copyright (c) 2017-2021 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.shift.provider.exolix;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.m2049r.xmrwallet.BuildConfig;
import com.m2049r.xmrwallet.data.CryptoAmount;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftError;
import com.m2049r.xmrwallet.service.shift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderStatus;
import com.m2049r.xmrwallet.service.shift.api.RequestQuote;
import com.m2049r.xmrwallet.service.shift.provider.ShiftApiImpl;
import com.m2049r.xmrwallet.util.IdHelper;
import com.m2049r.xmrwallet.util.NetCipherHelper;
import org.json.JSONException;
import org.json.JSONObject;
import lombok.Getter;
public class ExolixApiImpl extends ShiftApiImpl {
@Getter
final private String baseUrl = "https://exolix.com";
@Getter
final private String apiUrl = baseUrl + "/api/v2";
@Override
public void queryOrderParameters(CryptoAmount btcAmount, @NonNull final ShiftCallback<QueryOrderParameters> callback) {
QueryOrderParametersImpl.call(this, btcAmount, callback);
}
@Override
public void createOrder(@NonNull final RequestQuote quote, @NonNull final String btcAddress,
@NonNull final ShiftCallback<CreateOrder> callback) {
CreateOrderImpl.call(this, btcAddress, quote, callback);
}
@Override
public void queryOrderStatus(@NonNull final String uuid,
@NonNull final ShiftCallback<QueryOrderStatus> callback) {
QueryOrderStatusImpl.call(this, uuid, callback);
}
@Override
public Uri getQueryOrderUri(String orderId) {
return Uri.parse(getBaseUrl() + "/transaction/" + orderId);
}
@Override
protected ShiftError createShiftError(ShiftError.Type type, final JSONObject jsonObject) {
try {
if (jsonObject.has("message")) {
final String message = jsonObject.getString("message");
if (!"null".equals(message))
return new ShiftError(type, message);
}
} catch (JSONException ex) {
return new ShiftError(ShiftError.Type.INFRASTRUCTURE, "unknown");
}
return null;
}
@Override
protected void augment(@NonNull final NetCipherHelper.Request request, @Nullable final JSONObject data) {
request.setAugmenter((b) -> IdHelper.addHeader(b, "Authorization", BuildConfig.ID_F));
}
}

View File

@ -0,0 +1,93 @@
/*
* Copyright (c) 2017-2021 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.shift.provider.exolix;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.data.CryptoAmount;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderParameters;
import com.m2049r.xmrwallet.util.AmountHelper;
import com.m2049r.xmrwallet.util.Helper;
import org.json.JSONException;
import org.json.JSONObject;
import lombok.Getter;
@Getter
class QueryOrderParametersImpl implements QueryOrderParameters {
private final double lowerLimit;
private final double price;
private final double upperLimit;
QueryOrderParametersImpl(final JSONObject jsonObject) throws JSONException {
price = jsonObject.getDouble("rate");
lowerLimit = jsonObject.getDouble("minAmount"); // XMR
upperLimit = jsonObject.getDouble("maxAmount"); // XMR
}
public static void call(@NonNull final ShiftApiCall api, CryptoAmount btcAmount,
@NonNull final ShiftCallback<QueryOrderParameters> callback) {
if (btcAmount.getAmount() == 0) { // just checking rate without real amount
btcAmount = new CryptoAmount(Crypto.withSymbol(Helper.BASE_CRYPTO), 1); // might as well check for 1 XMR
}
final CryptoAmount cryptoAmount = btcAmount;
final StringBuilder params = new StringBuilder();
if (btcAmount.getCrypto() == Crypto.XMR) { // we are sending XMR, so float
params.append("rateType=float");
params.append("&amount=").append(AmountHelper.format(cryptoAmount.getAmount()));
params.append("&coinFrom=XMR");
} else { // we are receiving non-XMR, i.e. paying something, so fixed
params.append("rateType=fixed");
params.append("&withdrawalAmount=").append(AmountHelper.format(cryptoAmount.getAmount()));
params.append("&coinFrom=XMR");
}
params.append("&coinTo=").append(ShiftService.ASSET.getSymbol());
params.append("&networkTo=").append(ShiftService.ASSET.getNetwork());
api.get("rate", params.toString(), new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(new QueryOrderParametersImpl(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex, JSONObject json) {
if ((json != null) && json.has("minAmount")) {
try {
final double lowerLimit = json.getDouble((cryptoAmount.getCrypto() == Crypto.XMR) ? "minAmount" : "withdrawMin");
call(api, cryptoAmount.newWithAmount(1.5 * lowerLimit), callback); // try again with 150% of minimum
} catch (JSONException jex) {
callback.onError(jex);
}
} else {
callback.onError(ex);
}
}
});
}
}

View File

@ -0,0 +1,121 @@
/*
* Copyright (c) 2017-2021 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.shift.provider.exolix;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftService;
import com.m2049r.xmrwallet.service.shift.api.QueryOrderStatus;
import com.m2049r.xmrwallet.util.DateHelper;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.ParseException;
import java.util.Date;
class QueryOrderStatusImpl extends QueryOrderStatus {
private QueryOrderStatusImpl(String orderId, Status status, String btcCurrency, double btcAmount, String btcAddress, double xmrAmount, String xmrAddress, Date createdAt, Date expiresAt) {
super(orderId, status, btcCurrency, btcAmount, btcAddress, xmrAmount, xmrAddress, createdAt, expiresAt);
}
static private Status getStatus(String status) {
switch (status) {
case "wait":
return Status.WAITING;
case "confirmation":
case "confirmed":
return Status.PENDING;
case "exchanging":
case "sending":
return Status.SETTLING;
case "success":
return Status.SETTLED;
case "overdue":
return Status.EXPIRED;
case "refunded":
default:
return Status.UNDEFINED;
}
}
static private QueryOrderStatusImpl of(final JSONObject jsonObject) throws JSONException {
final JSONObject coinFrom = jsonObject.getJSONObject("coinFrom");
final JSONObject coinTo = jsonObject.getJSONObject("coinTo");
// sanity checks
final String depositMethod = coinFrom.getString("coinCode");
final String settleMethod = coinTo.getString("coinCode");
if (!"xmr".equalsIgnoreCase(depositMethod)
|| !ShiftService.ASSET.getSymbol().equalsIgnoreCase(settleMethod))
throw new IllegalStateException();
final double btcAmount = jsonObject.getDouble("amountTo");
final String btcAddress = jsonObject.getString("withdrawalAddress");
final double xmrAmount = jsonObject.getDouble("amount");
final String xmrAddress = jsonObject.getString("depositAddress");
final String orderId = jsonObject.getString("id");
Date createdAt;
Date expiresAt;
try {
final String created = jsonObject.getString("createdAt");
createdAt = DateHelper.parse(created);
expiresAt = new Date(createdAt.getTime() + 300000);
} catch (ParseException ex) {
throw new JSONException(ex.getLocalizedMessage());
}
final String status = jsonObject.getString("status");
return new QueryOrderStatusImpl(
orderId,
getStatus(status),
settleMethod,
btcAmount,
btcAddress,
xmrAmount,
xmrAddress,
createdAt,
expiresAt
);
}
public static void call(@NonNull final ShiftApiCall api, @NonNull final String orderId,
@NonNull final ShiftCallback<QueryOrderStatus> callback) {
api.get("transactions/" + orderId, null, new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(QueryOrderStatusImpl.of(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex, JSONObject json) {
callback.onError(ex);
}
});
}
}

View File

@ -1,65 +0,0 @@
/*
* Copyright (c) 2017-2021 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.shift.sideshift.api;
import java.util.Date;
public interface QueryOrderStatus {
enum State {
WAITING, // Waiting for mempool
PENDING, // Detected (waiting for confirmations)
SETTLING, // Settlement in progress
SETTLED, // Settlement completed
// no refunding in monerujo so theese are ignored:
// REFUND, // Queued for refund
// REFUNDING, // Refund in progress
// REFUNDED // Refund completed
UNDEFINED
}
boolean isCreated();
boolean isTerminal();
boolean isWaiting();
boolean isPending();
boolean isSent();
boolean isPaid();
boolean isError();
QueryOrderStatus.State getState();
String getOrderId();
Date getCreatedAt();
Date getExpiresAt();
double getBtcAmount();
String getBtcAddress();
double getXmrAmount();
String getXmrAddress();
double getPrice();
}

View File

@ -1,121 +0,0 @@
/*
* Copyright (c) 2017-2021 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.shift.sideshift.network;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.BuildConfig;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
import com.m2049r.xmrwallet.util.DateHelper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.ParseException;
import java.util.Date;
import lombok.Getter;
class CreateOrderImpl implements CreateOrder {
@Getter
private final String btcCurrency;
@Getter
private final double btcAmount;
@Getter
private final String btcAddress;
@Getter
private final String quoteId;
@Getter
private final String orderId;
@Getter
private final double xmrAmount;
@Getter
private final String xmrAddress;
@Getter
private final Date createdAt;
@Getter
private final Date expiresAt;
CreateOrderImpl(final JSONObject jsonObject) throws JSONException {
// sanity checks
final String depositMethod = jsonObject.getString("depositMethodId");
final String settleMethod = jsonObject.getString("settleMethodId");
if (!"xmr".equals(depositMethod) || !ServiceHelper.ASSET.equals(settleMethod))
throw new IllegalStateException();
btcCurrency = settleMethod.toUpperCase();
btcAmount = jsonObject.getDouble("settleAmount");
JSONObject settleAddress = jsonObject.getJSONObject("settleAddress");
btcAddress = settleAddress.getString("address");
xmrAmount = jsonObject.getDouble("depositAmount");
JSONObject depositAddress = jsonObject.getJSONObject("depositAddress");
xmrAddress = depositAddress.getString("address");
quoteId = jsonObject.getString("quoteId");
orderId = jsonObject.getString("orderId");
try {
final String created = jsonObject.getString("createdAtISO");
createdAt = DateHelper.parse(created);
final String expires = jsonObject.getString("expiresAtISO");
expiresAt = DateHelper.parse(expires);
} catch (ParseException ex) {
throw new JSONException(ex.getLocalizedMessage());
}
}
public static void call(@NonNull final ShiftApiCall api, final String quoteId, @NonNull final String btcAddress,
@NonNull final ShiftCallback<CreateOrder> callback) {
try {
final JSONObject request = createRequest(quoteId, btcAddress);
api.call("orders", request, new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(new CreateOrderImpl(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex) {
callback.onError(ex);
}
});
} catch (JSONException ex) {
callback.onError(ex);
}
}
static JSONObject createRequest(final String quoteId, final String address) throws JSONException {
final JSONObject jsonObject = new JSONObject();
jsonObject.put("type", "fixed");
jsonObject.put("quoteId", quoteId);
jsonObject.put("settleAddress", address);
if (!BuildConfig.ID_A.isEmpty() && !"null".equals(BuildConfig.ID_A)) {
jsonObject.put("affiliateId", BuildConfig.ID_A);
}
return jsonObject;
}
}

View File

@ -1,72 +0,0 @@
/*
* Copyright (c) 2017-2021 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.shift.sideshift.network;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderParameters;
import com.m2049r.xmrwallet.util.ServiceHelper;
import org.json.JSONException;
import org.json.JSONObject;
class QueryOrderParametersImpl implements QueryOrderParameters {
private double lowerLimit;
private double price;
private double upperLimit;
public double getLowerLimit() {
return lowerLimit;
}
public double getPrice() {
return price;
}
public double getUpperLimit() {
return upperLimit;
}
QueryOrderParametersImpl(final JSONObject jsonObject) throws JSONException {
lowerLimit = jsonObject.getDouble("min");
price = jsonObject.getDouble("rate");
upperLimit = jsonObject.getDouble("max");
}
public static void call(@NonNull final ShiftApiCall api,
@NonNull final ShiftCallback<QueryOrderParameters> callback) {
api.call("pairs/xmr/" + ServiceHelper.ASSET, new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(new QueryOrderParametersImpl(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex) {
callback.onError(ex);
}
});
}
}

View File

@ -1,145 +0,0 @@
/*
* Copyright (c) 2017-2021 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.shift.sideshift.network;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus;
import com.m2049r.xmrwallet.util.DateHelper;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.ParseException;
import java.util.Date;
import lombok.Getter;
class QueryOrderStatusImpl implements QueryOrderStatus {
@Getter
private QueryOrderStatus.State state;
@Getter
private final String orderId;
@Getter
private final Date createdAt;
@Getter
private final Date expiresAt;
@Getter
private final double btcAmount;
@Getter
private final String btcAddress;
@Getter
private final double xmrAmount;
@Getter
private final String xmrAddress;
public boolean isCreated() {
return true;
}
public boolean isTerminal() {
return (state.equals(State.SETTLED) || isError());
}
public boolean isError() {
return state.equals(State.UNDEFINED);
}
public boolean isWaiting() {
return state.equals(State.WAITING);
}
public boolean isPending() {
return state.equals(State.PENDING);
}
public boolean isSent() {
return state.equals(State.SETTLING);
}
public boolean isPaid() {
return state.equals(State.SETTLED);
}
public double getPrice() {
return btcAmount / xmrAmount;
}
QueryOrderStatusImpl(final JSONObject jsonObject) throws JSONException {
try {
String created = jsonObject.getString("createdAtISO");
createdAt = DateHelper.parse(created);
String expires = jsonObject.getString("expiresAtISO");
expiresAt = DateHelper.parse(expires);
} catch (ParseException ex) {
throw new JSONException(ex.getLocalizedMessage());
}
orderId = jsonObject.getString("orderId");
btcAmount = jsonObject.getDouble("settleAmount");
JSONObject settleAddress = jsonObject.getJSONObject("settleAddress");
btcAddress = settleAddress.getString("address");
xmrAmount = jsonObject.getDouble("depositAmount");
JSONObject depositAddress = jsonObject.getJSONObject("depositAddress");
xmrAddress = settleAddress.getString("address");
JSONArray deposits = jsonObject.getJSONArray("deposits");
// we only create one deposit, so die if there are more than one:
if (deposits.length() > 1)
throw new IllegalStateException("more than one deposits");
state = State.UNDEFINED;
if (deposits.length() == 0) {
state = State.WAITING;
} else if (deposits.length() == 1) {
// sanity check
if (!orderId.equals(deposits.getJSONObject(0).getString("orderId")))
throw new IllegalStateException("deposit has different order id!");
String stateName = deposits.getJSONObject(0).getString("status");
try {
state = State.valueOf(stateName.toUpperCase());
} catch (IllegalArgumentException ex) {
state = State.UNDEFINED;
}
}
}
public static void call(@NonNull final ShiftApiCall api, @NonNull final String orderId,
@NonNull final ShiftCallback<QueryOrderStatus> callback) {
api.call("orders/" + orderId, new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(new QueryOrderStatusImpl(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex) {
callback.onError(ex);
}
});
}
}

View File

@ -1,126 +0,0 @@
/*
* Copyright (c) 2017-2021 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.shift.sideshift.network;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.sideshift.api.RequestQuote;
import com.m2049r.xmrwallet.util.DateHelper;
import com.m2049r.xmrwallet.util.ServiceHelper;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.Date;
import java.util.Locale;
import lombok.Getter;
class RequestQuoteImpl implements RequestQuote {
@Getter
private final double btcAmount;
@Getter
private final String id;
@Getter
private final Date createdAt;
@Getter
private final Date expiresAt;
@Getter
private final double xmrAmount;
@Getter
private final double price;
// TODO do something with errors - they always seem to send us 500
RequestQuoteImpl(final JSONObject jsonObject) throws JSONException {
// sanity checks
final String depositMethod = jsonObject.getString("depositMethod");
final String settleMethod = jsonObject.getString("settleMethod");
if (!"xmr".equals(depositMethod) || !ServiceHelper.ASSET.equals(settleMethod))
throw new IllegalStateException();
btcAmount = jsonObject.getDouble("settleAmount");
id = jsonObject.getString("id");
try {
final String created = jsonObject.getString("createdAt");
createdAt = DateHelper.parse(created);
final String expires = jsonObject.getString("expiresAt");
expiresAt = DateHelper.parse(expires);
} catch (ParseException ex) {
throw new JSONException(ex.getLocalizedMessage());
}
xmrAmount = jsonObject.getDouble("depositAmount");
price = jsonObject.getDouble("rate");
}
public static void call(@NonNull final ShiftApiCall api, final double btcAmount,
@NonNull final ShiftCallback<RequestQuote> callback) {
try {
final JSONObject request = createRequest(btcAmount);
api.call("quotes", request, new NetworkCallback() {
@Override
public void onSuccess(JSONObject jsonObject) {
try {
callback.onSuccess(new RequestQuoteImpl(jsonObject));
} catch (JSONException ex) {
callback.onError(ex);
}
}
@Override
public void onError(Exception ex) {
callback.onError(ex);
}
});
} catch (JSONException ex) {
callback.onError(ex);
}
}
/**
* Create JSON request object
*
* @param btcAmount how much XMR to shift to BTC
*/
static JSONObject createRequest(final double btcAmount) throws JSONException {
final JSONObject jsonObject = new JSONObject();
jsonObject.put("depositMethod", "xmr");
jsonObject.put("settleMethod", ServiceHelper.ASSET);
// #sideshift is silly and likes numbers as strings
String amount = AmountFormatter.format(btcAmount);
jsonObject.put("settleAmount", amount);
return jsonObject;
}
static final DecimalFormat AmountFormatter;
static {
AmountFormatter = new DecimalFormat();
AmountFormatter.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.US));
AmountFormatter.setMinimumIntegerDigits(1);
AmountFormatter.setMaximumFractionDigits(12);
AmountFormatter.setGroupingUsed(false);
}
}

View File

@ -1,122 +0,0 @@
/*
* Copyright (c) 2017-2021 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.shift.sideshift.network;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.m2049r.xmrwallet.service.shift.NetworkCallback;
import com.m2049r.xmrwallet.service.shift.ShiftApiCall;
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
import com.m2049r.xmrwallet.service.shift.ShiftError;
import com.m2049r.xmrwallet.service.shift.ShiftException;
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderParameters;
import com.m2049r.xmrwallet.service.shift.sideshift.api.QueryOrderStatus;
import com.m2049r.xmrwallet.service.shift.sideshift.api.RequestQuote;
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
import com.m2049r.xmrwallet.util.NetCipherHelper;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.Response;
import timber.log.Timber;
public class SideShiftApiImpl implements SideShiftApi, ShiftApiCall {
private final HttpUrl baseUrl;
public SideShiftApiImpl(final HttpUrl baseUrl) {
this.baseUrl = baseUrl;
}
@Override
public void queryOrderParameters(@NonNull final ShiftCallback<QueryOrderParameters> callback) {
QueryOrderParametersImpl.call(this, callback);
}
@Override
public void requestQuote(final double btcAmount, @NonNull final ShiftCallback<RequestQuote> callback) {
RequestQuoteImpl.call(this, btcAmount, callback);
}
@Override
public void createOrder(final String quoteId, @NonNull final String btcAddress,
@NonNull final ShiftCallback<CreateOrder> callback) {
CreateOrderImpl.call(this, quoteId, btcAddress, callback);
}
@Override
public void queryOrderStatus(@NonNull final String uuid,
@NonNull final ShiftCallback<QueryOrderStatus> callback) {
QueryOrderStatusImpl.call(this, uuid, callback);
}
@Override
public Uri getQueryOrderUri(String orderId) {
return Uri.parse("https://sideshift.ai/orders/" + orderId);
}
@Override
public void call(@NonNull final String path, @NonNull final NetworkCallback callback) {
call(path, null, callback);
}
@Override
public void call(@NonNull final String path, final JSONObject request, @NonNull final NetworkCallback callback) {
final HttpUrl url = baseUrl.newBuilder()
.addPathSegments(path)
.build();
NetCipherHelper.Request httpRequest = new NetCipherHelper.Request(url, request);
httpRequest.enqueue(new okhttp3.Callback() {
@Override
public void onFailure(final Call call, final IOException ex) {
callback.onError(ex);
}
@Override
public void onResponse(@NonNull final Call call, @NonNull final Response response) throws IOException {
Timber.d("onResponse code=%d", response.code());
if (response.isSuccessful()) {
try {
final JSONObject json = new JSONObject(response.body().string());
callback.onSuccess(json);
} catch (JSONException ex) {
callback.onError(ex);
}
} else {
try {
final JSONObject json = new JSONObject(response.body().string());
Timber.d(json.toString(2));
final ShiftError error = new ShiftError(json);
Timber.w("%s says %d/%s", CreateOrder.TAG, response.code(), error.toString());
callback.onError(new ShiftException(response.code(), error));
} catch (JSONException ex) {
callback.onError(new ShiftException(response.code()));
}
}
}
});
}
}

View File

@ -0,0 +1,28 @@
package com.m2049r.xmrwallet.util;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;
public class AmountHelper {
static final DecimalFormat AmountFormatter_12;
static {
AmountFormatter_12 = new DecimalFormat();
AmountFormatter_12.setDecimalFormatSymbols(new DecimalFormatSymbols(Locale.US));
AmountFormatter_12.setMinimumIntegerDigits(1);
AmountFormatter_12.setMaximumFractionDigits(12);
AmountFormatter_12.setGroupingUsed(false);
}
public static String format(double amount) {
return AmountFormatter_12.format(amount);
}
public static String format_6(double amount) {
int n = (int) Math.ceil(Math.log10(amount));
int d = Math.max(2, 6 - n);
final String fmt = "%,." + d + "f";
return String.format(Locale.getDefault(), fmt, amount);
}
}

View File

@ -0,0 +1,65 @@
package com.m2049r.xmrwallet.util;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.view.View;
import androidx.core.content.res.ResourcesCompat;
import lombok.Getter;
public class Flasher {
public interface Light {
int getDrawableId();
}
final private static int ON_TIME = 80; //ms
final private static int DURATION = 100 + ON_TIME; //ms
final private static int OFF_TIME = 600; //ms
@Getter
final private Drawable drawable;
private long t;
private ValueAnimator animator;
private final int colorOff, colorOn;
private int colorCurrent;
public Flasher(Context ctx, Light light) {
colorOff = ThemeHelper.getThemedColor(ctx, android.R.attr.colorControlNormal);
colorOn = ThemeHelper.getThemedColor(ctx, android.R.attr.colorControlActivated);
drawable = getDrawable(ctx, light.getDrawableId());
drawable.setTint(colorOff);
}
public void flash(View view) {
if (view == null) return;
if (animator != null) animator.cancel();
final long now = System.currentTimeMillis();
t = now;
animator = ValueAnimator.ofArgb(colorOff, colorOn); // always blink nomatter what
animator.addUpdateListener(valueAnimator -> {
colorCurrent = (Integer) valueAnimator.getAnimatedValue();
drawable.setTint(colorCurrent);
});
animator.setDuration(ON_TIME);
animator.start();
view.postDelayed(() -> {
if (t == now) { // only turn it off if we turned it on last
animator = ValueAnimator.ofArgb(colorCurrent, colorOff);
animator.addUpdateListener(valueAnimator -> {
colorCurrent = (Integer) valueAnimator.getAnimatedValue();
drawable.setTint(colorCurrent);
});
animator.setDuration(Math.abs((long) (1. * OFF_TIME * ((colorCurrent - colorOff) / (colorOn - colorOff)))));
animator.start();
}
}, DURATION);
}
private Drawable getDrawable(Context ctx, int drawableId) {
return ResourcesCompat.getDrawable(ctx.getResources(), drawableId, null);
}
}

View File

@ -78,12 +78,13 @@ import timber.log.Timber;
public class Helper {
static public final String NOCRAZYPASS_FLAGFILE = ".nocrazypass";
static public final String BASE_CRYPTO = Crypto.XMR.getSymbol();
static public final Crypto BASE_CRYPTO_CRYPTO = Crypto.XMR;
static public final String BASE_CRYPTO = BASE_CRYPTO_CRYPTO.getSymbol();
static public final int XMR_DECIMALS = 12;
static public final long ONE_XMR = Math.round(Math.pow(10, Helper.XMR_DECIMALS));
static public final boolean SHOW_EXCHANGERATES = true;
static public boolean ALLOW_SHIFT = false;
static public boolean ALLOW_SHIFT = true;
static private final String WALLET_DIR = "wallets";
static private final String MONERO_DIR = "monero";

View File

@ -0,0 +1,34 @@
package com.m2049r.xmrwallet.util;
import com.m2049r.xmrwallet.BuildConfig;
import org.json.JSONException;
import org.json.JSONObject;
import okhttp3.Request;
public class IdHelper {
static public String idOrNot(String id) {
return isId(id) ? id : "";
}
static public boolean isId(String id) {
return (id != null) && !id.isEmpty() && !"null".equals(id);
}
static public String asParameter(String name, String id) {
return isId(id) ? (name + "=" + id) : "";
}
static public void jsonPut(JSONObject jsonObject, String name, String id) throws JSONException {
if (isId(id)) {
jsonObject.put(name, id);
}
}
static public void addHeader(Request.Builder builder, String name, String id) {
if (isId(id)) {
builder.addHeader(name, id);
}
}
}

View File

@ -23,6 +23,8 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.core.content.ContextCompat;
import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
import com.burgstaller.okhttp.digest.CachingAuthenticator;
@ -39,10 +41,11 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import info.guardianproject.netcipher.client.StrongOkHttpClientBuilder;
import info.guardianproject.netcipher.proxy.OrbotHelper;
import info.guardianproject.netcipher.proxy.MyOrbotHelper;
import info.guardianproject.netcipher.proxy.SignatureUtils;
import info.guardianproject.netcipher.proxy.StatusCallback;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import okhttp3.Call;
import okhttp3.Callback;
@ -56,11 +59,11 @@ import timber.log.Timber;
@RequiredArgsConstructor
public class NetCipherHelper implements StatusCallback {
public static final String USER_AGENT = "Monerujo/1.0";
public static final int HTTP_TIMEOUT_CONNECT = 1000; //ms
public static final int HTTP_TIMEOUT_READ = 2000; //ms
public static final int HTTP_TIMEOUT_WRITE = 1000; //ms
public static final int TOR_TIMEOUT_CONNECT = 5000; //ms
public static final int TOR_TIMEOUT = 2000; //ms
public static final int HTTP_TIMEOUT_CONNECT = 2500; //ms
public static final int HTTP_TIMEOUT_READ = 5000; //ms
public static final int HTTP_TIMEOUT_WRITE = 2500; //ms
public static final int TOR_TIMEOUT_CONNECT = 10000; //ms
public static final int TOR_TIMEOUT = 5000; //ms
public interface OnStatusChangedListener {
void connected();
@ -73,7 +76,7 @@ public class NetCipherHelper implements StatusCallback {
}
final private Context context;
final private OrbotHelper orbot;
final private MyOrbotHelper orbot;
@SuppressLint("StaticFieldLeak")
private static NetCipherHelper Instance;
@ -83,7 +86,7 @@ public class NetCipherHelper implements StatusCallback {
synchronized (NetCipherHelper.class) {
if (Instance == null) {
final Context applicationContext = context.getApplicationContext();
Instance = new NetCipherHelper(applicationContext, OrbotHelper.get(context).statusTimeout(5000));
Instance = new NetCipherHelper(applicationContext, MyOrbotHelper.get(context).statusTimeout(5000));
}
}
}
@ -97,9 +100,9 @@ public class NetCipherHelper implements StatusCallback {
private OkHttpClient client;
private void createTorClient(Intent statusIntent) {
String orbotStatus = statusIntent.getStringExtra(OrbotHelper.EXTRA_STATUS);
String orbotStatus = statusIntent.getStringExtra(MyOrbotHelper.EXTRA_STATUS);
if (orbotStatus == null) throw new IllegalStateException("status is null");
if (!orbotStatus.equals(OrbotHelper.STATUS_ON))
if (!orbotStatus.equals(MyOrbotHelper.STATUS_ON))
throw new IllegalStateException("Orbot is not ON");
try {
final OkHttpClient.Builder okBuilder = new OkHttpClient.Builder()
@ -110,7 +113,6 @@ public class NetCipherHelper implements StatusCallback {
.withSocksProxy()
.applyTo(okBuilder, statusIntent)
.build();
Helper.ALLOW_SHIFT = false; // no shifting with Tor
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
@ -123,7 +125,6 @@ public class NetCipherHelper implements StatusCallback {
.writeTimeout(HTTP_TIMEOUT_WRITE, TimeUnit.MILLISECONDS)
.readTimeout(HTTP_TIMEOUT_READ, TimeUnit.MILLISECONDS)
.build();
Helper.ALLOW_SHIFT = true;
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
@ -144,7 +145,7 @@ public class NetCipherHelper implements StatusCallback {
.addStatusCallback(me);
// deal with org.torproject.android.intent.action.STATUS = STARTS_DISABLED
me.context.registerReceiver(orbotStatusReceiver, new IntentFilter(OrbotHelper.ACTION_STATUS));
ContextCompat.registerReceiver(me.context, orbotStatusReceiver, new IntentFilter(MyOrbotHelper.ACTION_STATUS), ContextCompat.RECEIVER_EXPORTED);
me.startTor();
}
@ -270,7 +271,7 @@ public class NetCipherHelper implements StatusCallback {
hashes.add("A7:02:07:92:4F:61:FF:09:37:1D:54:84:14:5C:4B:EE:77:2C:55:C1:9E:EE:23:2F:57:70:E1:82:71:F7:CB:AE");
return null != SignatureUtils.validateBroadcastIntent(context,
OrbotHelper.getOrbotStartIntent(context),
MyOrbotHelper.getOrbotStartIntent(context),
hashes, false);
}
@ -293,19 +294,36 @@ public class NetCipherHelper implements StatusCallback {
@ToString
static public class Request {
final HttpUrl url;
final String json;
final JSONObject data;
final String username;
final String password;
@Setter
RequestAugmenter augmenter;
public Request(final HttpUrl url, final String json, final String username, final String password) {
this.url = url;
this.json = json;
this.username = username;
this.password = password;
final Method method;
public enum Method {
GET, POST;
}
public Request(final HttpUrl url, final JSONObject json) {
this(url, json == null ? null : json.toString(), null, null);
public interface RequestAugmenter {
void augment(okhttp3.Request.Builder builder);
}
public Request(final HttpUrl url, final JSONObject data, final String username, final String password) {
this.url = url;
this.data = data;
this.username = username;
this.password = password;
if (data == null) {
method = Method.GET;
} else {
method = Method.POST;
}
}
public Request(final HttpUrl url, final JSONObject data) {
this(url, data, null, null);
}
public Request(final HttpUrl url) {
@ -344,11 +362,16 @@ public class NetCipherHelper implements StatusCallback {
final okhttp3.Request.Builder builder =
new okhttp3.Request.Builder()
.url(url)
.header("User-Agent", USER_AGENT);
if (json != null) {
builder.post(RequestBody.create(json, MediaType.parse("application/json")));
} else {
builder.get();
.header("User-Agent", USER_AGENT)
.header("Accept", "application/json");
if (augmenter != null) augmenter.augment(builder);
switch (method) {
case GET:
builder.get();
break;
case POST:
builder.post(RequestBody.create(data.toString(), MediaType.parse("application/json")));
break;
}
return builder.build();
}
@ -380,9 +403,9 @@ public class NetCipherHelper implements StatusCallback {
private static final BroadcastReceiver orbotStatusReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Timber.d("%s/%s", intent.getAction(), intent.getStringExtra(OrbotHelper.EXTRA_STATUS));
if (OrbotHelper.ACTION_STATUS.equals(intent.getAction())) {
if (OrbotHelper.STATUS_STARTS_DISABLED.equals(intent.getStringExtra(OrbotHelper.EXTRA_STATUS))) {
Timber.d("%s/%s", intent.getAction(), intent.getStringExtra(MyOrbotHelper.EXTRA_STATUS));
if (MyOrbotHelper.ACTION_STATUS.equals(intent.getAction())) {
if (MyOrbotHelper.STATUS_STARTS_DISABLED.equals(intent.getStringExtra(MyOrbotHelper.EXTRA_STATUS))) {
getInstance().onNotEnabled();
}
}
@ -390,6 +413,6 @@ public class NetCipherHelper implements StatusCallback {
};
public void installOrbot(Activity host) {
host.startActivity(OrbotHelper.getOrbotInstallIntent(context));
host.startActivity(MyOrbotHelper.getOrbotInstallIntent(context));
}
}

View File

@ -143,8 +143,8 @@ public class OpenAliasHelper {
for (String txt : txts) {
BarcodeData bc = BarcodeData.parseOpenAlias(txt, dnssec);
if (bc != null) {
if (!dataMap.containsKey(bc.asset)) {
dataMap.put(bc.asset, bc);
if (!dataMap.containsKey(bc.getAsset())) {
dataMap.put(bc.getAsset(), bc);
}
}
}

View File

@ -7,16 +7,6 @@ import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
import okhttp3.HttpUrl;
public class ServiceHelper {
public static String ASSET = null;
static public HttpUrl getXmrToBaseUrl() {
if ((WalletManager.getInstance() == null)
|| (WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet)) {
throw new IllegalStateException("Only mainnet not supported");
} else {
return HttpUrl.parse("https://sideshift.ai/api/v1/");
}
}
static public ExchangeApi getExchangeApi() {
return new com.m2049r.xmrwallet.service.exchange.krakenFiat.ExchangeApiImpl();

View File

@ -1,51 +0,0 @@
package com.m2049r.xmrwallet.util.validator;
import lombok.Getter;
public enum BitcoinAddressType {
BTC(Type.BTC, Type.BTC_BECH32_PREFIX),
LTC(Type.LTC, Type.LTC_BECH32_PREFIX),
DASH(Type.DASH, null),
DOGE(Type.DOGE, null);
@Getter
private final byte[] production;
@Getter
private final byte[] testnet;
@Getter
private final String productionBech32Prefix;
@Getter
private final String testnetBech32Prefix;
public boolean hasBech32() {
return productionBech32Prefix != null;
}
public String getBech32Prefix(boolean testnet) {
return testnet ? testnetBech32Prefix : productionBech32Prefix;
}
BitcoinAddressType(byte[][] types, String[] bech32Prefix) {
production = types[0];
testnet = types[1];
if (bech32Prefix != null) {
productionBech32Prefix = bech32Prefix[0];
testnetBech32Prefix = bech32Prefix[1];
} else {
productionBech32Prefix = null;
testnetBech32Prefix = null;
}
}
// Java is silly and doesn't allow array initializers in the construction
private static class Type {
private static final byte[][] BTC = {{0x00, 0x05}, {0x6f, (byte) 0xc4}};
private static final String[] BTC_BECH32_PREFIX = {"bc", "tb"};
private static final byte[][] LTC = {{0x30, 0x05, 0x32}, {0x6f, (byte) 0xc4, 0x3a}};
private static final String[] LTC_BECH32_PREFIX = {"ltc", "tltc"};
private static final byte[][] DASH = {{0x4c, 0x10}, {(byte) 0x8c, 0x13}};
private static final byte[][] DOGE = {{0x1e, 0x16}, {0x71, (byte) 0xc4}};
}
}

View File

@ -1,220 +0,0 @@
/*
* Copyright (c) 2017 m2049r er 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.util.validator;
// mostly based on https://rosettacode.org/wiki/Bitcoin/address_validation#Java
import com.m2049r.xmrwallet.data.Crypto;
import com.m2049r.xmrwallet.model.NetworkType;
import com.m2049r.xmrwallet.model.WalletManager;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class BitcoinAddressValidator {
private static final String ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
public static Crypto validate(String address) {
for (BitcoinAddressType type : BitcoinAddressType.values()) {
if (validate(address, type))
return Crypto.valueOf(type.name());
}
return null;
}
// just for tests
public static boolean validateBTC(String addrress, boolean testnet) {
return validate(addrress, BitcoinAddressType.BTC, testnet);
}
public static boolean validate(String addrress, BitcoinAddressType type, boolean testnet) {
if (validate(addrress, testnet ? type.getTestnet() : type.getProduction()))
return true;
if (type.hasBech32())
return validateBech32Segwit(addrress, type, testnet);
else
return false;
}
public static boolean validate(String addrress, BitcoinAddressType type) {
final boolean testnet = WalletManager.getInstance().getNetworkType() != NetworkType.NetworkType_Mainnet;
return validate(addrress, type, testnet);
}
public static boolean validate(String addrress, byte[] addressTypes) {
if (addrress.length() < 26 || addrress.length() > 35)
return false;
byte[] decoded = decodeBase58To25Bytes(addrress);
if (decoded == null)
return false;
int v = decoded[0] & 0xFF;
boolean nok = true;
for (byte b : addressTypes) {
nok = nok && (v != (b & 0xFF));
}
if (nok) return false;
byte[] hash1 = sha256(Arrays.copyOfRange(decoded, 0, 21));
byte[] hash2 = sha256(hash1);
return Arrays.equals(Arrays.copyOfRange(hash2, 0, 4), Arrays.copyOfRange(decoded, 21, 25));
}
private static byte[] decodeBase58To25Bytes(String input) {
BigInteger num = BigInteger.ZERO;
for (char t : input.toCharArray()) {
int p = ALPHABET.indexOf(t);
if (p == -1)
return null;
num = num.multiply(BigInteger.valueOf(58)).add(BigInteger.valueOf(p));
}
byte[] result = new byte[25];
byte[] numBytes = num.toByteArray();
if (num.bitLength() > 200) return null;
if (num.bitLength() == 200) {
System.arraycopy(numBytes, 1, result, 0, 25);
} else {
System.arraycopy(numBytes, 0, result, result.length - numBytes.length, numBytes.length);
}
return result;
}
private static byte[] sha256(byte[] data) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(data);
return md.digest();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e);
}
}
//
// validate Bech32 segwit
// see https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki for spec
//
private static final String DATA_CHARS = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
public static boolean validateBech32Segwit(String bech32, BitcoinAddressType type, boolean testnet) {
if (!bech32.equals(bech32.toLowerCase()) && !bech32.equals(bech32.toUpperCase())) {
return false; // mixing upper and lower case not allowed
}
bech32 = bech32.toLowerCase();
if (!bech32.startsWith(type.getBech32Prefix(testnet))) return false;
final int hrpLength = type.getBech32Prefix(testnet).length();
if ((bech32.length() < (12 + hrpLength)) || (bech32.length() > (72 + hrpLength)))
return false;
int mod = (bech32.length() - hrpLength) % 8;
if ((mod == 6) || (mod == 1) || (mod == 3)) return false;
int sep = -1;
final byte[] bytes = bech32.getBytes(StandardCharsets.US_ASCII);
for (int i = 0; i < bytes.length; i++) {
if ((bytes[i] < 33) || (bytes[i] > 126)) {
return false;
}
if (bytes[i] == 49) sep = i; // 49 := '1' in ASCII
}
if (sep != hrpLength) return false;
if (sep > bytes.length - 7) {
return false; // min 6 bytes data
}
if (bytes.length < 8) { // hrp{min}=1 + sep=1 + data{min}=6 := 8
return false; // too short
}
if (bytes.length > 90) {
return false; // too long
}
final byte[] hrp = Arrays.copyOfRange(bytes, 0, sep);
final byte[] data = Arrays.copyOfRange(bytes, sep + 1, bytes.length);
for (int i = 0; i < data.length; i++) {
int b = DATA_CHARS.indexOf(data[i]);
if (b < 0) return false; // invalid character
data[i] = (byte) b;
}
if (!validateBech32Data(data)) return false;
return verifyChecksum(hrp, data);
}
private static int polymod(byte[] values) {
final int[] GEN = {0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3};
int chk = 1;
for (byte v : values) {
byte b = (byte) (chk >> 25);
chk = ((chk & 0x1ffffff) << 5) ^ v;
for (int i = 0; i < 5; i++) {
chk ^= ((b >> i) & 1) == 1 ? GEN[i] : 0;
}
}
return chk;
}
private static byte[] hrpExpand(byte[] hrp) {
final byte[] expanded = new byte[(2 * hrp.length) + 1];
int i = 0;
for (byte b : hrp) {
expanded[i++] = (byte) (b >> 5);
}
expanded[i++] = 0;
for (byte b : hrp) {
expanded[i++] = (byte) (b & 0x1f);
}
return expanded;
}
private static boolean verifyChecksum(byte[] hrp, byte[] data) {
final byte[] hrpExpanded = hrpExpand(hrp);
final byte[] values = new byte[hrpExpanded.length + data.length];
System.arraycopy(hrpExpanded, 0, values, 0, hrpExpanded.length);
System.arraycopy(data, 0, values, hrpExpanded.length, data.length);
return (polymod(values) == 1);
}
private static boolean validateBech32Data(final byte[] data) {
if ((data[0] < 0) || (data[0] > 16)) return false; // witness version
final int programLength = data.length - 1 - 6; // 1-byte version at beginning & 6-byte checksum at end
// since we are coming from our own decoder, we don't need to verify data is 5-bit bytes
final int convertedSize = programLength * 5 / 8;
final int remainderSize = programLength * 5 % 8;
if ((convertedSize < 2) || (convertedSize > 40)) return false;
if ((data[0] == 0) && (convertedSize != 20) && (convertedSize != 32)) return false;
if (remainderSize >= 5) return false;
// ignore checksum at end and get last byte of program
if ((data[data.length - 1 - 6] & ((1 << remainderSize) - 1)) != 0) return false;
return true;
}
}

Some files were not shown because too many files have changed in this diff Show More