mirror of
https://codeberg.org/r4v3r23/mysu.git
synced 2024-11-09 20:53:47 +01:00
Remove a lot of classes and cleanup code
This commit is contained in:
parent
460f6bd1b2
commit
209984417b
@ -28,8 +28,9 @@
|
||||
<activity
|
||||
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
||||
android:screenOrientation="portrait"
|
||||
tools:replace="android:screenOrientation"
|
||||
android:stateNotNeeded="true"/>
|
||||
android:stateNotNeeded="true"
|
||||
tools:replace="android:screenOrientation" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
|
@ -438,7 +438,8 @@ its rights to a jury trial in any resulting litigation.
|
||||
<h2>Licensed under the MIT License</h2>
|
||||
<h3>rapidjson (https://github.com/monero-project/monero/blob/master/external/rapidjson)</h3>
|
||||
Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. All rights reserved.
|
||||
<h3>easylogging++ (https://github.com/monero-project/monero/tree/master/external/easylogging%2B%2B)</h3>
|
||||
<h3>easylogging++
|
||||
(https://github.com/monero-project/monero/tree/master/external/easylogging%2B%2B)</h3>
|
||||
Copyright (c) 2017 muflihun.com
|
||||
<h3>zxcvbn4j (https://github.com/nulab/zxcvbn4j)</h3>
|
||||
Copyright (c) 2014 Nulab Inc
|
||||
@ -605,7 +606,9 @@ copied and put under another distribution licence
|
||||
<h2>Boost</h2>
|
||||
<ul>
|
||||
<li>Boost (https://sourceforge.net/projects/boost)</li>
|
||||
<li>Boost/Archive (https://github.com/monero-project/monero/tree/master/external/boost/archive)</li>
|
||||
<li>Boost/Archive
|
||||
(https://github.com/monero-project/monero/tree/master/external/boost/archive)
|
||||
</li>
|
||||
</ul>
|
||||
<h3>Boost Software License - Version 1.0 - August 17th, 2003</h3>
|
||||
Permission is hereby granted, free of charge, to any person or organization
|
||||
@ -766,12 +769,14 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
<h2>'Poppins' Font</h2>
|
||||
<h3>SIL Open Font License</h3>
|
||||
<p>Copyright (c) 2014, Indian Type Foundry (info@indiantypefoundry.com).</p>
|
||||
<p>This Font Software is licensed under the SIL Open Font License, Version 1.1.<br />
|
||||
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL</p>
|
||||
<p>—————————————————————————————-<br />
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007<br />
|
||||
—————————————————————————————-</p>
|
||||
<p>PREAMBLE<br />
|
||||
<p>This Font Software is licensed under the SIL Open Font License, Version 1.1.<br/>
|
||||
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
|
||||
</p>
|
||||
<p>—————————————————————————————-<br/>
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007<br/>
|
||||
—————————————————————————————-
|
||||
</p>
|
||||
<p>PREAMBLE<br/>
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide development of
|
||||
collaborative font projects, to support the font creation efforts of academic and
|
||||
linguistic communities, and to provide a free and open framework in which fonts may be
|
||||
@ -782,7 +787,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
names are not used by derivative works. The fonts and derivatives, however, cannot be
|
||||
released under any other type of license. The requirement for fonts to remain under this
|
||||
license does not apply to any document created using the fonts or their derivatives.</p>
|
||||
<p>DEFINITIONS<br />
|
||||
<p>DEFINITIONS<br/>
|
||||
“Font Software” refers to the set of files released by the Copyright Holder(s)
|
||||
under this license and clearly marked as such. This may include source files, build scripts
|
||||
and documentation.</p>
|
||||
@ -795,7 +800,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
by changing formats or by porting the Font Software to a new environment.</p>
|
||||
<p>“Author” refers to any designer, engineer, programmer, technical writer or other
|
||||
person who contributed to the Font Software.</p>
|
||||
<p>PERMISSION & CONDITIONS<br />
|
||||
<p>PERMISSION & CONDITIONS<br/>
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font
|
||||
Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and
|
||||
unmodified copies of the Font Software, subject to the following conditions:</p>
|
||||
@ -817,9 +822,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
under this license, and must not be distributed under any other license. The requirement
|
||||
for fonts to remain under this license does not apply to any document created using the Font
|
||||
Software.</p>
|
||||
<p>TERMINATION<br />
|
||||
<p>TERMINATION<br/>
|
||||
This license becomes null and void if any of the above conditions are not met.</p>
|
||||
<p>DISCLAIMER<br />
|
||||
<p>DISCLAIMER<br/>
|
||||
THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN
|
||||
|
@ -60,8 +60,6 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
|
||||
jenv->FindClass("com/m2049r/xmrwallet/model/Transfer")));
|
||||
class_WalletListener = static_cast<jclass>(jenv->NewGlobalRef(
|
||||
jenv->FindClass("com/m2049r/xmrwallet/model/WalletListener")));
|
||||
class_Ledger = static_cast<jclass>(jenv->NewGlobalRef(
|
||||
jenv->FindClass("com/m2049r/xmrwallet/ledger/Ledger")));
|
||||
class_WalletStatus = static_cast<jclass>(jenv->NewGlobalRef(
|
||||
jenv->FindClass("com/m2049r/xmrwallet/model/Wallet$Status")));
|
||||
return JNI_VERSION_1_6;
|
||||
@ -705,7 +703,8 @@ JNIEXPORT jboolean JNICALL
|
||||
Java_com_m2049r_xmrwallet_model_Wallet_initJ(JNIEnv *env, jobject instance,
|
||||
jstring daemon_address,
|
||||
jlong upper_transaction_size_limit,
|
||||
jstring daemon_username, jstring daemon_password, jstring proxy) {
|
||||
jstring daemon_username, jstring daemon_password,
|
||||
jstring proxy) {
|
||||
const char *_daemon_address = env->GetStringUTFChars(daemon_address, nullptr);
|
||||
const char *_daemon_username = env->GetStringUTFChars(daemon_username, nullptr);
|
||||
const char *_daemon_password = env->GetStringUTFChars(daemon_password, nullptr);
|
||||
|
@ -54,7 +54,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,14 +62,16 @@ 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
|
||||
|
@ -1,53 +0,0 @@
|
||||
/*
|
||||
*******************************************************************************
|
||||
* BTChip Bitcoin Hardware Wallet Java API
|
||||
* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn
|
||||
*
|
||||
* 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.btchip;
|
||||
|
||||
public class BTChipException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 5512803003827126405L;
|
||||
|
||||
public BTChipException(String reason) {
|
||||
super(reason);
|
||||
}
|
||||
|
||||
public BTChipException(String reason, Throwable cause) {
|
||||
super(reason, cause);
|
||||
}
|
||||
|
||||
public BTChipException(String reason, int sw) {
|
||||
super(reason);
|
||||
this.sw = sw;
|
||||
}
|
||||
|
||||
public int getSW() {
|
||||
return sw;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
if (sw == 0) {
|
||||
return "BTChip Exception : " + getMessage();
|
||||
} else {
|
||||
return "BTChip Exception : " + getMessage() + " " + Integer.toHexString(sw);
|
||||
}
|
||||
}
|
||||
|
||||
private int sw;
|
||||
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
/*
|
||||
*******************************************************************************
|
||||
* BTChip Bitcoin Hardware Wallet Java API
|
||||
* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn
|
||||
* (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.btchip.comm;
|
||||
|
||||
import com.btchip.BTChipException;
|
||||
|
||||
public interface BTChipTransport {
|
||||
byte[] exchange(byte[] command);
|
||||
|
||||
void close();
|
||||
|
||||
void setDebug(boolean debugFlag);
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
/*
|
||||
*******************************************************************************
|
||||
* BTChip Bitcoin Hardware Wallet Java API
|
||||
* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn
|
||||
* (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.btchip.comm;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
public class LedgerHelper {
|
||||
|
||||
private static final int TAG_APDU = 0x05;
|
||||
|
||||
public static byte[] wrapCommandAPDU(int channel, byte[] command, int packetSize) {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
if (packetSize < 3) {
|
||||
throw new IllegalArgumentException("Can't handle Ledger framing with less than 3 bytes for the report");
|
||||
}
|
||||
int sequenceIdx = 0;
|
||||
int offset = 0;
|
||||
output.write(channel >> 8);
|
||||
output.write(channel);
|
||||
output.write(TAG_APDU);
|
||||
output.write(sequenceIdx >> 8);
|
||||
output.write(sequenceIdx);
|
||||
sequenceIdx++;
|
||||
output.write(command.length >> 8);
|
||||
output.write(command.length);
|
||||
int blockSize = (command.length > packetSize - 7 ? packetSize - 7 : command.length);
|
||||
output.write(command, offset, blockSize);
|
||||
offset += blockSize;
|
||||
while (offset != command.length) {
|
||||
output.write(channel >> 8);
|
||||
output.write(channel);
|
||||
output.write(TAG_APDU);
|
||||
output.write(sequenceIdx >> 8);
|
||||
output.write(sequenceIdx);
|
||||
sequenceIdx++;
|
||||
blockSize = (command.length - offset > packetSize - 5 ? packetSize - 5 : command.length - offset);
|
||||
output.write(command, offset, blockSize);
|
||||
offset += blockSize;
|
||||
}
|
||||
if ((output.size() % packetSize) != 0) {
|
||||
byte[] padding = new byte[packetSize - (output.size() % packetSize)];
|
||||
output.write(padding, 0, padding.length);
|
||||
}
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
public static byte[] unwrapResponseAPDU(int channel, byte[] data, int packetSize) {
|
||||
ByteArrayOutputStream response = new ByteArrayOutputStream();
|
||||
int offset = 0;
|
||||
int responseLength;
|
||||
int sequenceIdx = 0;
|
||||
if ((data == null) || (data.length < 7 + 5)) {
|
||||
return null;
|
||||
}
|
||||
if (data[offset++] != (channel >> 8)) {
|
||||
throw new IllegalArgumentException("Invalid channel");
|
||||
}
|
||||
if (data[offset++] != (channel & 0xff)) {
|
||||
throw new IllegalArgumentException("Invalid channel");
|
||||
}
|
||||
if (data[offset++] != TAG_APDU) {
|
||||
throw new IllegalArgumentException("Invalid tag");
|
||||
}
|
||||
if (data[offset++] != 0x00) {
|
||||
throw new IllegalArgumentException("Invalid sequence");
|
||||
}
|
||||
if (data[offset++] != 0x00) {
|
||||
throw new IllegalArgumentException("Invalid sequence");
|
||||
}
|
||||
responseLength = ((data[offset++] & 0xff) << 8);
|
||||
responseLength |= (data[offset++] & 0xff);
|
||||
if (data.length < 7 + responseLength) {
|
||||
return null;
|
||||
}
|
||||
int blockSize = (responseLength > packetSize - 7 ? packetSize - 7 : responseLength);
|
||||
response.write(data, offset, blockSize);
|
||||
offset += blockSize;
|
||||
while (response.size() != responseLength) {
|
||||
sequenceIdx++;
|
||||
if (offset == data.length) {
|
||||
return null;
|
||||
}
|
||||
if (data[offset++] != (channel >> 8)) {
|
||||
throw new IllegalArgumentException("Invalid channel");
|
||||
}
|
||||
if (data[offset++] != (channel & 0xff)) {
|
||||
throw new IllegalArgumentException("Invalid channel");
|
||||
}
|
||||
if (data[offset++] != TAG_APDU) {
|
||||
throw new IllegalArgumentException("Invalid tag");
|
||||
}
|
||||
if (data[offset++] != (sequenceIdx >> 8)) {
|
||||
throw new IllegalArgumentException("Invalid sequence");
|
||||
}
|
||||
if (data[offset++] != (sequenceIdx & 0xff)) {
|
||||
throw new IllegalArgumentException("Invalid sequence");
|
||||
}
|
||||
blockSize = (responseLength - response.size() > packetSize - 5 ? packetSize - 5 : responseLength - response.size());
|
||||
if (blockSize > data.length - offset) {
|
||||
return null;
|
||||
}
|
||||
response.write(data, offset, blockSize);
|
||||
offset += blockSize;
|
||||
}
|
||||
return response.toByteArray();
|
||||
}
|
||||
|
||||
}
|
@ -1,149 +0,0 @@
|
||||
/*
|
||||
*******************************************************************************
|
||||
* BTChip Bitcoin Hardware Wallet Java API
|
||||
* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn
|
||||
* (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.btchip.comm.android;
|
||||
|
||||
import android.hardware.usb.UsbConstants;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbDeviceConnection;
|
||||
import android.hardware.usb.UsbEndpoint;
|
||||
import android.hardware.usb.UsbInterface;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import android.hardware.usb.UsbRequest;
|
||||
|
||||
import com.btchip.BTChipException;
|
||||
import com.btchip.comm.BTChipTransport;
|
||||
import com.btchip.comm.LedgerHelper;
|
||||
import com.btchip.utils.Dump;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.HashMap;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class BTChipTransportAndroidHID implements BTChipTransport {
|
||||
|
||||
public static UsbDevice getDevice(UsbManager manager) {
|
||||
HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
|
||||
for (UsbDevice device : deviceList.values()) {
|
||||
Timber.d("%04X:%04X %s, %s", device.getVendorId(), device.getProductId(), device.getManufacturerName(), device.getProductName());
|
||||
if (device.getVendorId() == VID) {
|
||||
final int deviceProductId = device.getProductId();
|
||||
for (int pid : PID_HIDS) {
|
||||
if (deviceProductId == pid)
|
||||
return device;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static BTChipTransport open(UsbManager manager, UsbDevice device) throws IOException {
|
||||
UsbDeviceConnection connection = manager.openDevice(device);
|
||||
if (connection == null) throw new IOException("Device not connected");
|
||||
// Must only be called once permission is granted (see http://developer.android.com/reference/android/hardware/usb/UsbManager.html)
|
||||
// Important if enumerating, rather than being awaken by the intent notification
|
||||
UsbInterface dongleInterface = device.getInterface(0);
|
||||
UsbEndpoint in = null;
|
||||
UsbEndpoint out = null;
|
||||
for (int i = 0; i < dongleInterface.getEndpointCount(); i++) {
|
||||
UsbEndpoint tmpEndpoint = dongleInterface.getEndpoint(i);
|
||||
if (tmpEndpoint.getDirection() == UsbConstants.USB_DIR_IN) {
|
||||
in = tmpEndpoint;
|
||||
} else {
|
||||
out = tmpEndpoint;
|
||||
}
|
||||
}
|
||||
connection.claimInterface(dongleInterface, true);
|
||||
return new BTChipTransportAndroidHID(connection, dongleInterface, in, out);
|
||||
}
|
||||
|
||||
private static final int VID = 0x2C97;
|
||||
private static final int[] PID_HIDS = {0x0001, 0x0004, 0x0005};
|
||||
|
||||
private UsbDeviceConnection connection;
|
||||
private UsbInterface dongleInterface;
|
||||
private UsbEndpoint in;
|
||||
private UsbEndpoint out;
|
||||
private byte transferBuffer[];
|
||||
private boolean debug;
|
||||
|
||||
public BTChipTransportAndroidHID(UsbDeviceConnection connection, UsbInterface dongleInterface, UsbEndpoint in, UsbEndpoint out) {
|
||||
this.connection = connection;
|
||||
this.dongleInterface = dongleInterface;
|
||||
this.in = in;
|
||||
this.out = out;
|
||||
transferBuffer = new byte[HID_BUFFER_SIZE];
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] exchange(byte[] command) {
|
||||
ByteArrayOutputStream response = new ByteArrayOutputStream();
|
||||
byte[] responseData = null;
|
||||
int offset = 0;
|
||||
if (debug) {
|
||||
Timber.d("=> %s", Dump.dump(command));
|
||||
}
|
||||
command = LedgerHelper.wrapCommandAPDU(LEDGER_DEFAULT_CHANNEL, command, HID_BUFFER_SIZE);
|
||||
UsbRequest requestOut = new UsbRequest();
|
||||
requestOut.initialize(connection, out);
|
||||
while (offset != command.length) {
|
||||
int blockSize = (command.length - offset > HID_BUFFER_SIZE ? HID_BUFFER_SIZE : command.length - offset);
|
||||
System.arraycopy(command, offset, transferBuffer, 0, blockSize);
|
||||
requestOut.queue(ByteBuffer.wrap(transferBuffer), HID_BUFFER_SIZE);
|
||||
connection.requestWait();
|
||||
offset += blockSize;
|
||||
}
|
||||
requestOut.close();
|
||||
ByteBuffer responseBuffer = ByteBuffer.allocate(HID_BUFFER_SIZE);
|
||||
UsbRequest requestIn = new UsbRequest();
|
||||
requestIn.initialize(connection, in);
|
||||
while ((responseData = LedgerHelper.unwrapResponseAPDU(LEDGER_DEFAULT_CHANNEL, response.toByteArray(), HID_BUFFER_SIZE)) == null) {
|
||||
responseBuffer.clear();
|
||||
requestIn.queue(responseBuffer, HID_BUFFER_SIZE);
|
||||
connection.requestWait();
|
||||
responseBuffer.rewind();
|
||||
responseBuffer.get(transferBuffer, 0, HID_BUFFER_SIZE);
|
||||
response.write(transferBuffer, 0, HID_BUFFER_SIZE);
|
||||
}
|
||||
requestIn.close();
|
||||
if (debug) {
|
||||
Timber.d("<= %s", Dump.dump(responseData));
|
||||
}
|
||||
return responseData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
connection.releaseInterface(dongleInterface);
|
||||
connection.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDebug(boolean debugFlag) {
|
||||
this.debug = debugFlag;
|
||||
}
|
||||
|
||||
private static final int HID_BUFFER_SIZE = 64;
|
||||
private static final int LEDGER_DEFAULT_CHANNEL = 1;
|
||||
private static final int SW1_DATA_AVAILABLE = 0x61;
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
/*
|
||||
*******************************************************************************
|
||||
* BTChip Bitcoin Hardware Wallet Java API
|
||||
* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn
|
||||
*
|
||||
* 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.btchip.utils;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
public class Dump {
|
||||
|
||||
public static String dump(byte[] buffer, int offset, int length) {
|
||||
String result = "";
|
||||
for (int i = 0; i < length; i++) {
|
||||
String temp = Integer.toHexString((buffer[offset + i]) & 0xff);
|
||||
if (temp.length() < 2) {
|
||||
temp = "0" + temp;
|
||||
}
|
||||
result += temp;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String dump(byte[] buffer) {
|
||||
return dump(buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
public static byte[] hexToBin(String src) {
|
||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
||||
int i = 0;
|
||||
while (i < src.length()) {
|
||||
char x = src.charAt(i);
|
||||
if (!((x >= '0' && x <= '9') || (x >= 'A' && x <= 'F') || (x >= 'a' && x <= 'f'))) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
result.write(Integer.valueOf("" + src.charAt(i) + src.charAt(i + 1), 16));
|
||||
i += 2;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return result.toByteArray();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,145 +0,0 @@
|
||||
/*
|
||||
* 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.levin.data;
|
||||
|
||||
import com.m2049r.levin.util.HexHelper;
|
||||
import com.m2049r.levin.util.LevinReader;
|
||||
|
||||
import java.io.DataInput;
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
|
||||
public class Bucket {
|
||||
|
||||
// constants copied from monero p2p & epee
|
||||
|
||||
public final static int P2P_COMMANDS_POOL_BASE = 1000;
|
||||
public final static int COMMAND_HANDSHAKE_ID = P2P_COMMANDS_POOL_BASE + 1;
|
||||
public final static int COMMAND_TIMED_SYNC_ID = P2P_COMMANDS_POOL_BASE + 2;
|
||||
public final static int COMMAND_PING_ID = P2P_COMMANDS_POOL_BASE + 3;
|
||||
public final static int COMMAND_REQUEST_STAT_INFO_ID = P2P_COMMANDS_POOL_BASE + 4;
|
||||
public final static int COMMAND_REQUEST_NETWORK_STATE_ID = P2P_COMMANDS_POOL_BASE + 5;
|
||||
public final static int COMMAND_REQUEST_PEER_ID_ID = P2P_COMMANDS_POOL_BASE + 6;
|
||||
public final static int COMMAND_REQUEST_SUPPORT_FLAGS_ID = P2P_COMMANDS_POOL_BASE + 7;
|
||||
|
||||
public final static long LEVIN_SIGNATURE = 0x0101010101012101L; // Bender's nightmare
|
||||
|
||||
public final static long LEVIN_DEFAULT_MAX_PACKET_SIZE = 100000000; // 100MB by default
|
||||
|
||||
public final static int LEVIN_PACKET_REQUEST = 0x00000001;
|
||||
public final static int LEVIN_PACKET_RESPONSE = 0x00000002;
|
||||
|
||||
public final static int LEVIN_PROTOCOL_VER_0 = 0;
|
||||
public final static int LEVIN_PROTOCOL_VER_1 = 1;
|
||||
|
||||
public final static int LEVIN_OK = 0;
|
||||
public final static int LEVIN_ERROR_CONNECTION = -1;
|
||||
public final static int LEVIN_ERROR_CONNECTION_NOT_FOUND = -2;
|
||||
public final static int LEVIN_ERROR_CONNECTION_DESTROYED = -3;
|
||||
public final static int LEVIN_ERROR_CONNECTION_TIMEDOUT = -4;
|
||||
public final static int LEVIN_ERROR_CONNECTION_NO_DUPLEX_PROTOCOL = -5;
|
||||
public final static int LEVIN_ERROR_CONNECTION_HANDLER_NOT_DEFINED = -6;
|
||||
public final static int LEVIN_ERROR_FORMAT = -7;
|
||||
|
||||
public final static int P2P_SUPPORT_FLAG_FLUFFY_BLOCKS = 0x01;
|
||||
public final static int P2P_SUPPORT_FLAGS = P2P_SUPPORT_FLAG_FLUFFY_BLOCKS;
|
||||
|
||||
final private long signature;
|
||||
final private long cb;
|
||||
final public boolean haveToReturnData;
|
||||
final public int command;
|
||||
final public int returnCode;
|
||||
final private int flags;
|
||||
final private int protcolVersion;
|
||||
final byte[] payload;
|
||||
|
||||
final public Section payloadSection;
|
||||
|
||||
// create a request
|
||||
public Bucket(int command, byte[] payload) throws IOException {
|
||||
this.signature = LEVIN_SIGNATURE;
|
||||
this.cb = payload.length;
|
||||
this.haveToReturnData = true;
|
||||
this.command = command;
|
||||
this.returnCode = 0;
|
||||
this.flags = LEVIN_PACKET_REQUEST;
|
||||
this.protcolVersion = LEVIN_PROTOCOL_VER_1;
|
||||
this.payload = payload;
|
||||
payloadSection = LevinReader.readPayload(payload);
|
||||
}
|
||||
|
||||
// create a response
|
||||
public Bucket(int command, byte[] payload, int rc) throws IOException {
|
||||
this.signature = LEVIN_SIGNATURE;
|
||||
this.cb = payload.length;
|
||||
this.haveToReturnData = false;
|
||||
this.command = command;
|
||||
this.returnCode = rc;
|
||||
this.flags = LEVIN_PACKET_RESPONSE;
|
||||
this.protcolVersion = LEVIN_PROTOCOL_VER_1;
|
||||
this.payload = payload;
|
||||
payloadSection = LevinReader.readPayload(payload);
|
||||
}
|
||||
|
||||
public Bucket(DataInput in) throws IOException {
|
||||
signature = in.readLong();
|
||||
cb = in.readLong();
|
||||
haveToReturnData = in.readBoolean();
|
||||
command = in.readInt();
|
||||
returnCode = in.readInt();
|
||||
flags = in.readInt();
|
||||
protcolVersion = in.readInt();
|
||||
|
||||
if (signature == Bucket.LEVIN_SIGNATURE) {
|
||||
if (cb > Integer.MAX_VALUE)
|
||||
throw new IllegalArgumentException();
|
||||
payload = new byte[(int) cb];
|
||||
in.readFully(payload);
|
||||
} else
|
||||
throw new IllegalStateException();
|
||||
payloadSection = LevinReader.readPayload(payload);
|
||||
}
|
||||
|
||||
public Section getPayloadSection() {
|
||||
return payloadSection;
|
||||
}
|
||||
|
||||
public void send(DataOutput out) throws IOException {
|
||||
out.writeLong(signature);
|
||||
out.writeLong(cb);
|
||||
out.writeBoolean(haveToReturnData);
|
||||
out.writeInt(command);
|
||||
out.writeInt(returnCode);
|
||||
out.writeInt(flags);
|
||||
out.writeInt(protcolVersion);
|
||||
out.write(payload);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("sig: ").append(signature).append("\n");
|
||||
sb.append("cb: ").append(cb).append("\n");
|
||||
sb.append("call: ").append(haveToReturnData).append("\n");
|
||||
sb.append("cmd: ").append(command).append("\n");
|
||||
sb.append("rc: ").append(returnCode).append("\n");
|
||||
sb.append("flags:").append(flags).append("\n");
|
||||
sb.append("proto:").append(protcolVersion).append("\n");
|
||||
sb.append(HexHelper.bytesToHex(payload)).append("\n");
|
||||
sb.append(payloadSection.toString());
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
/*
|
||||
* 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.levin.data;
|
||||
|
||||
import com.m2049r.levin.util.HexHelper;
|
||||
import com.m2049r.levin.util.LevinReader;
|
||||
import com.m2049r.levin.util.LevinWriter;
|
||||
import com.m2049r.levin.util.LittleEndianDataOutputStream;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class Section {
|
||||
|
||||
// constants copied from monero p2p & epee
|
||||
|
||||
static final public int PORTABLE_STORAGE_SIGNATUREA = 0x01011101;
|
||||
static final public int PORTABLE_STORAGE_SIGNATUREB = 0x01020101;
|
||||
|
||||
static final public byte PORTABLE_STORAGE_FORMAT_VER = 1;
|
||||
|
||||
static final public byte PORTABLE_RAW_SIZE_MARK_MASK = 0x03;
|
||||
static final public byte PORTABLE_RAW_SIZE_MARK_BYTE = 0;
|
||||
static final public byte PORTABLE_RAW_SIZE_MARK_WORD = 1;
|
||||
static final public byte PORTABLE_RAW_SIZE_MARK_DWORD = 2;
|
||||
static final public byte PORTABLE_RAW_SIZE_MARK_INT64 = 3;
|
||||
|
||||
static final long MAX_STRING_LEN_POSSIBLE = 2000000000; // do not let string be so big
|
||||
|
||||
// data types
|
||||
static final public byte SERIALIZE_TYPE_INT64 = 1;
|
||||
static final public byte SERIALIZE_TYPE_INT32 = 2;
|
||||
static final public byte SERIALIZE_TYPE_INT16 = 3;
|
||||
static final public byte SERIALIZE_TYPE_INT8 = 4;
|
||||
static final public byte SERIALIZE_TYPE_UINT64 = 5;
|
||||
static final public byte SERIALIZE_TYPE_UINT32 = 6;
|
||||
static final public byte SERIALIZE_TYPE_UINT16 = 7;
|
||||
static final public byte SERIALIZE_TYPE_UINT8 = 8;
|
||||
static final public byte SERIALIZE_TYPE_DUOBLE = 9;
|
||||
static final public byte SERIALIZE_TYPE_STRING = 10;
|
||||
static final public byte SERIALIZE_TYPE_BOOL = 11;
|
||||
static final public byte SERIALIZE_TYPE_OBJECT = 12;
|
||||
static final public byte SERIALIZE_TYPE_ARRAY = 13;
|
||||
|
||||
static final public byte SERIALIZE_FLAG_ARRAY = (byte) 0x80;
|
||||
|
||||
private final Map<String, Object> entries = new HashMap<String, Object>();
|
||||
|
||||
public void add(String key, Object entry) {
|
||||
entries.put(key, entry);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
public Set<Map.Entry<String, Object>> entrySet() {
|
||||
return entries.entrySet();
|
||||
}
|
||||
|
||||
public Object get(String key) {
|
||||
return entries.get(key);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
for (Map.Entry<String, Object> entry : entries.entrySet()) {
|
||||
sb.append(entry.getKey()).append("=");
|
||||
final Object value = entry.getValue();
|
||||
if (value instanceof List) {
|
||||
@SuppressWarnings("unchecked") final List<Object> list = (List<Object>) value;
|
||||
for (Object listEntry : list) {
|
||||
sb.append(listEntry.toString()).append("\n");
|
||||
}
|
||||
} else if (value instanceof String) {
|
||||
sb.append("(").append(value).append(")\n");
|
||||
} else if (value instanceof byte[]) {
|
||||
sb.append(HexHelper.bytesToHex((byte[]) value)).append("\n");
|
||||
} else {
|
||||
sb.append(value.toString()).append("\n");
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static public Section fromByteArray(byte[] buffer) {
|
||||
try {
|
||||
return LevinReader.readPayload(buffer);
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] asByteArray() {
|
||||
try {
|
||||
ByteArrayOutputStream bas = new ByteArrayOutputStream();
|
||||
DataOutput out = new LittleEndianDataOutputStream(bas);
|
||||
LevinWriter writer = new LevinWriter(out);
|
||||
writer.writePayload(this);
|
||||
return bas.toByteArray();
|
||||
} catch (IOException ex) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,195 +0,0 @@
|
||||
/*
|
||||
* 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.levin.scanner;
|
||||
|
||||
import com.m2049r.xmrwallet.data.NodeInfo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class Dispatcher implements PeerRetriever.OnGetPeers {
|
||||
static final public int NUM_THREADS = 50;
|
||||
static final public int MAX_PEERS = 1000;
|
||||
static final public long MAX_TIME = 30000000000L; //30 seconds
|
||||
|
||||
private int peerCount = 0;
|
||||
final private Set<NodeInfo> knownNodes = new HashSet<>(); // set of nodes to test
|
||||
final private Set<NodeInfo> rpcNodes = new HashSet<>(); // set of RPC nodes we like
|
||||
final private ExecutorService exeService = Executors.newFixedThreadPool(NUM_THREADS);
|
||||
|
||||
public interface Listener {
|
||||
void onGet(NodeInfo nodeInfo);
|
||||
}
|
||||
|
||||
private Listener listener;
|
||||
|
||||
public Dispatcher(Listener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public Set<NodeInfo> getRpcNodes() {
|
||||
return rpcNodes;
|
||||
}
|
||||
|
||||
public int getPeerCount() {
|
||||
return peerCount;
|
||||
}
|
||||
|
||||
public boolean getMorePeers() {
|
||||
return peerCount < MAX_PEERS;
|
||||
}
|
||||
|
||||
public void awaitTermination(int nodesToFind) {
|
||||
try {
|
||||
final long t = System.nanoTime();
|
||||
while (!jobs.isEmpty()) {
|
||||
try {
|
||||
Timber.d("Remaining jobs %d", jobs.size());
|
||||
final PeerRetriever retrievedPeer = jobs.poll().get();
|
||||
if (retrievedPeer.isGood() && getMorePeers())
|
||||
retrievePeers(retrievedPeer);
|
||||
final NodeInfo nodeInfo = retrievedPeer.getNodeInfo();
|
||||
Timber.d("Retrieved %s", nodeInfo);
|
||||
if ((nodeInfo.isValid() || nodeInfo.isFavourite())) {
|
||||
nodeInfo.setDefaultName();
|
||||
rpcNodes.add(nodeInfo);
|
||||
Timber.d("RPC: %s", nodeInfo);
|
||||
// the following is not totally correct but it works (otherwise we need to
|
||||
// load much more before filtering - but we don't have time
|
||||
if (listener != null) listener.onGet(nodeInfo);
|
||||
if (rpcNodes.size() >= nodesToFind) {
|
||||
Timber.d("are we done here?");
|
||||
filterRpcNodes();
|
||||
if (rpcNodes.size() >= nodesToFind) {
|
||||
Timber.d("we're done here");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (System.nanoTime() - t > MAX_TIME) break; // watchdog
|
||||
} catch (ExecutionException ex) {
|
||||
Timber.d(ex); // tell us about it and continue
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
Timber.d(ex);
|
||||
} finally {
|
||||
Timber.d("Shutting down!");
|
||||
exeService.shutdownNow();
|
||||
try {
|
||||
exeService.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
|
||||
} catch (InterruptedException ex) {
|
||||
Timber.d(ex);
|
||||
}
|
||||
}
|
||||
filterRpcNodes();
|
||||
}
|
||||
|
||||
static final public int HEIGHT_WINDOW = 1;
|
||||
|
||||
private boolean testHeight(long height, long consensus) {
|
||||
return (height >= (consensus - HEIGHT_WINDOW))
|
||||
&& (height <= (consensus + HEIGHT_WINDOW));
|
||||
}
|
||||
|
||||
private long calcConsensusHeight() {
|
||||
Timber.d("Calc Consensus height from %d nodes", rpcNodes.size());
|
||||
final Map<Long, Integer> nodeHeights = new TreeMap<Long, Integer>();
|
||||
for (NodeInfo info : rpcNodes) {
|
||||
if (!info.isValid()) continue;
|
||||
Integer h = nodeHeights.get(info.getHeight());
|
||||
if (h == null)
|
||||
h = 0;
|
||||
nodeHeights.put(info.getHeight(), h + 1);
|
||||
}
|
||||
long consensusHeight = 0;
|
||||
long consensusCount = 0;
|
||||
for (Map.Entry<Long, Integer> entry : nodeHeights.entrySet()) {
|
||||
final long entryHeight = entry.getKey();
|
||||
int count = 0;
|
||||
for (long i = entryHeight - HEIGHT_WINDOW; i <= entryHeight + HEIGHT_WINDOW; i++) {
|
||||
Integer v = nodeHeights.get(i);
|
||||
if (v == null)
|
||||
v = 0;
|
||||
count += v;
|
||||
}
|
||||
if (count >= consensusCount) {
|
||||
consensusCount = count;
|
||||
consensusHeight = entryHeight;
|
||||
}
|
||||
Timber.d("%d - %d/%d", entryHeight, count, entry.getValue());
|
||||
}
|
||||
return consensusHeight;
|
||||
}
|
||||
|
||||
private void filterRpcNodes() {
|
||||
long consensus = calcConsensusHeight();
|
||||
Timber.d("Consensus Height = %d for %d nodes", consensus, rpcNodes.size());
|
||||
for (Iterator<NodeInfo> iter = rpcNodes.iterator(); iter.hasNext(); ) {
|
||||
NodeInfo info = iter.next();
|
||||
// don't remove favourites
|
||||
if (!info.isFavourite()) {
|
||||
if (!testHeight(info.getHeight(), consensus)) {
|
||||
iter.remove();
|
||||
Timber.d("Removed %s", info);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: does this NEED to be a ConcurrentLinkedDeque?
|
||||
private ConcurrentLinkedDeque<Future<PeerRetriever>> jobs = new ConcurrentLinkedDeque<>();
|
||||
|
||||
private void retrievePeer(NodeInfo nodeInfo) {
|
||||
if (knownNodes.add(nodeInfo)) {
|
||||
Timber.d("\t%d:%s", knownNodes.size(), nodeInfo);
|
||||
jobs.add(exeService.submit(new PeerRetriever(nodeInfo, this)));
|
||||
peerCount++; // jobs.size() does not perform well
|
||||
}
|
||||
}
|
||||
|
||||
private void retrievePeers(PeerRetriever peer) {
|
||||
for (LevinPeer levinPeer : peer.getPeers()) {
|
||||
if (getMorePeers())
|
||||
retrievePeer(new NodeInfo(levinPeer));
|
||||
else
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void seedPeers(Collection<NodeInfo> seedNodes) {
|
||||
for (NodeInfo node : seedNodes) {
|
||||
if (node.isFavourite()) {
|
||||
rpcNodes.add(node);
|
||||
if (listener != null) listener.onGet(node);
|
||||
}
|
||||
retrievePeer(node);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
/*
|
||||
* 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.levin.scanner;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
public class LevinPeer {
|
||||
final public InetSocketAddress socketAddress;
|
||||
final public int version;
|
||||
final public long height;
|
||||
final public String top;
|
||||
|
||||
|
||||
public InetSocketAddress getSocketAddress() {
|
||||
return socketAddress;
|
||||
}
|
||||
|
||||
LevinPeer(InetAddress address, int port, int version, long height, String top) {
|
||||
this.socketAddress = new InetSocketAddress(address, port);
|
||||
this.version = version;
|
||||
this.height = height;
|
||||
this.top = top;
|
||||
}
|
||||
}
|
@ -1,231 +0,0 @@
|
||||
/*
|
||||
* 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.levin.scanner;
|
||||
|
||||
import com.m2049r.levin.data.Bucket;
|
||||
import com.m2049r.levin.data.Section;
|
||||
import com.m2049r.levin.util.HexHelper;
|
||||
import com.m2049r.levin.util.LittleEndianDataInputStream;
|
||||
import com.m2049r.levin.util.LittleEndianDataOutputStream;
|
||||
import com.m2049r.xmrwallet.data.NodeInfo;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import java.io.DataInput;
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class PeerRetriever implements Callable<PeerRetriever> {
|
||||
static final public int CONNECT_TIMEOUT = 500; //ms
|
||||
static final public int SOCKET_TIMEOUT = 500; //ms
|
||||
static final public long PEER_ID = new Random().nextLong();
|
||||
static final private byte[] HANDSHAKE = handshakeRequest().asByteArray();
|
||||
static final private byte[] FLAGS_RESP = flagsResponse().asByteArray();
|
||||
|
||||
final private List<LevinPeer> peers = new ArrayList<>();
|
||||
|
||||
private NodeInfo nodeInfo;
|
||||
private OnGetPeers onGetPeersCallback;
|
||||
|
||||
public interface OnGetPeers {
|
||||
boolean getMorePeers();
|
||||
}
|
||||
|
||||
public PeerRetriever(NodeInfo nodeInfo, OnGetPeers onGetPeers) {
|
||||
this.nodeInfo = nodeInfo;
|
||||
this.onGetPeersCallback = onGetPeers;
|
||||
}
|
||||
|
||||
public NodeInfo getNodeInfo() {
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
public boolean isGood() {
|
||||
return !peers.isEmpty();
|
||||
}
|
||||
|
||||
public List<LevinPeer> getPeers() {
|
||||
return peers;
|
||||
}
|
||||
|
||||
public PeerRetriever call() {
|
||||
if (isGood()) // we have already been called?
|
||||
throw new IllegalStateException();
|
||||
// first check for an rpc service
|
||||
nodeInfo.findRpcService();
|
||||
if (onGetPeersCallback.getMorePeers())
|
||||
try {
|
||||
Timber.d("%s CONN", nodeInfo.getLevinSocketAddress());
|
||||
if (!connect())
|
||||
return this;
|
||||
Bucket handshakeBucket = new Bucket(Bucket.COMMAND_HANDSHAKE_ID, HANDSHAKE);
|
||||
handshakeBucket.send(getDataOutput());
|
||||
|
||||
while (true) {// wait for response (which may never come)
|
||||
Bucket recv = new Bucket(getDataInput()); // times out after SOCKET_TIMEOUT
|
||||
if ((recv.command == Bucket.COMMAND_HANDSHAKE_ID)
|
||||
&& (!recv.haveToReturnData)) {
|
||||
readAddressList(recv.payloadSection);
|
||||
return this;
|
||||
} else if ((recv.command == Bucket.COMMAND_REQUEST_SUPPORT_FLAGS_ID)
|
||||
&& (recv.haveToReturnData)) {
|
||||
Bucket flagsBucket = new Bucket(Bucket.COMMAND_REQUEST_SUPPORT_FLAGS_ID, FLAGS_RESP, 1);
|
||||
flagsBucket.send(getDataOutput());
|
||||
} else {// and ignore others
|
||||
Timber.d("Ignored LEVIN COMMAND %d", recv.command);
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
} finally {
|
||||
disconnect(); // we have what we want - byebye
|
||||
Timber.d("%s DISCONN", nodeInfo.getLevinSocketAddress());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private void readAddressList(Section section) {
|
||||
Section data = (Section) section.get("payload_data");
|
||||
int topVersion = (Integer) data.get("top_version");
|
||||
long currentHeight = (Long) data.get("current_height");
|
||||
String topId = HexHelper.bytesToHex((byte[]) data.get("top_id"));
|
||||
Timber.d("PAYLOAD_DATA %d/%d/%s", topVersion, currentHeight, topId);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Section> peerList = (List<Section>) section.get("local_peerlist_new");
|
||||
if (peerList != null) {
|
||||
for (Section peer : peerList) {
|
||||
Section adr = (Section) peer.get("adr");
|
||||
Integer type = (Integer) adr.get("type");
|
||||
if ((type == null) || (type != 1))
|
||||
continue;
|
||||
Section addr = (Section) adr.get("addr");
|
||||
if (addr == null)
|
||||
continue;
|
||||
Integer ip = (Integer) addr.get("m_ip");
|
||||
if (ip == null)
|
||||
continue;
|
||||
Integer sport = (Integer) addr.get("m_port");
|
||||
if (sport == null)
|
||||
continue;
|
||||
int port = sport;
|
||||
if (port < 0) // port is unsigned
|
||||
port = port + 0x10000;
|
||||
InetAddress inet = HexHelper.toInetAddress(ip);
|
||||
// make sure this is an address we want to talk to (i.e. a remote address)
|
||||
if (!inet.isSiteLocalAddress() && !inet.isAnyLocalAddress()
|
||||
&& !inet.isLoopbackAddress()
|
||||
&& !inet.isMulticastAddress()
|
||||
&& !inet.isLinkLocalAddress()) {
|
||||
peers.add(new LevinPeer(inet, port, topVersion, currentHeight, topId));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Socket socket = null;
|
||||
|
||||
private boolean connect() {
|
||||
if (socket != null) throw new IllegalStateException();
|
||||
try {
|
||||
socket = new Socket();
|
||||
socket.connect(nodeInfo.getLevinSocketAddress(), CONNECT_TIMEOUT);
|
||||
socket.setSoTimeout(SOCKET_TIMEOUT);
|
||||
} catch (IOException ex) {
|
||||
//Timber.d(ex);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isConnected() {
|
||||
return socket.isConnected();
|
||||
}
|
||||
|
||||
private void disconnect() {
|
||||
try {
|
||||
dataInput = null;
|
||||
dataOutput = null;
|
||||
if ((socket != null) && (!socket.isClosed())) {
|
||||
socket.close();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
Timber.d(ex);
|
||||
} finally {
|
||||
socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
private DataOutput dataOutput = null;
|
||||
|
||||
private DataOutput getDataOutput() throws IOException {
|
||||
if (dataOutput == null)
|
||||
synchronized (this) {
|
||||
if (dataOutput == null)
|
||||
dataOutput = new LittleEndianDataOutputStream(
|
||||
socket.getOutputStream());
|
||||
}
|
||||
return dataOutput;
|
||||
}
|
||||
|
||||
private DataInput dataInput = null;
|
||||
|
||||
private DataInput getDataInput() throws IOException {
|
||||
if (dataInput == null)
|
||||
synchronized (this) {
|
||||
if (dataInput == null)
|
||||
dataInput = new LittleEndianDataInputStream(
|
||||
socket.getInputStream());
|
||||
}
|
||||
return dataInput;
|
||||
}
|
||||
|
||||
static private Section handshakeRequest() {
|
||||
Section section = new Section(); // root object
|
||||
|
||||
Section nodeData = new Section();
|
||||
nodeData.add("local_time", (new Date()).getTime());
|
||||
nodeData.add("my_port", 0);
|
||||
byte[] networkId = Helper.hexToBytes("1230f171610441611731008216a1a110"); // mainnet
|
||||
nodeData.add("network_id", networkId);
|
||||
nodeData.add("peer_id", PEER_ID);
|
||||
section.add("node_data", nodeData);
|
||||
|
||||
Section payloadData = new Section();
|
||||
payloadData.add("cumulative_difficulty", 1L);
|
||||
payloadData.add("current_height", 1L);
|
||||
byte[] genesisHash =
|
||||
Helper.hexToBytes("418015bb9ae982a1975da7d79277c2705727a56894ba0fb246adaabb1f4632e3");
|
||||
payloadData.add("top_id", genesisHash);
|
||||
payloadData.add("top_version", (byte) 1);
|
||||
section.add("payload_data", payloadData);
|
||||
return section;
|
||||
}
|
||||
|
||||
static private Section flagsResponse() {
|
||||
Section section = new Section(); // root object
|
||||
section.add("support_flags", Bucket.P2P_SUPPORT_FLAGS);
|
||||
return section;
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
/*
|
||||
* 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.levin.util;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
public class HexHelper {
|
||||
|
||||
static public String bytesToHex(byte[] data) {
|
||||
if ((data != null) && (data.length > 0))
|
||||
return String.format("%0" + (data.length * 2) + "X",
|
||||
new BigInteger(1, data));
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
static public InetAddress toInetAddress(int ip) {
|
||||
try {
|
||||
String ipAddress = String.format("%d.%d.%d.%d", (ip & 0xff),
|
||||
(ip >> 8 & 0xff), (ip >> 16 & 0xff), (ip >> 24 & 0xff));
|
||||
return InetAddress.getByName(ipAddress);
|
||||
} catch (UnknownHostException ex) {
|
||||
throw new IllegalArgumentException(ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,184 +0,0 @@
|
||||
/*
|
||||
* 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.levin.util;
|
||||
|
||||
import com.m2049r.levin.data.Section;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInput;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
// Full Levin reader as seen on epee
|
||||
|
||||
public class LevinReader {
|
||||
private DataInput in;
|
||||
|
||||
private LevinReader(byte[] buffer) {
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(buffer);
|
||||
in = new LittleEndianDataInputStream(bis);
|
||||
}
|
||||
|
||||
static public Section readPayload(byte[] payload) throws IOException {
|
||||
LevinReader r = new LevinReader(payload);
|
||||
return r.readPayload();
|
||||
}
|
||||
|
||||
private Section readPayload() throws IOException {
|
||||
if (in.readInt() != Section.PORTABLE_STORAGE_SIGNATUREA)
|
||||
throw new IllegalStateException();
|
||||
if (in.readInt() != Section.PORTABLE_STORAGE_SIGNATUREB)
|
||||
throw new IllegalStateException();
|
||||
if (in.readByte() != Section.PORTABLE_STORAGE_FORMAT_VER)
|
||||
throw new IllegalStateException();
|
||||
return readSection();
|
||||
}
|
||||
|
||||
private Section readSection() throws IOException {
|
||||
Section section = new Section();
|
||||
long count = readVarint();
|
||||
while (count-- > 0) {
|
||||
// read section name string
|
||||
String sectionName = readSectionName();
|
||||
section.add(sectionName, loadStorageEntry());
|
||||
}
|
||||
return section;
|
||||
}
|
||||
|
||||
private Object loadStorageArrayEntry(int type) throws IOException {
|
||||
type &= ~Section.SERIALIZE_FLAG_ARRAY;
|
||||
return readArrayEntry(type);
|
||||
}
|
||||
|
||||
private List<Object> readArrayEntry(int type) throws IOException {
|
||||
List<Object> list = new ArrayList<Object>();
|
||||
long size = readVarint();
|
||||
while (size-- > 0)
|
||||
list.add(read(type));
|
||||
return list;
|
||||
}
|
||||
|
||||
private Object read(int type) throws IOException {
|
||||
switch (type) {
|
||||
case Section.SERIALIZE_TYPE_UINT64:
|
||||
case Section.SERIALIZE_TYPE_INT64:
|
||||
return in.readLong();
|
||||
case Section.SERIALIZE_TYPE_UINT32:
|
||||
case Section.SERIALIZE_TYPE_INT32:
|
||||
return in.readInt();
|
||||
case Section.SERIALIZE_TYPE_UINT16:
|
||||
return in.readUnsignedShort();
|
||||
case Section.SERIALIZE_TYPE_INT16:
|
||||
return in.readShort();
|
||||
case Section.SERIALIZE_TYPE_UINT8:
|
||||
return in.readUnsignedByte();
|
||||
case Section.SERIALIZE_TYPE_INT8:
|
||||
return in.readByte();
|
||||
case Section.SERIALIZE_TYPE_OBJECT:
|
||||
return readSection();
|
||||
case Section.SERIALIZE_TYPE_STRING:
|
||||
return readByteArray();
|
||||
default:
|
||||
throw new IllegalArgumentException("type " + type
|
||||
+ " not supported");
|
||||
}
|
||||
}
|
||||
|
||||
private Object loadStorageEntry() throws IOException {
|
||||
int type = in.readUnsignedByte();
|
||||
if ((type & Section.SERIALIZE_FLAG_ARRAY) != 0)
|
||||
return loadStorageArrayEntry(type);
|
||||
if (type == Section.SERIALIZE_TYPE_ARRAY)
|
||||
return readStorageEntryArrayEntry();
|
||||
else
|
||||
return readStorageEntry(type);
|
||||
}
|
||||
|
||||
private Object readStorageEntry(int type) throws IOException {
|
||||
return read(type);
|
||||
}
|
||||
|
||||
private Object readStorageEntryArrayEntry() throws IOException {
|
||||
int type = in.readUnsignedByte();
|
||||
if ((type & Section.SERIALIZE_FLAG_ARRAY) != 0)
|
||||
throw new IllegalStateException("wrong type sequences");
|
||||
return loadStorageArrayEntry(type);
|
||||
}
|
||||
|
||||
private String readSectionName() throws IOException {
|
||||
int nameLen = in.readUnsignedByte();
|
||||
return readString(nameLen);
|
||||
}
|
||||
|
||||
private byte[] read(long count) throws IOException {
|
||||
if (count > Integer.MAX_VALUE)
|
||||
throw new IllegalArgumentException();
|
||||
int len = (int) count;
|
||||
final byte buffer[] = new byte[len];
|
||||
in.readFully(buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private String readString(long count) throws IOException {
|
||||
return new String(read(count), StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
private byte[] readByteArray(long count) throws IOException {
|
||||
return read(count);
|
||||
}
|
||||
|
||||
private byte[] readByteArray() throws IOException {
|
||||
long len = readVarint();
|
||||
return readByteArray(len);
|
||||
}
|
||||
|
||||
private long readVarint() throws IOException {
|
||||
long v = 0;
|
||||
int b = in.readUnsignedByte();
|
||||
int sizeMask = b & Section.PORTABLE_RAW_SIZE_MARK_MASK;
|
||||
switch (sizeMask) {
|
||||
case Section.PORTABLE_RAW_SIZE_MARK_BYTE:
|
||||
v = b >>> 2;
|
||||
break;
|
||||
case Section.PORTABLE_RAW_SIZE_MARK_WORD:
|
||||
v = readRest(b, 1) >>> 2;
|
||||
break;
|
||||
case Section.PORTABLE_RAW_SIZE_MARK_DWORD:
|
||||
v = readRest(b, 3) >>> 2;
|
||||
break;
|
||||
case Section.PORTABLE_RAW_SIZE_MARK_INT64:
|
||||
v = readRest(b, 7) >>> 2;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// this should be in LittleEndianDataInputStream because it has little
|
||||
// endian logic
|
||||
private long readRest(final int firstByte, final int bytes) throws IOException {
|
||||
long result = firstByte;
|
||||
for (int i = 1; i < bytes + 1; i++) {
|
||||
result = result + (((long) in.readUnsignedByte()) << (8 * i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
/*
|
||||
* 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.levin.util;
|
||||
|
||||
import com.m2049r.levin.data.Section;
|
||||
|
||||
import java.io.DataOutput;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
// a simplified Levin Writer WITHOUT support for arrays
|
||||
|
||||
public class LevinWriter {
|
||||
private DataOutput out;
|
||||
|
||||
public LevinWriter(DataOutput out) {
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
public void writePayload(Section section) throws IOException {
|
||||
out.writeInt(Section.PORTABLE_STORAGE_SIGNATUREA);
|
||||
out.writeInt(Section.PORTABLE_STORAGE_SIGNATUREB);
|
||||
out.writeByte(Section.PORTABLE_STORAGE_FORMAT_VER);
|
||||
putSection(section);
|
||||
}
|
||||
|
||||
private void writeSection(Section section) throws IOException {
|
||||
out.writeByte(Section.SERIALIZE_TYPE_OBJECT);
|
||||
putSection(section);
|
||||
}
|
||||
|
||||
private void putSection(Section section) throws IOException {
|
||||
writeVarint(section.size());
|
||||
for (Map.Entry<String, Object> kv : section.entrySet()) {
|
||||
byte[] key = kv.getKey().getBytes(StandardCharsets.US_ASCII);
|
||||
out.writeByte(key.length);
|
||||
out.write(key);
|
||||
write(kv.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private void writeVarint(long i) throws IOException {
|
||||
if (i <= 63) {
|
||||
out.writeByte(((int) i << 2) | Section.PORTABLE_RAW_SIZE_MARK_BYTE);
|
||||
} else if (i <= 16383) {
|
||||
out.writeShort(((int) i << 2) | Section.PORTABLE_RAW_SIZE_MARK_WORD);
|
||||
} else if (i <= 1073741823) {
|
||||
out.writeInt(((int) i << 2) | Section.PORTABLE_RAW_SIZE_MARK_DWORD);
|
||||
} else {
|
||||
if (i > 4611686018427387903L)
|
||||
throw new IllegalArgumentException();
|
||||
out.writeLong((i << 2) | Section.PORTABLE_RAW_SIZE_MARK_INT64);
|
||||
}
|
||||
}
|
||||
|
||||
private void write(Object object) throws IOException {
|
||||
if (object instanceof byte[]) {
|
||||
byte[] value = (byte[]) object;
|
||||
out.writeByte(Section.SERIALIZE_TYPE_STRING);
|
||||
writeVarint(value.length);
|
||||
out.write(value);
|
||||
} else if (object instanceof String) {
|
||||
byte[] value = ((String) object)
|
||||
.getBytes(StandardCharsets.US_ASCII);
|
||||
out.writeByte(Section.SERIALIZE_TYPE_STRING);
|
||||
writeVarint(value.length);
|
||||
out.write(value);
|
||||
} else if (object instanceof Integer) {
|
||||
out.writeByte(Section.SERIALIZE_TYPE_UINT32);
|
||||
out.writeInt((int) object);
|
||||
} else if (object instanceof Long) {
|
||||
out.writeByte(Section.SERIALIZE_TYPE_UINT64);
|
||||
out.writeLong((long) object);
|
||||
} else if (object instanceof Byte) {
|
||||
out.writeByte(Section.SERIALIZE_TYPE_UINT8);
|
||||
out.writeByte((byte) object);
|
||||
} else if (object instanceof Section) {
|
||||
writeSection((Section) object);
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,564 +0,0 @@
|
||||
/*
|
||||
* 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.levin.util;
|
||||
|
||||
import java.io.DataInput;
|
||||
import java.io.EOFException;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UTFDataFormatException;
|
||||
|
||||
/**
|
||||
* A little endian java.io.DataInputStream (without readLine())
|
||||
*/
|
||||
|
||||
public class LittleEndianDataInputStream extends FilterInputStream implements
|
||||
DataInput {
|
||||
|
||||
/**
|
||||
* Creates a DataInputStream that uses the specified underlying InputStream.
|
||||
*
|
||||
* @param in the specified input stream
|
||||
*/
|
||||
public LittleEndianDataInputStream(InputStream in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public final String readLine() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads some number of bytes from the contained input stream and stores
|
||||
* them into the buffer array <code>b</code>. The number of bytes actually
|
||||
* read is returned as an integer. This method blocks until input data is
|
||||
* available, end of file is detected, or an exception is thrown.
|
||||
*
|
||||
* <p>
|
||||
* If <code>b</code> is null, a <code>NullPointerException</code> is thrown.
|
||||
* If the length of <code>b</code> is zero, then no bytes are read and
|
||||
* <code>0</code> is returned; otherwise, there is an attempt to read at
|
||||
* least one byte. If no byte is available because the stream is at end of
|
||||
* file, the value <code>-1</code> is returned; otherwise, at least one byte
|
||||
* is read and stored into <code>b</code>.
|
||||
*
|
||||
* <p>
|
||||
* The first byte read is stored into element <code>b[0]</code>, the next
|
||||
* one into <code>b[1]</code>, and so on. The number of bytes read is, at
|
||||
* most, equal to the length of <code>b</code>. Let <code>k</code> be the
|
||||
* number of bytes actually read; these bytes will be stored in elements
|
||||
* <code>b[0]</code> through <code>b[k-1]</code>, leaving elements
|
||||
* <code>b[k]</code> through <code>b[b.length-1]</code> unaffected.
|
||||
*
|
||||
* <p>
|
||||
* The <code>read(b)</code> method has the same effect as: <blockquote>
|
||||
*
|
||||
* <pre>
|
||||
* read(b, 0, b.length)
|
||||
* </pre>
|
||||
*
|
||||
* </blockquote>
|
||||
*
|
||||
* @param b the buffer into which the data is read.
|
||||
* @return the total number of bytes read into the buffer, or
|
||||
* <code>-1</code> if there is no more data because the end of the
|
||||
* stream has been reached.
|
||||
* @throws IOException if the first byte cannot be read for any reason other than
|
||||
* end of file, the stream has been closed and the underlying
|
||||
* input stream does not support reading after close, or
|
||||
* another I/O error occurs.
|
||||
* @see FilterInputStream#in
|
||||
* @see InputStream#read(byte[], int, int)
|
||||
*/
|
||||
public final int read(byte b[]) throws IOException {
|
||||
return in.read(b, 0, b.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads up to <code>len</code> bytes of data from the contained input
|
||||
* stream into an array of bytes. An attempt is made to read as many as
|
||||
* <code>len</code> bytes, but a smaller number may be read, possibly zero.
|
||||
* The number of bytes actually read is returned as an integer.
|
||||
*
|
||||
* <p>
|
||||
* This method blocks until input data is available, end of file is
|
||||
* detected, or an exception is thrown.
|
||||
*
|
||||
* <p>
|
||||
* If <code>len</code> is zero, then no bytes are read and <code>0</code> is
|
||||
* returned; otherwise, there is an attempt to read at least one byte. If no
|
||||
* byte is available because the stream is at end of file, the value
|
||||
* <code>-1</code> is returned; otherwise, at least one byte is read and
|
||||
* stored into <code>b</code>.
|
||||
*
|
||||
* <p>
|
||||
* The first byte read is stored into element <code>b[off]</code>, the next
|
||||
* one into <code>b[off+1]</code>, and so on. The number of bytes read is,
|
||||
* at most, equal to <code>len</code>. Let <i>k</i> be the number of bytes
|
||||
* actually read; these bytes will be stored in elements <code>b[off]</code>
|
||||
* through <code>b[off+</code><i>k</i><code>-1]</code>, leaving elements
|
||||
* <code>b[off+</code><i>k</i><code>]</code> through
|
||||
* <code>b[off+len-1]</code> unaffected.
|
||||
*
|
||||
* <p>
|
||||
* In every case, elements <code>b[0]</code> through <code>b[off]</code> and
|
||||
* elements <code>b[off+len]</code> through <code>b[b.length-1]</code> are
|
||||
* unaffected.
|
||||
*
|
||||
* @param b the buffer into which the data is read.
|
||||
* @param off the start offset in the destination array <code>b</code>
|
||||
* @param len the maximum number of bytes read.
|
||||
* @return the total number of bytes read into the buffer, or
|
||||
* <code>-1</code> if there is no more data because the end of the
|
||||
* stream has been reached.
|
||||
* @throws NullPointerException If <code>b</code> is <code>null</code>.
|
||||
* @throws IndexOutOfBoundsException If <code>off</code> is negative, <code>len</code> is
|
||||
* negative, or <code>len</code> is greater than
|
||||
* <code>b.length - off</code>
|
||||
* @throws IOException if the first byte cannot be read for any reason other than
|
||||
* end of file, the stream has been closed and the underlying
|
||||
* input stream does not support reading after close, or
|
||||
* another I/O error occurs.
|
||||
* @see FilterInputStream#in
|
||||
* @see InputStream#read(byte[], int, int)
|
||||
*/
|
||||
public final int read(byte b[], int off, int len) throws IOException {
|
||||
return in.read(b, off, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readFully</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @param b the buffer into which the data is read.
|
||||
* @throws EOFException if this input stream reaches the end before reading all
|
||||
* the bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final void readFully(byte b[]) throws IOException {
|
||||
readFully(b, 0, b.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readFully</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @param b the buffer into which the data is read.
|
||||
* @param off the start offset of the data.
|
||||
* @param len the number of bytes to read.
|
||||
* @throws EOFException if this input stream reaches the end before reading all
|
||||
* the bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final void readFully(byte b[], int off, int len) throws IOException {
|
||||
if (len < 0)
|
||||
throw new IndexOutOfBoundsException();
|
||||
int n = 0;
|
||||
while (n < len) {
|
||||
int count = in.read(b, off + n, len - n);
|
||||
if (count < 0)
|
||||
throw new EOFException();
|
||||
n += count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>skipBytes</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @param n the number of bytes to be skipped.
|
||||
* @return the actual number of bytes skipped.
|
||||
* @throws IOException if the contained input stream does not support seek, or
|
||||
* the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
*/
|
||||
public final int skipBytes(int n) throws IOException {
|
||||
int total = 0;
|
||||
int cur = 0;
|
||||
|
||||
while ((total < n) && ((cur = (int) in.skip(n - total)) > 0)) {
|
||||
total += cur;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readBoolean</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the <code>boolean</code> value read.
|
||||
* @throws EOFException if this input stream has reached the end.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final boolean readBoolean() throws IOException {
|
||||
int ch = in.read();
|
||||
if (ch < 0)
|
||||
throw new EOFException();
|
||||
return (ch != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readByte</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next byte of this input stream as a signed 8-bit
|
||||
* <code>byte</code>.
|
||||
* @throws EOFException if this input stream has reached the end.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final byte readByte() throws IOException {
|
||||
int ch = in.read();
|
||||
if (ch < 0)
|
||||
throw new EOFException();
|
||||
return (byte) (ch);
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readUnsignedByte</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next byte of this input stream, interpreted as an unsigned
|
||||
* 8-bit number.
|
||||
* @throws EOFException if this input stream has reached the end.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final int readUnsignedByte() throws IOException {
|
||||
int ch = in.read();
|
||||
if (ch < 0)
|
||||
throw new EOFException();
|
||||
return ch;
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readShort</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next two bytes of this input stream, interpreted as a signed
|
||||
* 16-bit number.
|
||||
* @throws EOFException if this input stream reaches the end before reading two
|
||||
* bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final short readShort() throws IOException {
|
||||
int ch1 = in.read();
|
||||
int ch2 = in.read();
|
||||
if ((ch1 | ch2) < 0)
|
||||
throw new EOFException();
|
||||
return (short) ((ch1 << 0) + (ch2 << 8));
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readUnsignedShort</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next two bytes of this input stream, interpreted as an
|
||||
* unsigned 16-bit integer.
|
||||
* @throws EOFException if this input stream reaches the end before reading two
|
||||
* bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final int readUnsignedShort() throws IOException {
|
||||
int ch1 = in.read();
|
||||
int ch2 = in.read();
|
||||
if ((ch1 | ch2) < 0)
|
||||
throw new EOFException();
|
||||
return (ch1 << 0) + (ch2 << 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readChar</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next two bytes of this input stream, interpreted as a
|
||||
* <code>char</code>.
|
||||
* @throws EOFException if this input stream reaches the end before reading two
|
||||
* bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final char readChar() throws IOException {
|
||||
int ch1 = in.read();
|
||||
int ch2 = in.read();
|
||||
if ((ch1 | ch2) < 0)
|
||||
throw new EOFException();
|
||||
return (char) ((ch1 << 0) + (ch2 << 8));
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readInt</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next four bytes of this input stream, interpreted as an
|
||||
* <code>int</code>.
|
||||
* @throws EOFException if this input stream reaches the end before reading four
|
||||
* bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final int readInt() throws IOException {
|
||||
int ch1 = in.read();
|
||||
int ch2 = in.read();
|
||||
int ch3 = in.read();
|
||||
int ch4 = in.read();
|
||||
if ((ch1 | ch2 | ch3 | ch4) < 0)
|
||||
throw new EOFException();
|
||||
return ((ch1 << 0) + (ch2 << 8) + (ch3 << 16) + (ch4 << 24));
|
||||
}
|
||||
|
||||
private byte readBuffer[] = new byte[8];
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readLong</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next eight bytes of this input stream, interpreted as a
|
||||
* <code>long</code>.
|
||||
* @throws EOFException if this input stream reaches the end before reading eight
|
||||
* bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see FilterInputStream#in
|
||||
*/
|
||||
public final long readLong() throws IOException {
|
||||
readFully(readBuffer, 0, 8);
|
||||
return (((long) readBuffer[7] << 56)
|
||||
+ ((long) (readBuffer[6] & 255) << 48)
|
||||
+ ((long) (readBuffer[5] & 255) << 40)
|
||||
+ ((long) (readBuffer[4] & 255) << 32)
|
||||
+ ((long) (readBuffer[3] & 255) << 24)
|
||||
+ ((readBuffer[2] & 255) << 16) + ((readBuffer[1] & 255) << 8) + ((readBuffer[0] & 255) << 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readFloat</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next four bytes of this input stream, interpreted as a
|
||||
* <code>float</code>.
|
||||
* @throws EOFException if this input stream reaches the end before reading four
|
||||
* bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see java.io.DataInputStream#readInt()
|
||||
* @see Float#intBitsToFloat(int)
|
||||
*/
|
||||
public final float readFloat() throws IOException {
|
||||
return Float.intBitsToFloat(readInt());
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readDouble</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return the next eight bytes of this input stream, interpreted as a
|
||||
* <code>double</code>.
|
||||
* @throws EOFException if this input stream reaches the end before reading eight
|
||||
* bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @see java.io.DataInputStream#readLong()
|
||||
* @see Double#longBitsToDouble(long)
|
||||
*/
|
||||
public final double readDouble() throws IOException {
|
||||
return Double.longBitsToDouble(readLong());
|
||||
}
|
||||
|
||||
/**
|
||||
* See the general contract of the <code>readUTF</code> method of
|
||||
* <code>DataInput</code>.
|
||||
* <p>
|
||||
* Bytes for this operation are read from the contained input stream.
|
||||
*
|
||||
* @return a Unicode string.
|
||||
* @throws EOFException if this input stream reaches the end before reading all
|
||||
* the bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @throws UTFDataFormatException if the bytes do not represent a valid modified UTF-8
|
||||
* encoding of a string.
|
||||
* @see java.io.DataInputStream#readUTF(DataInput)
|
||||
*/
|
||||
public final String readUTF() throws IOException {
|
||||
return readUTF(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* working arrays initialized on demand by readUTF
|
||||
*/
|
||||
private byte bytearr[] = new byte[80];
|
||||
private char chararr[] = new char[80];
|
||||
|
||||
/**
|
||||
* Reads from the stream <code>in</code> a representation of a Unicode
|
||||
* character string encoded in <a
|
||||
* href="DataInput.html#modified-utf-8">modified UTF-8</a> format; this
|
||||
* string of characters is then returned as a <code>String</code>. The
|
||||
* details of the modified UTF-8 representation are exactly the same as for
|
||||
* the <code>readUTF</code> method of <code>DataInput</code>.
|
||||
*
|
||||
* @param in a data input stream.
|
||||
* @return a Unicode string.
|
||||
* @throws EOFException if the input stream reaches the end before all the bytes.
|
||||
* @throws IOException the stream has been closed and the contained input stream
|
||||
* does not support reading after close, or another I/O error
|
||||
* occurs.
|
||||
* @throws UTFDataFormatException if the bytes do not represent a valid modified UTF-8
|
||||
* encoding of a Unicode string.
|
||||
* @see java.io.DataInputStream#readUnsignedShort()
|
||||
*/
|
||||
public final static String readUTF(DataInput in) throws IOException {
|
||||
int utflen = in.readUnsignedShort();
|
||||
byte[] bytearr = null;
|
||||
char[] chararr = null;
|
||||
if (in instanceof LittleEndianDataInputStream) {
|
||||
LittleEndianDataInputStream dis = (LittleEndianDataInputStream) in;
|
||||
if (dis.bytearr.length < utflen) {
|
||||
dis.bytearr = new byte[utflen * 2];
|
||||
dis.chararr = new char[utflen * 2];
|
||||
}
|
||||
chararr = dis.chararr;
|
||||
bytearr = dis.bytearr;
|
||||
} else {
|
||||
bytearr = new byte[utflen];
|
||||
chararr = new char[utflen];
|
||||
}
|
||||
|
||||
int c, char2, char3;
|
||||
int count = 0;
|
||||
int chararr_count = 0;
|
||||
|
||||
in.readFully(bytearr, 0, utflen);
|
||||
|
||||
while (count < utflen) {
|
||||
c = (int) bytearr[count] & 0xff;
|
||||
if (c > 127)
|
||||
break;
|
||||
count++;
|
||||
chararr[chararr_count++] = (char) c;
|
||||
}
|
||||
|
||||
while (count < utflen) {
|
||||
c = (int) bytearr[count] & 0xff;
|
||||
switch (c >> 4) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
case 6:
|
||||
case 7:
|
||||
/* 0xxxxxxx */
|
||||
count++;
|
||||
chararr[chararr_count++] = (char) c;
|
||||
break;
|
||||
case 12:
|
||||
case 13:
|
||||
/* 110x xxxx 10xx xxxx */
|
||||
count += 2;
|
||||
if (count > utflen)
|
||||
throw new UTFDataFormatException(
|
||||
"malformed input: partial character at end");
|
||||
char2 = (int) bytearr[count - 1];
|
||||
if ((char2 & 0xC0) != 0x80)
|
||||
throw new UTFDataFormatException(
|
||||
"malformed input around byte " + count);
|
||||
chararr[chararr_count++] = (char) (((c & 0x1F) << 6) | (char2 & 0x3F));
|
||||
break;
|
||||
case 14:
|
||||
/* 1110 xxxx 10xx xxxx 10xx xxxx */
|
||||
count += 3;
|
||||
if (count > utflen)
|
||||
throw new UTFDataFormatException(
|
||||
"malformed input: partial character at end");
|
||||
char2 = (int) bytearr[count - 2];
|
||||
char3 = (int) bytearr[count - 1];
|
||||
if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
|
||||
throw new UTFDataFormatException(
|
||||
"malformed input around byte " + (count - 1));
|
||||
chararr[chararr_count++] = (char) (((c & 0x0F) << 12)
|
||||
| ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0));
|
||||
break;
|
||||
default:
|
||||
/* 10xx xxxx, 1111 xxxx */
|
||||
throw new UTFDataFormatException("malformed input around byte "
|
||||
+ count);
|
||||
}
|
||||
}
|
||||
// The number of chars produced may be less than utflen
|
||||
return new String(chararr, 0, chararr_count);
|
||||
}
|
||||
}
|
@ -1,403 +0,0 @@
|
||||
/*
|
||||
* 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.levin.util;
|
||||
|
||||
import java.io.DataOutput;
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UTFDataFormatException;
|
||||
|
||||
/**
|
||||
* A little endian java.io.DataOutputStream
|
||||
*/
|
||||
|
||||
public class LittleEndianDataOutputStream extends FilterOutputStream implements
|
||||
DataOutput {
|
||||
|
||||
/**
|
||||
* The number of bytes written to the data output stream so far. If this
|
||||
* counter overflows, it will be wrapped to Integer.MAX_VALUE.
|
||||
*/
|
||||
protected int written;
|
||||
|
||||
/**
|
||||
* Creates a new data output stream to write data to the specified
|
||||
* underlying output stream. The counter <code>written</code> is set to
|
||||
* zero.
|
||||
*
|
||||
* @param out the underlying output stream, to be saved for later use.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public LittleEndianDataOutputStream(OutputStream out) {
|
||||
super(out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the written counter by the specified value until it reaches
|
||||
* Integer.MAX_VALUE.
|
||||
*/
|
||||
private void incCount(int value) {
|
||||
int temp = written + value;
|
||||
if (temp < 0) {
|
||||
temp = Integer.MAX_VALUE;
|
||||
}
|
||||
written = temp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the specified byte (the low eight bits of the argument
|
||||
* <code>b</code>) to the underlying output stream. If no exception is
|
||||
* thrown, the counter <code>written</code> is incremented by <code>1</code>
|
||||
* .
|
||||
* <p>
|
||||
* Implements the <code>write</code> method of <code>OutputStream</code>.
|
||||
*
|
||||
* @param b the <code>byte</code> to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public synchronized void write(int b) throws IOException {
|
||||
out.write(b);
|
||||
incCount(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes <code>len</code> bytes from the specified byte array starting at
|
||||
* offset <code>off</code> to the underlying output stream. If no exception
|
||||
* is thrown, the counter <code>written</code> is incremented by
|
||||
* <code>len</code>.
|
||||
*
|
||||
* @param b the data.
|
||||
* @param off the start offset in the data.
|
||||
* @param len the number of bytes to write.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public synchronized void write(byte b[], int off, int len)
|
||||
throws IOException {
|
||||
out.write(b, off, len);
|
||||
incCount(len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes this data output stream. This forces any buffered output bytes to
|
||||
* be written out to the stream.
|
||||
* <p>
|
||||
* The <code>flush</code> method of <code>DataOutputStream</code> calls the
|
||||
* <code>flush</code> method of its underlying output stream.
|
||||
*
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
* @see OutputStream#flush()
|
||||
*/
|
||||
public void flush() throws IOException {
|
||||
out.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a <code>boolean</code> to the underlying output stream as a 1-byte
|
||||
* value. The value <code>true</code> is written out as the value
|
||||
* <code>(byte)1</code>; the value <code>false</code> is written out as the
|
||||
* value <code>(byte)0</code>. If no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by <code>1</code>.
|
||||
*
|
||||
* @param v a <code>boolean</code> value to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeBoolean(boolean v) throws IOException {
|
||||
out.write(v ? 1 : 0);
|
||||
incCount(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes out a <code>byte</code> to the underlying output stream as a
|
||||
* 1-byte value. If no exception is thrown, the counter <code>written</code>
|
||||
* is incremented by <code>1</code>.
|
||||
*
|
||||
* @param v a <code>byte</code> value to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeByte(int v) throws IOException {
|
||||
out.write(v);
|
||||
incCount(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a <code>short</code> to the underlying output stream as two bytes,
|
||||
* low byte first. If no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by <code>2</code>.
|
||||
*
|
||||
* @param v a <code>short</code> to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeShort(int v) throws IOException {
|
||||
out.write((v >>> 0) & 0xFF);
|
||||
out.write((v >>> 8) & 0xFF);
|
||||
incCount(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a <code>char</code> to the underlying output stream as a 2-byte
|
||||
* value, low byte first. If no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by <code>2</code>.
|
||||
*
|
||||
* @param v a <code>char</code> value to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeChar(int v) throws IOException {
|
||||
out.write((v >>> 0) & 0xFF);
|
||||
out.write((v >>> 8) & 0xFF);
|
||||
incCount(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an <code>int</code> to the underlying output stream as four bytes,
|
||||
* low byte first. If no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by <code>4</code>.
|
||||
*
|
||||
* @param v an <code>int</code> to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeInt(int v) throws IOException {
|
||||
out.write((v >>> 0) & 0xFF);
|
||||
out.write((v >>> 8) & 0xFF);
|
||||
out.write((v >>> 16) & 0xFF);
|
||||
out.write((v >>> 24) & 0xFF);
|
||||
incCount(4);
|
||||
}
|
||||
|
||||
private byte writeBuffer[] = new byte[8];
|
||||
|
||||
/**
|
||||
* Writes a <code>long</code> to the underlying output stream as eight
|
||||
* bytes, low byte first. In no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by <code>8</code>.
|
||||
*
|
||||
* @param v a <code>long</code> to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeLong(long v) throws IOException {
|
||||
writeBuffer[7] = (byte) (v >>> 56);
|
||||
writeBuffer[6] = (byte) (v >>> 48);
|
||||
writeBuffer[5] = (byte) (v >>> 40);
|
||||
writeBuffer[4] = (byte) (v >>> 32);
|
||||
writeBuffer[3] = (byte) (v >>> 24);
|
||||
writeBuffer[2] = (byte) (v >>> 16);
|
||||
writeBuffer[1] = (byte) (v >>> 8);
|
||||
writeBuffer[0] = (byte) (v >>> 0);
|
||||
out.write(writeBuffer, 0, 8);
|
||||
incCount(8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the float argument to an <code>int</code> using the
|
||||
* <code>floatToIntBits</code> method in class <code>Float</code>, and then
|
||||
* writes that <code>int</code> value to the underlying output stream as a
|
||||
* 4-byte quantity, low byte first. If no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by <code>4</code>.
|
||||
*
|
||||
* @param v a <code>float</code> value to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
* @see Float#floatToIntBits(float)
|
||||
*/
|
||||
public final void writeFloat(float v) throws IOException {
|
||||
writeInt(Float.floatToIntBits(v));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the double argument to a <code>long</code> using the
|
||||
* <code>doubleToLongBits</code> method in class <code>Double</code>, and
|
||||
* then writes that <code>long</code> value to the underlying output stream
|
||||
* as an 8-byte quantity, low byte first. If no exception is thrown, the
|
||||
* counter <code>written</code> is incremented by <code>8</code>.
|
||||
*
|
||||
* @param v a <code>double</code> value to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
* @see Double#doubleToLongBits(double)
|
||||
*/
|
||||
public final void writeDouble(double v) throws IOException {
|
||||
writeLong(Double.doubleToLongBits(v));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes out the string to the underlying output stream as a sequence of
|
||||
* bytes. Each character in the string is written out, in sequence, by
|
||||
* discarding its high eight bits. If no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by the length of <code>s</code>.
|
||||
*
|
||||
* @param s a string of bytes to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeBytes(String s) throws IOException {
|
||||
int len = s.length();
|
||||
for (int i = 0; i < len; i++) {
|
||||
out.write((byte) s.charAt(i));
|
||||
}
|
||||
incCount(len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a string to the underlying output stream as a sequence of
|
||||
* characters. Each character is written to the data output stream as if by
|
||||
* the <code>writeChar</code> method. If no exception is thrown, the counter
|
||||
* <code>written</code> is incremented by twice the length of <code>s</code>
|
||||
* .
|
||||
*
|
||||
* @param s a <code>String</code> value to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
* @see java.io.DataOutputStream#writeChar(int)
|
||||
* @see FilterOutputStream#out
|
||||
*/
|
||||
public final void writeChars(String s) throws IOException {
|
||||
int len = s.length();
|
||||
for (int i = 0; i < len; i++) {
|
||||
int v = s.charAt(i);
|
||||
out.write((v >>> 0) & 0xFF);
|
||||
out.write((v >>> 8) & 0xFF);
|
||||
}
|
||||
incCount(len * 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a string to the underlying output stream using <a
|
||||
* href="DataInput.html#modified-utf-8">modified UTF-8</a> encoding in a
|
||||
* machine-independent manner.
|
||||
* <p>
|
||||
* First, two bytes are written to the output stream as if by the
|
||||
* <code>writeShort</code> method giving the number of bytes to follow. This
|
||||
* value is the number of bytes actually written out, not the length of the
|
||||
* string. Following the length, each character of the string is output, in
|
||||
* sequence, using the modified UTF-8 encoding for the character. If no
|
||||
* exception is thrown, the counter <code>written</code> is incremented by
|
||||
* the total number of bytes written to the output stream. This will be at
|
||||
* least two plus the length of <code>str</code>, and at most two plus
|
||||
* thrice the length of <code>str</code>.
|
||||
*
|
||||
* @param str a string to be written.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
public final void writeUTF(String str) throws IOException {
|
||||
writeUTF(str, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* bytearr is initialized on demand by writeUTF
|
||||
*/
|
||||
private byte[] bytearr = null;
|
||||
|
||||
/**
|
||||
* Writes a string to the specified DataOutput using <a
|
||||
* href="DataInput.html#modified-utf-8">modified UTF-8</a> encoding in a
|
||||
* machine-independent manner.
|
||||
* <p>
|
||||
* First, two bytes are written to out as if by the <code>writeShort</code>
|
||||
* method giving the number of bytes to follow. This value is the number of
|
||||
* bytes actually written out, not the length of the string. Following the
|
||||
* length, each character of the string is output, in sequence, using the
|
||||
* modified UTF-8 encoding for the character. If no exception is thrown, the
|
||||
* counter <code>written</code> is incremented by the total number of bytes
|
||||
* written to the output stream. This will be at least two plus the length
|
||||
* of <code>str</code>, and at most two plus thrice the length of
|
||||
* <code>str</code>.
|
||||
*
|
||||
* @param str a string to be written.
|
||||
* @param out destination to write to
|
||||
* @return The number of bytes written out.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
static int writeUTF(String str, DataOutput out) throws IOException {
|
||||
int strlen = str.length();
|
||||
int utflen = 0;
|
||||
int c, count = 0;
|
||||
|
||||
/* use charAt instead of copying String to char array */
|
||||
for (int i = 0; i < strlen; i++) {
|
||||
c = str.charAt(i);
|
||||
if ((c >= 0x0001) && (c <= 0x007F)) {
|
||||
utflen++;
|
||||
} else if (c > 0x07FF) {
|
||||
utflen += 3;
|
||||
} else {
|
||||
utflen += 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (utflen > 65535)
|
||||
throw new UTFDataFormatException("encoded string too long: "
|
||||
+ utflen + " bytes");
|
||||
|
||||
byte[] bytearr = null;
|
||||
if (out instanceof LittleEndianDataOutputStream) {
|
||||
LittleEndianDataOutputStream dos = (LittleEndianDataOutputStream) out;
|
||||
if (dos.bytearr == null || (dos.bytearr.length < (utflen + 2)))
|
||||
dos.bytearr = new byte[(utflen * 2) + 2];
|
||||
bytearr = dos.bytearr;
|
||||
} else {
|
||||
bytearr = new byte[utflen + 2];
|
||||
}
|
||||
|
||||
bytearr[count++] = (byte) ((utflen >>> 8) & 0xFF);
|
||||
bytearr[count++] = (byte) ((utflen >>> 0) & 0xFF);
|
||||
|
||||
int i = 0;
|
||||
for (i = 0; i < strlen; i++) {
|
||||
c = str.charAt(i);
|
||||
if (!((c >= 0x0001) && (c <= 0x007F)))
|
||||
break;
|
||||
bytearr[count++] = (byte) c;
|
||||
}
|
||||
|
||||
for (; i < strlen; i++) {
|
||||
c = str.charAt(i);
|
||||
if ((c >= 0x0001) && (c <= 0x007F)) {
|
||||
bytearr[count++] = (byte) c;
|
||||
|
||||
} else if (c > 0x07FF) {
|
||||
bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
|
||||
bytearr[count++] = (byte) (0x80 | ((c >> 6) & 0x3F));
|
||||
bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
|
||||
} else {
|
||||
bytearr[count++] = (byte) (0xC0 | ((c >> 6) & 0x1F));
|
||||
bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
|
||||
}
|
||||
}
|
||||
out.write(bytearr, 0, utflen + 2);
|
||||
return utflen + 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current value of the counter <code>written</code>, the number
|
||||
* of bytes written to this data output stream so far. If the counter
|
||||
* overflows, it will be wrapped to Integer.MAX_VALUE.
|
||||
*
|
||||
* @return the value of the <code>written</code> field.
|
||||
* @see java.io.DataOutputStream#written
|
||||
*/
|
||||
public final int size() {
|
||||
return written;
|
||||
}
|
||||
}
|
@ -18,154 +18,33 @@ package com.m2049r.xmrwallet.adapter;
|
||||
|
||||
import static com.m2049r.xmrwallet.util.DateHelper.DATETIME_FORMATTER;
|
||||
|
||||
import android.content.Context;
|
||||
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 androidx.core.content.ContextCompat;
|
||||
import androidx.leanback.widget.DiffCallback;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.Crypto;
|
||||
import com.m2049r.xmrwallet.data.UserNotes;
|
||||
import com.m2049r.xmrwallet.model.TransactionInfo;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfoAdapter.ViewHolder> {
|
||||
|
||||
private List<TransactionInfo> localDataSet;
|
||||
private TxInfoAdapterListener listener = null;
|
||||
|
||||
/**
|
||||
* Provide a reference to the type of views that you are using
|
||||
* (custom ViewHolder).
|
||||
*/
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
private final int outboundColour;
|
||||
private final int inboundColour;
|
||||
private final int pendingColour;
|
||||
private final int failedColour;
|
||||
private TxInfoAdapterListener listener = null;
|
||||
private TextView amountTextView = null;
|
||||
public ViewHolder(TxInfoAdapterListener listener, View view) {
|
||||
super(view);
|
||||
inboundColour = ThemeHelper.getThemedColor(view.getContext(), R.attr.positiveColor);
|
||||
outboundColour = ThemeHelper.getThemedColor(view.getContext(), R.attr.negativeColor);
|
||||
pendingColour = ThemeHelper.getThemedColor(view.getContext(), R.attr.neutralColor);
|
||||
failedColour = ThemeHelper.getThemedColor(view.getContext(), R.attr.neutralColor);
|
||||
this.listener = listener;
|
||||
Calendar cal = Calendar.getInstance();
|
||||
TimeZone tz = cal.getTimeZone(); //get the local time zone.
|
||||
DATETIME_FORMATTER.setTimeZone(tz);
|
||||
}
|
||||
|
||||
public void bind(TransactionInfo txInfo) {
|
||||
String displayAmount = Helper.getDisplayAmount(txInfo.amount, Helper.DISPLAY_DIGITS_INFO);
|
||||
|
||||
TextView confirmationsTextView = ((TextView)itemView.findViewById(R.id.tvConfirmations));
|
||||
CircularProgressIndicator confirmationsProgressBar = ((CircularProgressIndicator)itemView.findViewById(R.id.pbConfirmations));
|
||||
confirmationsProgressBar.setMax(TransactionInfo.CONFIRMATION);
|
||||
this.amountTextView = ((TextView)itemView.findViewById(R.id.tx_amount));
|
||||
((TextView)itemView.findViewById(R.id.tx_failed)).setVisibility(View.GONE);
|
||||
if(txInfo.isFailed) {
|
||||
((TextView)itemView.findViewById(R.id.tx_amount)).setText(itemView.getContext().getString(R.string.tx_list_amount_negative, displayAmount));
|
||||
((TextView)itemView.findViewById(R.id.tx_failed)).setVisibility(View.VISIBLE);
|
||||
setTxColour(failedColour);
|
||||
confirmationsTextView.setVisibility(View.GONE);
|
||||
confirmationsProgressBar.setVisibility(View.GONE);
|
||||
} else if(txInfo.isPending) {
|
||||
setTxColour(pendingColour);
|
||||
confirmationsProgressBar.setVisibility(View.GONE);
|
||||
confirmationsProgressBar.setIndeterminate(true);
|
||||
confirmationsProgressBar.setVisibility(View.VISIBLE);
|
||||
confirmationsTextView.setVisibility(View.GONE);
|
||||
} else if (txInfo.direction == TransactionInfo.Direction.Direction_In) {
|
||||
setTxColour(inboundColour);
|
||||
if (!txInfo.isConfirmed()) {
|
||||
confirmationsProgressBar.setVisibility(View.VISIBLE);
|
||||
final int confirmations = (int) txInfo.confirmations;
|
||||
confirmationsProgressBar.setProgressCompat(confirmations, true);
|
||||
final String confCount = Integer.toString(confirmations);
|
||||
confirmationsTextView.setText(confCount);
|
||||
if (confCount.length() == 1) // we only have space for character in the progress circle
|
||||
confirmationsTextView.setVisibility(View.VISIBLE);
|
||||
else
|
||||
confirmationsTextView.setVisibility(View.GONE);
|
||||
} else {
|
||||
confirmationsProgressBar.setVisibility(View.GONE);
|
||||
confirmationsTextView.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
setTxColour(outboundColour);
|
||||
confirmationsProgressBar.setVisibility(View.GONE);
|
||||
confirmationsTextView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (txInfo.direction == TransactionInfo.Direction.Direction_Out) {
|
||||
((TextView)itemView.findViewById(R.id.tx_amount)).setText(itemView.getContext().getString(R.string.tx_list_amount_negative, displayAmount));
|
||||
} else {
|
||||
((TextView)itemView.findViewById(R.id.tx_amount)).setText(itemView.getContext().getString(R.string.tx_list_amount_positive, displayAmount));
|
||||
}
|
||||
|
||||
TextView paymentIdTextView = ((TextView)itemView.findViewById(R.id.tx_paymentid));
|
||||
String tag = null;
|
||||
String info = "";
|
||||
UserNotes userNotes = new UserNotes(txInfo.notes);
|
||||
if ((txInfo.addressIndex != 0) && (txInfo.direction == TransactionInfo.Direction.Direction_In))
|
||||
tag = txInfo.getDisplayLabel();
|
||||
if ((userNotes.note.isEmpty())) {
|
||||
if (!txInfo.paymentId.equals("0000000000000000")) {
|
||||
info = txInfo.paymentId;
|
||||
}
|
||||
} else {
|
||||
info = userNotes.note;
|
||||
}
|
||||
if (tag == null) {
|
||||
paymentIdTextView.setText(info);
|
||||
} else {
|
||||
Spanned label = Html.fromHtml(itemView.getContext().getString(R.string.tx_details_notes,
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(itemView.getContext(), R.attr.positiveColor) & 0xFFFFFF),
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(itemView.getContext(), android.R.attr.colorBackground) & 0xFFFFFF),
|
||||
tag, info.isEmpty() ? "" : (" " + info)));
|
||||
paymentIdTextView.setText(label);
|
||||
}
|
||||
((TextView)itemView.findViewById(R.id.tx_datetime)).setText(getDateTime(txInfo.timestamp));
|
||||
itemView.setOnClickListener(view -> {
|
||||
listener.onClickTransaction(txInfo);
|
||||
});
|
||||
}
|
||||
|
||||
private void setTxColour(int clr) {
|
||||
amountTextView.setTextColor(clr);
|
||||
}
|
||||
|
||||
private String getDateTime(long time) {
|
||||
return DATETIME_FORMATTER.format(new Date(time * 1000));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the dataset of the Adapter.
|
||||
*/
|
||||
@ -205,5 +84,114 @@ public class TransactionInfoAdapter extends RecyclerView.Adapter<TransactionInfo
|
||||
public interface TxInfoAdapterListener {
|
||||
void onClickTransaction(TransactionInfo txInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a reference to the type of views that you are using
|
||||
* (custom ViewHolder).
|
||||
*/
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
private final int outboundColour;
|
||||
private final int inboundColour;
|
||||
private final int pendingColour;
|
||||
private final int failedColour;
|
||||
private TxInfoAdapterListener listener = null;
|
||||
private TextView amountTextView = null;
|
||||
|
||||
public ViewHolder(TxInfoAdapterListener listener, View view) {
|
||||
super(view);
|
||||
inboundColour = ThemeHelper.getThemedColor(view.getContext(), R.attr.positiveColor);
|
||||
outboundColour = ThemeHelper.getThemedColor(view.getContext(), R.attr.negativeColor);
|
||||
pendingColour = ThemeHelper.getThemedColor(view.getContext(), R.attr.neutralColor);
|
||||
failedColour = ThemeHelper.getThemedColor(view.getContext(), R.attr.neutralColor);
|
||||
this.listener = listener;
|
||||
Calendar cal = Calendar.getInstance();
|
||||
TimeZone tz = cal.getTimeZone(); //get the local time zone.
|
||||
DATETIME_FORMATTER.setTimeZone(tz);
|
||||
}
|
||||
|
||||
public void bind(TransactionInfo txInfo) {
|
||||
String displayAmount = Helper.getDisplayAmount(txInfo.amount, Helper.DISPLAY_DIGITS_INFO);
|
||||
|
||||
TextView confirmationsTextView = ((TextView) itemView.findViewById(R.id.tvConfirmations));
|
||||
CircularProgressIndicator confirmationsProgressBar = ((CircularProgressIndicator) itemView.findViewById(R.id.pbConfirmations));
|
||||
confirmationsProgressBar.setMax(TransactionInfo.CONFIRMATION);
|
||||
this.amountTextView = ((TextView) itemView.findViewById(R.id.tx_amount));
|
||||
((TextView) itemView.findViewById(R.id.tx_failed)).setVisibility(View.GONE);
|
||||
if (txInfo.isFailed) {
|
||||
((TextView) itemView.findViewById(R.id.tx_amount)).setText(itemView.getContext().getString(R.string.tx_list_amount_negative, displayAmount));
|
||||
((TextView) itemView.findViewById(R.id.tx_failed)).setVisibility(View.VISIBLE);
|
||||
setTxColour(failedColour);
|
||||
confirmationsTextView.setVisibility(View.GONE);
|
||||
confirmationsProgressBar.setVisibility(View.GONE);
|
||||
} else if (txInfo.isPending) {
|
||||
setTxColour(pendingColour);
|
||||
confirmationsProgressBar.setVisibility(View.GONE);
|
||||
confirmationsProgressBar.setIndeterminate(true);
|
||||
confirmationsProgressBar.setVisibility(View.VISIBLE);
|
||||
confirmationsTextView.setVisibility(View.GONE);
|
||||
} else if (txInfo.direction == TransactionInfo.Direction.Direction_In) {
|
||||
setTxColour(inboundColour);
|
||||
if (!txInfo.isConfirmed()) {
|
||||
confirmationsProgressBar.setVisibility(View.VISIBLE);
|
||||
final int confirmations = (int) txInfo.confirmations;
|
||||
confirmationsProgressBar.setProgressCompat(confirmations, true);
|
||||
final String confCount = Integer.toString(confirmations);
|
||||
confirmationsTextView.setText(confCount);
|
||||
if (confCount.length() == 1) // we only have space for character in the progress circle
|
||||
confirmationsTextView.setVisibility(View.VISIBLE);
|
||||
else
|
||||
confirmationsTextView.setVisibility(View.GONE);
|
||||
} else {
|
||||
confirmationsProgressBar.setVisibility(View.GONE);
|
||||
confirmationsTextView.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
setTxColour(outboundColour);
|
||||
confirmationsProgressBar.setVisibility(View.GONE);
|
||||
confirmationsTextView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (txInfo.direction == TransactionInfo.Direction.Direction_Out) {
|
||||
((TextView) itemView.findViewById(R.id.tx_amount)).setText(itemView.getContext().getString(R.string.tx_list_amount_negative, displayAmount));
|
||||
} else {
|
||||
((TextView) itemView.findViewById(R.id.tx_amount)).setText(itemView.getContext().getString(R.string.tx_list_amount_positive, displayAmount));
|
||||
}
|
||||
|
||||
TextView paymentIdTextView = ((TextView) itemView.findViewById(R.id.tx_paymentid));
|
||||
String tag = null;
|
||||
String info = "";
|
||||
UserNotes userNotes = new UserNotes(txInfo.notes);
|
||||
if ((txInfo.addressIndex != 0) && (txInfo.direction == TransactionInfo.Direction.Direction_In))
|
||||
tag = txInfo.getDisplayLabel();
|
||||
if ((userNotes.note.isEmpty())) {
|
||||
if (!txInfo.paymentId.equals("0000000000000000")) {
|
||||
info = txInfo.paymentId;
|
||||
}
|
||||
} else {
|
||||
info = userNotes.note;
|
||||
}
|
||||
if (tag == null) {
|
||||
paymentIdTextView.setText(info);
|
||||
} else {
|
||||
Spanned label = Html.fromHtml(itemView.getContext().getString(R.string.tx_details_notes,
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(itemView.getContext(), R.attr.positiveColor) & 0xFFFFFF),
|
||||
Integer.toHexString(ThemeHelper.getThemedColor(itemView.getContext(), android.R.attr.colorBackground) & 0xFFFFFF),
|
||||
tag, info.isEmpty() ? "" : (" " + info)));
|
||||
paymentIdTextView.setText(label);
|
||||
}
|
||||
((TextView) itemView.findViewById(R.id.tx_datetime)).setText(getDateTime(txInfo.timestamp));
|
||||
itemView.setOnClickListener(view -> {
|
||||
listener.onClickTransaction(txInfo);
|
||||
});
|
||||
}
|
||||
|
||||
private void setTxColour(int clr) {
|
||||
amountTextView.setTextColor(clr);
|
||||
}
|
||||
|
||||
private String getDateTime(long time) {
|
||||
return DATETIME_FORMATTER.format(new Date(time * 1000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,232 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 m2049r
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.m2049r.xmrwallet.data;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import com.m2049r.xmrwallet.util.OpenAliasHelper;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.ToString;
|
||||
import timber.log.Timber;
|
||||
|
||||
@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;
|
||||
|
||||
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;
|
||||
this.address = address;
|
||||
if (assets.size() == 1) {
|
||||
this.asset = assets.get(0);
|
||||
this.ambiguousAssets = null;
|
||||
} else {
|
||||
this.asset = null;
|
||||
this.ambiguousAssets = assets;
|
||||
}
|
||||
}
|
||||
|
||||
public BarcodeData(Crypto asset, String address, String description, String amount) {
|
||||
this(asset, address, null, description, amount, Security.NORMAL);
|
||||
}
|
||||
|
||||
public BarcodeData(Crypto asset, String address, String addressName, String description, String amount, Security security) {
|
||||
this.ambiguousAssets = null;
|
||||
this.asset = asset;
|
||||
this.address = address;
|
||||
this.addressName = addressName;
|
||||
this.description = description;
|
||||
this.amount = amount;
|
||||
this.security = security;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return Uri.parse(getUriString());
|
||||
}
|
||||
|
||||
public String getUriString() {
|
||||
if (asset != Crypto.XMR) throw new IllegalStateException("We can only do XMR stuff!");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(Crypto.XMR.getUriScheme())
|
||||
.append(':')
|
||||
.append(address);
|
||||
boolean first = true;
|
||||
if ((description != null) && !description.isEmpty()) {
|
||||
sb.append(first ? "?" : "&");
|
||||
first = false;
|
||||
sb.append(Crypto.XMR.getUriMessage()).append('=').append(Uri.encode(description));
|
||||
}
|
||||
if ((amount != null) && !amount.isEmpty()) {
|
||||
sb.append(first ? "?" : "&");
|
||||
sb.append(Crypto.XMR.getUriAmount()).append('=').append(amount);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
static private BarcodeData parseNaked(String address) {
|
||||
List<Crypto> possibleAssets = new ArrayList<>();
|
||||
for (Crypto crypto : Crypto.values()) {
|
||||
if (crypto.validate(address)) {
|
||||
possibleAssets.add(crypto);
|
||||
}
|
||||
}
|
||||
if (possibleAssets.isEmpty())
|
||||
return null;
|
||||
return new BarcodeData(possibleAssets, address);
|
||||
}
|
||||
|
||||
static public BarcodeData parseUri(String uriString) {
|
||||
Timber.d("parseBitUri=%s", uriString);
|
||||
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(uriString);
|
||||
} catch (URISyntaxException ex) {
|
||||
return null;
|
||||
}
|
||||
if (!uri.isOpaque()) return null;
|
||||
final String scheme = uri.getScheme();
|
||||
Crypto crypto = Crypto.withScheme(scheme);
|
||||
if (crypto == null) return null;
|
||||
|
||||
String[] parts = uri.getRawSchemeSpecificPart().split("[?]");
|
||||
if ((parts.length <= 0) || (parts.length > 2)) {
|
||||
Timber.d("invalid number of parts %d", parts.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, String> parms = new HashMap<>();
|
||||
if (parts.length == 2) {
|
||||
String[] args = parts[1].split("&");
|
||||
for (String arg : args) {
|
||||
String[] namevalue = arg.split("=");
|
||||
if (namevalue.length == 0) {
|
||||
continue;
|
||||
}
|
||||
parms.put(Uri.decode(namevalue[0]).toLowerCase(),
|
||||
namevalue.length > 1 ? Uri.decode(namevalue[1]) : "");
|
||||
}
|
||||
}
|
||||
|
||||
String addressName = parms.get(crypto.getUriLabel());
|
||||
String description = parms.get(crypto.getUriMessage());
|
||||
String address = parts[0]; // no need to decode as there can be no special characters
|
||||
if (address.isEmpty()) {
|
||||
Timber.d("no address");
|
||||
return null;
|
||||
}
|
||||
if (!crypto.validate(address)) {
|
||||
Timber.d("%s address (%s) invalid", crypto, address);
|
||||
return null;
|
||||
}
|
||||
String amount = parms.get(crypto.getUriAmount());
|
||||
if ((amount != null) && (!amount.isEmpty())) {
|
||||
try {
|
||||
Double.parseDouble(amount);
|
||||
} catch (NumberFormatException ex) {
|
||||
Timber.d(ex.getLocalizedMessage());
|
||||
return null; // we have an amount but its not a number!
|
||||
}
|
||||
}
|
||||
return new BarcodeData(crypto, address, addressName, description, amount, Security.NORMAL);
|
||||
}
|
||||
|
||||
|
||||
static public BarcodeData fromString(String qrCode) {
|
||||
BarcodeData bcData = parseUri(qrCode);
|
||||
if (bcData == null) {
|
||||
// maybe it's naked?
|
||||
bcData = parseNaked(qrCode);
|
||||
}
|
||||
if (bcData == null) {
|
||||
// check for OpenAlias
|
||||
bcData = parseOpenAlias(qrCode, false);
|
||||
}
|
||||
return bcData;
|
||||
}
|
||||
|
||||
static public BarcodeData parseOpenAlias(String oaString, boolean dnssec) {
|
||||
Timber.d("parseOpenAlias=%s", oaString);
|
||||
if (oaString == null) return null;
|
||||
|
||||
Map<String, String> oaAttrs = OpenAliasHelper.parse(oaString);
|
||||
if (oaAttrs == null) return null;
|
||||
|
||||
String oaAsset = oaAttrs.get(OpenAliasHelper.OA1_ASSET);
|
||||
if (oaAsset == null) return null;
|
||||
|
||||
String address = oaAttrs.get(OpenAliasHelper.OA1_ADDRESS);
|
||||
if (address == null) return null;
|
||||
|
||||
Crypto crypto = Crypto.withSymbol(oaAsset);
|
||||
if (crypto == null) {
|
||||
Timber.i("Unsupported OpenAlias asset %s", oaAsset);
|
||||
return null;
|
||||
}
|
||||
if (!crypto.validate(address)) {
|
||||
Timber.d("%s address invalid", crypto);
|
||||
return null;
|
||||
}
|
||||
|
||||
String description = oaAttrs.get(OpenAliasHelper.OA1_DESCRIPTION);
|
||||
if (description == null) {
|
||||
description = oaAttrs.get(OpenAliasHelper.OA1_NAME);
|
||||
}
|
||||
String amount = oaAttrs.get(OpenAliasHelper.OA1_AMOUNT);
|
||||
String addressName = oaAttrs.get(OpenAliasHelper.OA1_NAME);
|
||||
|
||||
if (amount != null) {
|
||||
try {
|
||||
Double.parseDouble(amount);
|
||||
} catch (NumberFormatException ex) {
|
||||
Timber.d(ex.getLocalizedMessage());
|
||||
return null; // we have an amount but its not a number!
|
||||
}
|
||||
}
|
||||
|
||||
Security sec = dnssec ? BarcodeData.Security.OA_DNSSEC : BarcodeData.Security.OA_NO_DNSSEC;
|
||||
|
||||
return new BarcodeData(crypto, address, addressName, description, amount, sec);
|
||||
}
|
||||
|
||||
public boolean isAmbiguous() {
|
||||
return ambiguousAssets != null;
|
||||
}
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
package com.m2049r.xmrwallet.data;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
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 lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public enum Crypto {
|
||||
XMR("XMR", true, "monero:tx_amount:recipient_name:tx_description", 0, R.drawable.ic_monero, R.drawable.ic_monero_bw, Wallet::isAddressValid),
|
||||
BTC("BTC", true, "bitcoin:amount:label:message", 0, 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", 0, 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", 0, 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", 0, R.drawable.ic_xmrto_eth, R.drawable.ic_xmrto_eth_off, EthAddressValidator::validate),
|
||||
LTC("LTC", true, "litecoin:amount:label:message", 0, R.drawable.ic_xmrto_ltc, R.drawable.ic_xmrto_ltc_off, address -> {
|
||||
return BitcoinAddressValidator.validate(address, BitcoinAddressType.LTC);
|
||||
});
|
||||
|
||||
@Getter
|
||||
@NonNull
|
||||
private final String symbol;
|
||||
@Getter
|
||||
private final boolean casefull;
|
||||
@NonNull
|
||||
private final String uriSpec;
|
||||
@Getter
|
||||
private final int buttonId;
|
||||
@Getter
|
||||
private final int iconEnabledId;
|
||||
@Getter
|
||||
private final int iconDisabledId;
|
||||
@NonNull
|
||||
private final Validator validator;
|
||||
|
||||
@Nullable
|
||||
public static Crypto withScheme(@NonNull String scheme) {
|
||||
for (Crypto crypto : values()) {
|
||||
if (crypto.getUriScheme().equals(scheme)) return crypto;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Crypto withSymbol(@NonNull String symbol) {
|
||||
final String upperSymbol = symbol.toUpperCase();
|
||||
for (Crypto crypto : values()) {
|
||||
if (crypto.symbol.equals(upperSymbol)) return crypto;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface Validator {
|
||||
boolean validate(String address);
|
||||
}
|
||||
|
||||
// TODO maybe cache these segments
|
||||
String getUriScheme() {
|
||||
return uriSpec.split(":")[0];
|
||||
}
|
||||
|
||||
String getUriAmount() {
|
||||
return uriSpec.split(":")[1];
|
||||
}
|
||||
|
||||
String getUriLabel() {
|
||||
return uriSpec.split(":")[2];
|
||||
}
|
||||
|
||||
String getUriMessage() {
|
||||
return uriSpec.split(":")[3];
|
||||
}
|
||||
|
||||
boolean validate(String address) {
|
||||
return validator.validate(address);
|
||||
}
|
||||
}
|
@ -35,69 +35,21 @@ public class Node {
|
||||
static public final String MAINNET = "mainnet";
|
||||
static public final String STAGENET = "stagenet";
|
||||
static public final String TESTNET = "testnet";
|
||||
|
||||
static class Address {
|
||||
final private InetAddress inet;
|
||||
final private String onion;
|
||||
|
||||
public boolean isOnion() {
|
||||
return onion != null;
|
||||
}
|
||||
|
||||
public String getHostName() {
|
||||
if (inet != null) {
|
||||
return inet.getHostName();
|
||||
} else {
|
||||
return onion;
|
||||
}
|
||||
}
|
||||
|
||||
public String getHostAddress() {
|
||||
if (inet != null) {
|
||||
return inet.getHostAddress();
|
||||
} else {
|
||||
return onion;
|
||||
}
|
||||
}
|
||||
|
||||
private Address(InetAddress address, String onion) {
|
||||
this.inet = address;
|
||||
this.onion = onion;
|
||||
}
|
||||
|
||||
static Address of(InetAddress address) {
|
||||
return new Address(address, null);
|
||||
}
|
||||
|
||||
static Address of(String host) throws UnknownHostException {
|
||||
if (OnionHelper.isOnionHost(host)) {
|
||||
return new Address(null, host);
|
||||
} else {
|
||||
return new Address(InetAddress.getByName(host), null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getHostAddress().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return (other instanceof Address) && (getHostAddress().equals(((Address) other).getHostAddress()));
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
private String name = null;
|
||||
static private int DEFAULT_LEVIN_PORT = 0;
|
||||
static private int DEFAULT_RPC_PORT = 0;
|
||||
@Getter
|
||||
final private NetworkType networkType;
|
||||
Address hostAddress;
|
||||
@Getter
|
||||
private String host;
|
||||
@Setter
|
||||
private final boolean selected = false;
|
||||
Address hostAddress;
|
||||
@Getter
|
||||
@Setter
|
||||
int rpcPort = 0;
|
||||
@Getter
|
||||
private String name = null;
|
||||
@Getter
|
||||
private String host;
|
||||
private int levinPort = 0;
|
||||
@Getter
|
||||
@Setter
|
||||
@ -108,37 +60,6 @@ public class Node {
|
||||
@Getter
|
||||
@Setter
|
||||
private boolean favourite = false;
|
||||
@Getter
|
||||
@Setter
|
||||
private final boolean selected = false;
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hostAddress.hashCode();
|
||||
}
|
||||
|
||||
// Nodes are equal if they are the same host address:port & are on the same network
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (!(other instanceof Node)) return false;
|
||||
final Node anotherNode = (Node) other;
|
||||
return (hostAddress.equals(anotherNode.hostAddress)
|
||||
&& (rpcPort == anotherNode.rpcPort)
|
||||
&& (networkType == anotherNode.networkType));
|
||||
}
|
||||
|
||||
public boolean isOnion() {
|
||||
return hostAddress.isOnion();
|
||||
}
|
||||
|
||||
static public Node fromString(String nodeString) {
|
||||
try {
|
||||
return new Node(nodeString);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Timber.w(ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Node(String nodeString) {
|
||||
if ((nodeString == null) || nodeString.isEmpty())
|
||||
@ -223,6 +144,92 @@ public class Node {
|
||||
this.levinPort = getDefaultLevinPort();
|
||||
}
|
||||
|
||||
public Node() {
|
||||
this.networkType = WalletManager.getInstance().getNetworkType();
|
||||
}
|
||||
|
||||
// constructor used for created nodes from retrieved peer lists
|
||||
public Node(InetSocketAddress socketAddress) {
|
||||
this();
|
||||
this.hostAddress = Address.of(socketAddress.getAddress());
|
||||
this.host = socketAddress.getHostString();
|
||||
this.rpcPort = 0; // unknown
|
||||
this.levinPort = socketAddress.getPort();
|
||||
this.username = "";
|
||||
this.password = "";
|
||||
}
|
||||
|
||||
public Node(Node anotherNode) {
|
||||
networkType = anotherNode.networkType;
|
||||
overwriteWith(anotherNode);
|
||||
}
|
||||
|
||||
static public Node fromString(String nodeString) {
|
||||
try {
|
||||
return new Node(nodeString);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Timber.w(ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// every node knows its network, but they are all the same
|
||||
static public int getDefaultLevinPort() {
|
||||
if (DEFAULT_LEVIN_PORT > 0) return DEFAULT_LEVIN_PORT;
|
||||
switch (WalletManager.getInstance().getNetworkType()) {
|
||||
case NetworkType_Mainnet:
|
||||
DEFAULT_LEVIN_PORT = 18080;
|
||||
break;
|
||||
case NetworkType_Testnet:
|
||||
DEFAULT_LEVIN_PORT = 28080;
|
||||
break;
|
||||
case NetworkType_Stagenet:
|
||||
DEFAULT_LEVIN_PORT = 38080;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("unsupported net " + WalletManager.getInstance().getNetworkType());
|
||||
}
|
||||
return DEFAULT_LEVIN_PORT;
|
||||
}
|
||||
|
||||
// every node knows its network, but they are all the same
|
||||
static public int getDefaultRpcPort() {
|
||||
if (DEFAULT_RPC_PORT > 0) return DEFAULT_RPC_PORT;
|
||||
switch (WalletManager.getInstance().getNetworkType()) {
|
||||
case NetworkType_Mainnet:
|
||||
DEFAULT_RPC_PORT = 18081;
|
||||
break;
|
||||
case NetworkType_Testnet:
|
||||
DEFAULT_RPC_PORT = 28081;
|
||||
break;
|
||||
case NetworkType_Stagenet:
|
||||
DEFAULT_RPC_PORT = 38081;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("unsupported net " + WalletManager.getInstance().getNetworkType());
|
||||
}
|
||||
return DEFAULT_RPC_PORT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hostAddress.hashCode();
|
||||
}
|
||||
|
||||
// Nodes are equal if they are the same host address:port & are on the same network
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (!(other instanceof Node)) return false;
|
||||
final Node anotherNode = (Node) other;
|
||||
return (hostAddress.equals(anotherNode.hostAddress)
|
||||
&& (rpcPort == anotherNode.rpcPort)
|
||||
&& (networkType == anotherNode.networkType));
|
||||
}
|
||||
|
||||
public boolean isOnion() {
|
||||
return hostAddress.isOnion();
|
||||
}
|
||||
|
||||
public String toNodeString() {
|
||||
return toString();
|
||||
}
|
||||
@ -255,21 +262,6 @@ public class Node {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public Node() {
|
||||
this.networkType = WalletManager.getInstance().getNetworkType();
|
||||
}
|
||||
|
||||
// constructor used for created nodes from retrieved peer lists
|
||||
public Node(InetSocketAddress socketAddress) {
|
||||
this();
|
||||
this.hostAddress = Address.of(socketAddress.getAddress());
|
||||
this.host = socketAddress.getHostString();
|
||||
this.rpcPort = 0; // unknown
|
||||
this.levinPort = socketAddress.getPort();
|
||||
this.username = "";
|
||||
this.password = "";
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return getHostAddress() + ":" + rpcPort;
|
||||
}
|
||||
@ -309,11 +301,6 @@ public class Node {
|
||||
favourite = !favourite;
|
||||
}
|
||||
|
||||
public Node(Node anotherNode) {
|
||||
networkType = anotherNode.networkType;
|
||||
overwriteWith(anotherNode);
|
||||
}
|
||||
|
||||
public void overwriteWith(Node anotherNode) {
|
||||
if (networkType != anotherNode.networkType)
|
||||
throw new IllegalStateException("network types do not match");
|
||||
@ -327,45 +314,55 @@ public class Node {
|
||||
favourite = anotherNode.favourite;
|
||||
}
|
||||
|
||||
static private int DEFAULT_LEVIN_PORT = 0;
|
||||
static class Address {
|
||||
final private InetAddress inet;
|
||||
final private String onion;
|
||||
|
||||
// every node knows its network, but they are all the same
|
||||
static public int getDefaultLevinPort() {
|
||||
if (DEFAULT_LEVIN_PORT > 0) return DEFAULT_LEVIN_PORT;
|
||||
switch (WalletManager.getInstance().getNetworkType()) {
|
||||
case NetworkType_Mainnet:
|
||||
DEFAULT_LEVIN_PORT = 18080;
|
||||
break;
|
||||
case NetworkType_Testnet:
|
||||
DEFAULT_LEVIN_PORT = 28080;
|
||||
break;
|
||||
case NetworkType_Stagenet:
|
||||
DEFAULT_LEVIN_PORT = 38080;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("unsupported net " + WalletManager.getInstance().getNetworkType());
|
||||
}
|
||||
return DEFAULT_LEVIN_PORT;
|
||||
private Address(InetAddress address, String onion) {
|
||||
this.inet = address;
|
||||
this.onion = onion;
|
||||
}
|
||||
|
||||
static private int DEFAULT_RPC_PORT = 0;
|
||||
|
||||
// every node knows its network, but they are all the same
|
||||
static public int getDefaultRpcPort() {
|
||||
if (DEFAULT_RPC_PORT > 0) return DEFAULT_RPC_PORT;
|
||||
switch (WalletManager.getInstance().getNetworkType()) {
|
||||
case NetworkType_Mainnet:
|
||||
DEFAULT_RPC_PORT = 18081;
|
||||
break;
|
||||
case NetworkType_Testnet:
|
||||
DEFAULT_RPC_PORT = 28081;
|
||||
break;
|
||||
case NetworkType_Stagenet:
|
||||
DEFAULT_RPC_PORT = 38081;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("unsupported net " + WalletManager.getInstance().getNetworkType());
|
||||
static Address of(InetAddress address) {
|
||||
return new Address(address, null);
|
||||
}
|
||||
|
||||
static Address of(String host) throws UnknownHostException {
|
||||
if (OnionHelper.isOnionHost(host)) {
|
||||
return new Address(null, host);
|
||||
} else {
|
||||
return new Address(InetAddress.getByName(host), null);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isOnion() {
|
||||
return onion != null;
|
||||
}
|
||||
|
||||
public String getHostName() {
|
||||
if (inet != null) {
|
||||
return inet.getHostName();
|
||||
} else {
|
||||
return onion;
|
||||
}
|
||||
}
|
||||
|
||||
public String getHostAddress() {
|
||||
if (inet != null) {
|
||||
return inet.getHostAddress();
|
||||
} else {
|
||||
return onion;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getHostAddress().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return (other instanceof Address) && (getHostAddress().equals(((Address) other).getHostAddress()));
|
||||
}
|
||||
return DEFAULT_RPC_PORT;
|
||||
}
|
||||
}
|
||||
|
@ -1,301 +0,0 @@
|
||||
/*
|
||||
* 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.content.Context;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.m2049r.levin.scanner.LevinPeer;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper.Request;
|
||||
import com.m2049r.xmrwallet.util.NodePinger;
|
||||
import com.m2049r.xmrwallet.util.ThemeHelper;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.Calendar;
|
||||
import java.util.Comparator;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class NodeInfo extends Node {
|
||||
final static public int MIN_MAJOR_VERSION = 14;
|
||||
final static public String RPC_VERSION = "2.0";
|
||||
|
||||
@Getter
|
||||
private long height = 0;
|
||||
@Getter
|
||||
private long timestamp = 0;
|
||||
@Getter
|
||||
private int majorVersion = 0;
|
||||
@Getter
|
||||
private double responseTime = Double.MAX_VALUE;
|
||||
@Getter
|
||||
private int responseCode = 0;
|
||||
@Getter
|
||||
private boolean tested = false;
|
||||
@Getter
|
||||
@Setter
|
||||
private final boolean selecting = false;
|
||||
|
||||
public void clear() {
|
||||
height = 0;
|
||||
majorVersion = 0;
|
||||
responseTime = Double.MAX_VALUE;
|
||||
responseCode = 0;
|
||||
timestamp = 0;
|
||||
tested = false;
|
||||
}
|
||||
|
||||
static public NodeInfo fromString(String nodeString) {
|
||||
try {
|
||||
return new NodeInfo(nodeString);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public NodeInfo(NodeInfo anotherNode) {
|
||||
super(anotherNode);
|
||||
overwriteWith(anotherNode);
|
||||
}
|
||||
|
||||
private SocketAddress levinSocketAddress = null;
|
||||
|
||||
synchronized public SocketAddress getLevinSocketAddress() {
|
||||
if (levinSocketAddress == null) {
|
||||
// use default peer port if not set - very few peers use nonstandard port
|
||||
levinSocketAddress = new InetSocketAddress(hostAddress.getHostAddress(), getDefaultLevinPort());
|
||||
}
|
||||
return levinSocketAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return super.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return super.equals(other);
|
||||
}
|
||||
|
||||
public NodeInfo(String nodeString) {
|
||||
super(nodeString);
|
||||
}
|
||||
|
||||
public NodeInfo(LevinPeer levinPeer) {
|
||||
super(levinPeer.getSocketAddress());
|
||||
}
|
||||
|
||||
public NodeInfo(InetSocketAddress address) {
|
||||
super(address);
|
||||
}
|
||||
|
||||
public NodeInfo() {
|
||||
super();
|
||||
}
|
||||
|
||||
public boolean isSuccessful() {
|
||||
return (responseCode >= 200) && (responseCode < 300);
|
||||
}
|
||||
|
||||
public boolean isUnauthorized() {
|
||||
return responseCode == HttpURLConnection.HTTP_UNAUTHORIZED;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return isSuccessful() && (majorVersion >= MIN_MAJOR_VERSION) && (responseTime < Double.MAX_VALUE);
|
||||
}
|
||||
|
||||
static public Comparator<NodeInfo> BestNodeComparator = (o1, o2) -> {
|
||||
if (o1.isValid()) {
|
||||
if (o2.isValid()) { // both are valid
|
||||
// higher node wins
|
||||
int heightDiff = (int) (o2.height - o1.height);
|
||||
if (heightDiff != 0)
|
||||
return heightDiff;
|
||||
// if they are equal, faster node wins
|
||||
return (int) Math.signum(o1.responseTime - o2.responseTime);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
public void overwriteWith(NodeInfo anotherNode) {
|
||||
super.overwriteWith(anotherNode);
|
||||
height = anotherNode.height;
|
||||
timestamp = anotherNode.timestamp;
|
||||
majorVersion = anotherNode.majorVersion;
|
||||
responseTime = anotherNode.responseTime;
|
||||
responseCode = anotherNode.responseCode;
|
||||
}
|
||||
|
||||
public String toNodeString() {
|
||||
return super.toString();
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(super.toString());
|
||||
sb.append("?rc=").append(responseCode);
|
||||
sb.append("?v=").append(majorVersion);
|
||||
sb.append("&h=").append(height);
|
||||
sb.append("&ts=").append(timestamp);
|
||||
if (responseTime < Double.MAX_VALUE) {
|
||||
sb.append("&t=").append(responseTime).append("ms");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static final int HTTP_TIMEOUT = 1000; //ms
|
||||
public static final double PING_GOOD = HTTP_TIMEOUT / 3.0; //ms
|
||||
public static final double PING_MEDIUM = 2 * PING_GOOD; //ms
|
||||
public static final double PING_BAD = HTTP_TIMEOUT;
|
||||
|
||||
public boolean testRpcService() {
|
||||
return testRpcService(rpcPort);
|
||||
}
|
||||
|
||||
public boolean testRpcService(NodePinger.Listener listener) {
|
||||
boolean result = testRpcService(rpcPort);
|
||||
if (listener != null)
|
||||
listener.publish(this);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Request rpcServiceRequest(int port) {
|
||||
final HttpUrl url = new HttpUrl.Builder()
|
||||
.scheme("http")
|
||||
.host(getHost())
|
||||
.port(port)
|
||||
.addPathSegment("json_rpc")
|
||||
.build();
|
||||
final String json = "{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getlastblockheader\"}";
|
||||
return new Request(url, json, getUsername(), getPassword());
|
||||
}
|
||||
|
||||
private boolean testRpcService(int port) {
|
||||
Timber.d("Testing %s", toNodeString());
|
||||
clear();
|
||||
if (hostAddress.isOnion() && !NetCipherHelper.isTor()) {
|
||||
tested = true; // sortof
|
||||
responseCode = 418; // I'm a teapot - or I need an Onion - who knows
|
||||
return false; // autofail
|
||||
}
|
||||
try {
|
||||
long ta = System.nanoTime();
|
||||
try (Response response = rpcServiceRequest(port).execute()) {
|
||||
Timber.d("%s: %s", response.code(), response.request().url());
|
||||
responseTime = (System.nanoTime() - ta) / 1000000.0;
|
||||
responseCode = response.code();
|
||||
if (response.isSuccessful()) {
|
||||
ResponseBody respBody = response.body(); // closed through Response object
|
||||
if ((respBody != null) && (respBody.contentLength() < 2000)) { // sanity check
|
||||
final JSONObject json = new JSONObject(respBody.string());
|
||||
String rpcVersion = json.getString("jsonrpc");
|
||||
if (!RPC_VERSION.equals(rpcVersion))
|
||||
return false;
|
||||
final JSONObject result = json.getJSONObject("result");
|
||||
if (!result.has("credits")) // introduced in monero v0.15.0
|
||||
return false;
|
||||
final JSONObject header = result.getJSONObject("block_header");
|
||||
height = header.getLong("height");
|
||||
timestamp = header.getLong("timestamp");
|
||||
majorVersion = header.getInt("major_version");
|
||||
return true; // success
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException | JSONException ex) {
|
||||
Timber.d("EX: %s", ex.getMessage()); //TODO: do something here (show error?)
|
||||
} finally {
|
||||
tested = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static final private int[] TEST_PORTS = {18089}; // check only opt-in port
|
||||
|
||||
public boolean findRpcService() {
|
||||
// if already have an rpcPort, use that
|
||||
if (rpcPort > 0) return testRpcService(rpcPort);
|
||||
// otherwise try to find one
|
||||
for (int port : TEST_PORTS) {
|
||||
if (testRpcService(port)) { // found a service
|
||||
this.rpcPort = port;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static public final int STALE_NODE_HOURS = 2;
|
||||
|
||||
public void showInfo(TextView view, String info, boolean isError) {
|
||||
final Context ctx = view.getContext();
|
||||
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),
|
||||
(hostAddress.isOnion() ? " .onion " : ""), " " + info));
|
||||
view.setText(text);
|
||||
if (isError)
|
||||
view.setTextColor(ThemeHelper.getThemedColor(ctx, R.attr.colorError));
|
||||
else
|
||||
view.setTextColor(ThemeHelper.getThemedColor(ctx, android.R.attr.textColorSecondary));
|
||||
}
|
||||
|
||||
public void showInfo(TextView view) {
|
||||
if (!isTested()) {
|
||||
showInfo(view, "", false);
|
||||
return;
|
||||
}
|
||||
final Context ctx = view.getContext();
|
||||
final long now = Calendar.getInstance().getTimeInMillis() / 1000;
|
||||
final long secs = (now - timestamp);
|
||||
final long mins = secs / 60;
|
||||
final long hours = mins / 60;
|
||||
final long days = hours / 24;
|
||||
String info;
|
||||
if (mins < 2) {
|
||||
info = ctx.getString(R.string.node_updated_now, secs);
|
||||
} else if (hours < 2) {
|
||||
info = ctx.getString(R.string.node_updated_mins, mins);
|
||||
} else if (days < 2) {
|
||||
info = ctx.getString(R.string.node_updated_hours, hours);
|
||||
} else {
|
||||
info = ctx.getString(R.string.node_updated_days, days);
|
||||
}
|
||||
showInfo(view, info, hours >= STALE_NODE_HOURS);
|
||||
}
|
||||
}
|
@ -28,6 +28,7 @@ import lombok.ToString;
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
public class Subaddress implements Comparable<Subaddress> {
|
||||
public static final Pattern DEFAULT_LABEL_FORMATTER = Pattern.compile("^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}:[0-9]{2}:[0-9]{2}$");
|
||||
@Getter
|
||||
final private int accountIndex;
|
||||
@Getter
|
||||
@ -52,8 +53,6 @@ public class Subaddress implements Comparable<Subaddress> {
|
||||
return address.substring(0, 8) + "…" + address.substring(address.length() - 8);
|
||||
}
|
||||
|
||||
public static final Pattern DEFAULT_LABEL_FORMATTER = Pattern.compile("^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}:[0-9]{2}:[0-9]{2}$");
|
||||
|
||||
public String getDisplayLabel() {
|
||||
if (label.isEmpty() || (DEFAULT_LABEL_FORMATTER.matcher(label).matches()))
|
||||
return ("#" + addressIndex);
|
||||
|
@ -26,6 +26,22 @@ import com.m2049r.xmrwallet.util.Helper;
|
||||
// https://stackoverflow.com/questions/2139134/how-to-send-an-object-from-one-android-activity-to-another-using-intents
|
||||
public class TxData implements Parcelable {
|
||||
|
||||
// this is used to regenerate your object. All Parcelables must have a CREATOR that implements these two methods
|
||||
public static final Parcelable.Creator<TxData> CREATOR = new Parcelable.Creator<TxData>() {
|
||||
public TxData createFromParcel(Parcel in) {
|
||||
return new TxData(in);
|
||||
}
|
||||
|
||||
public TxData[] newArray(int size) {
|
||||
return new TxData[size];
|
||||
}
|
||||
};
|
||||
private String dstAddr;
|
||||
private long amount;
|
||||
private int mixin;
|
||||
private PendingTransaction.Priority priority;
|
||||
private UserNotes userNotes;
|
||||
|
||||
public TxData() {
|
||||
}
|
||||
|
||||
@ -46,30 +62,26 @@ public class TxData implements Parcelable {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
protected TxData(Parcel in) {
|
||||
dstAddr = in.readString();
|
||||
amount = in.readLong();
|
||||
mixin = in.readInt();
|
||||
priority = PendingTransaction.Priority.fromInteger(in.readInt());
|
||||
|
||||
}
|
||||
|
||||
public String getDestinationAddress() {
|
||||
return dstAddr;
|
||||
}
|
||||
|
||||
public long getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public double getAmountAsDouble() {
|
||||
return 1.0 * amount / Helper.ONE_XMR;
|
||||
}
|
||||
|
||||
public int getMixin() {
|
||||
return mixin;
|
||||
}
|
||||
|
||||
public PendingTransaction.Priority getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
public void setDestinationAddress(String dstAddr) {
|
||||
this.dstAddr = dstAddr;
|
||||
}
|
||||
|
||||
public long getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public void setAmount(long amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
@ -78,10 +90,22 @@ public class TxData implements Parcelable {
|
||||
this.amount = Wallet.getAmountFromDouble(amount);
|
||||
}
|
||||
|
||||
public double getAmountAsDouble() {
|
||||
return 1.0 * amount / Helper.ONE_XMR;
|
||||
}
|
||||
|
||||
public int getMixin() {
|
||||
return mixin;
|
||||
}
|
||||
|
||||
public void setMixin(int mixin) {
|
||||
this.mixin = mixin;
|
||||
}
|
||||
|
||||
public PendingTransaction.Priority getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
public void setPriority(PendingTransaction.Priority priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
@ -94,13 +118,6 @@ public class TxData implements Parcelable {
|
||||
this.userNotes = userNotes;
|
||||
}
|
||||
|
||||
private String dstAddr;
|
||||
private long amount;
|
||||
private int mixin;
|
||||
private PendingTransaction.Priority priority;
|
||||
|
||||
private UserNotes userNotes;
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeString(dstAddr);
|
||||
@ -109,25 +126,6 @@ public class TxData implements Parcelable {
|
||||
out.writeInt(priority.getValue());
|
||||
}
|
||||
|
||||
// this is used to regenerate your object. All Parcelables must have a CREATOR that implements these two methods
|
||||
public static final Parcelable.Creator<TxData> CREATOR = new Parcelable.Creator<TxData>() {
|
||||
public TxData createFromParcel(Parcel in) {
|
||||
return new TxData(in);
|
||||
}
|
||||
|
||||
public TxData[] newArray(int size) {
|
||||
return new TxData[size];
|
||||
}
|
||||
};
|
||||
|
||||
protected TxData(Parcel in) {
|
||||
dstAddr = in.readString();
|
||||
amount = in.readLong();
|
||||
mixin = in.readInt();
|
||||
priority = PendingTransaction.Priority.fromInteger(in.readInt());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
|
@ -1,101 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 m2049r
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.m2049r.xmrwallet.data;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
public class TxDataBtc extends TxData {
|
||||
@Getter
|
||||
@Setter
|
||||
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;
|
||||
|
||||
public TxDataBtc() {
|
||||
super();
|
||||
}
|
||||
|
||||
public TxDataBtc(TxDataBtc txDataBtc) {
|
||||
super(txDataBtc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
super.writeToParcel(out, flags);
|
||||
out.writeString(btcSymbol);
|
||||
out.writeString(xmrtoOrderId);
|
||||
out.writeString(btcAddress);
|
||||
out.writeDouble(btcAmount);
|
||||
}
|
||||
|
||||
// this is used to regenerate your object. All Parcelables must have a CREATOR that implements these two methods
|
||||
public static final Creator<TxDataBtc> CREATOR = new Creator<TxDataBtc>() {
|
||||
public TxDataBtc createFromParcel(Parcel in) {
|
||||
return new TxDataBtc(in);
|
||||
}
|
||||
|
||||
public TxDataBtc[] newArray(int size) {
|
||||
return new TxDataBtc[size];
|
||||
}
|
||||
};
|
||||
|
||||
protected TxDataBtc(Parcel in) {
|
||||
super(in);
|
||||
btcSymbol = in.readString();
|
||||
xmrtoOrderId = in.readString();
|
||||
btcAddress = in.readString();
|
||||
btcAmount = in.readDouble();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("xmrtoOrderId:");
|
||||
sb.append(xmrtoOrderId);
|
||||
sb.append(",btcSymbol:");
|
||||
sb.append(btcSymbol);
|
||||
sb.append(",btcAddress:");
|
||||
sb.append(btcAddress);
|
||||
sb.append(",btcAmount:");
|
||||
sb.append(btcAmount);
|
||||
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.equalsIgnoreCase(btcAddress);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,8 +4,6 @@ import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
@ -14,13 +12,8 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.Constants;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class InformationBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
public boolean showCopyButton = false;
|
||||
public String information = "";
|
||||
@ -34,7 +27,7 @@ public class InformationBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
ImageButton copyInformationImageButton = view.findViewById(R.id.copy_information_imagebutton);
|
||||
if(showCopyButton) {
|
||||
if (showCopyButton) {
|
||||
copyInformationImageButton.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
copyInformationImageButton.setVisibility(View.INVISIBLE);
|
||||
|
@ -7,20 +7,13 @@ import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.service.BalanceService;
|
||||
import com.m2049r.xmrwallet.service.TxService;
|
||||
import com.m2049r.xmrwallet.util.Constants;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
@ -51,7 +44,7 @@ public class PasswordBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
unlockWalletButton.setOnClickListener(view1 -> {
|
||||
String password = passwordEditText.getText().toString();
|
||||
boolean success = checkPassword(walletFile, password);
|
||||
if(success) {
|
||||
if (success) {
|
||||
listener.onPasswordSuccess(password);
|
||||
dismiss();
|
||||
} else {
|
||||
@ -66,6 +59,7 @@ public class PasswordBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
|
||||
public interface PasswordListener {
|
||||
void onPasswordSuccess(String password);
|
||||
|
||||
void onPasswordFail();
|
||||
}
|
||||
}
|
@ -2,20 +2,8 @@ package com.m2049r.xmrwallet.fragment.dialog;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.google.zxing.client.android.Intents;
|
||||
import com.journeyapps.barcodescanner.ScanContract;
|
||||
import com.journeyapps.barcodescanner.ScanOptions;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.model.PendingTransaction;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.service.BalanceService;
|
||||
import com.m2049r.xmrwallet.service.TxService;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@ -32,31 +20,38 @@ import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.google.zxing.client.android.Intents;
|
||||
import com.journeyapps.barcodescanner.ScanContract;
|
||||
import com.journeyapps.barcodescanner.ScanOptions;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.model.PendingTransaction;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.service.BalanceService;
|
||||
import com.m2049r.xmrwallet.service.TxService;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SendBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
private final ActivityResultLauncher<ScanOptions> barcodeLauncher = registerForActivityResult(new ScanContract(),
|
||||
result -> {
|
||||
if(result.getContents() != null) {
|
||||
pasteAddress(result.getContents());
|
||||
}
|
||||
});
|
||||
|
||||
private final ActivityResultLauncher<String> cameraPermissionsLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(),
|
||||
private final MutableLiveData<Boolean> _sendingMax = new MutableLiveData<>(false);
|
||||
public LiveData<Boolean> sendingMax = _sendingMax; private final ActivityResultLauncher<String> cameraPermissionsLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(),
|
||||
granted -> {
|
||||
if(granted) {
|
||||
if (granted) {
|
||||
onScan();
|
||||
} else {
|
||||
Toast.makeText(getActivity(), getString(R.string.no_camera_permission), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
|
||||
private MutableLiveData<Boolean> _sendingMax = new MutableLiveData<>(false);
|
||||
public LiveData<Boolean> sendingMax = _sendingMax;
|
||||
private MutableLiveData<PendingTransaction> _pendingTransaction = new MutableLiveData<>(null);
|
||||
private final MutableLiveData<PendingTransaction> _pendingTransaction = new MutableLiveData<>(null);
|
||||
public LiveData<PendingTransaction> pendingTransaction = _pendingTransaction;
|
||||
|
||||
private EditText addressEditText;
|
||||
private final ActivityResultLauncher<ScanOptions> barcodeLauncher = registerForActivityResult(new ScanContract(),
|
||||
result -> {
|
||||
if (result.getContents() != null) {
|
||||
pasteAddress(result.getContents());
|
||||
}
|
||||
});
|
||||
private EditText amountEditText;
|
||||
private TextView sendAllTextView;
|
||||
private TextView feeTextView;
|
||||
@ -90,9 +85,9 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
|
||||
pasteAddressImageButton.setOnClickListener(view1 -> {
|
||||
Context ctx = getContext();
|
||||
if(ctx != null) {
|
||||
if (ctx != null) {
|
||||
String clipboard = Helper.getClipBoardText(ctx);
|
||||
if(clipboard != null) {
|
||||
if (clipboard != null) {
|
||||
pasteAddress(clipboard);
|
||||
}
|
||||
}
|
||||
@ -115,7 +110,7 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
if (validAddress && (!amount.isEmpty() || sendAll)) {
|
||||
long amountRaw = Wallet.getAmountFromString(amount);
|
||||
long balance = BalanceService.getInstance().getUnlockedBalanceRaw();
|
||||
if((amountRaw >= balance || amountRaw <= 0) && !sendAll) {
|
||||
if ((amountRaw >= balance || amountRaw <= 0) && !sendAll) {
|
||||
Toast.makeText(getActivity(), getString(R.string.send_amount_invalid), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
@ -131,7 +126,7 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
|
||||
sendButton.setOnClickListener(view1 -> {
|
||||
PendingTransaction pendingTx = pendingTransaction.getValue();
|
||||
if(pendingTx != null) {
|
||||
if (pendingTx != null) {
|
||||
Toast.makeText(getActivity(), getString(R.string.sending_tx), Toast.LENGTH_SHORT).show();
|
||||
sendButton.setEnabled(false);
|
||||
sendTx(pendingTx);
|
||||
@ -139,7 +134,7 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
});
|
||||
|
||||
sendingMax.observe(getViewLifecycleOwner(), sendingMax -> {
|
||||
if(pendingTransaction.getValue() == null) {
|
||||
if (pendingTransaction.getValue() == null) {
|
||||
if (sendingMax) {
|
||||
amountEditText.setVisibility(View.INVISIBLE);
|
||||
sendAllTextView.setVisibility(View.VISIBLE);
|
||||
@ -155,7 +150,7 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
pendingTransaction.observe(getViewLifecycleOwner(), pendingTx -> {
|
||||
showConfirmationLayout(pendingTx != null);
|
||||
|
||||
if(pendingTx != null) {
|
||||
if (pendingTx != null) {
|
||||
String address = addressEditText.getText().toString();
|
||||
addressTextView.setText(getString(R.string.tx_address_text, address));
|
||||
amountTextView.setText(getString(R.string.tx_amount_text, Helper.getDisplayAmount(pendingTx.getAmount())));
|
||||
@ -179,7 +174,7 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
AsyncTask.execute(() -> {
|
||||
boolean success = TxService.getInstance().sendTx(pendingTx);
|
||||
Activity activity = getActivity();
|
||||
if(activity != null) {
|
||||
if (activity != null) {
|
||||
activity.runOnUiThread(() -> {
|
||||
if (success) {
|
||||
Toast.makeText(getActivity(), getString(R.string.sent_tx), Toast.LENGTH_SHORT).show();
|
||||
@ -196,11 +191,11 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
private void createTx(String address, String amount, boolean sendAll, PendingTransaction.Priority feePriority) {
|
||||
AsyncTask.execute(() -> {
|
||||
PendingTransaction pendingTx = TxService.getInstance().createTx(address, amount, sendAll, feePriority);
|
||||
if(pendingTx != null) {
|
||||
if (pendingTx != null) {
|
||||
_pendingTransaction.postValue(pendingTx);
|
||||
} else {
|
||||
Activity activity = getActivity();
|
||||
if(activity != null) {
|
||||
if (activity != null) {
|
||||
activity.runOnUiThread(() -> {
|
||||
createButton.setEnabled(true);
|
||||
Toast.makeText(getActivity(), getString(R.string.error_creating_tx), Toast.LENGTH_SHORT).show();
|
||||
@ -211,7 +206,7 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
}
|
||||
|
||||
private void showConfirmationLayout(boolean show) {
|
||||
if(show) {
|
||||
if (show) {
|
||||
sendButton.setVisibility(View.VISIBLE);
|
||||
addressEditText.setVisibility(View.GONE);
|
||||
amountEditText.setVisibility(View.GONE);
|
||||
@ -241,10 +236,12 @@ public class SendBottomSheetDialog extends BottomSheetDialogFragment {
|
||||
private void pasteAddress(String address) {
|
||||
String modifiedAddress = address.replace("monero:", "").split("\\?")[0];
|
||||
boolean isValid = Wallet.isAddressValid(modifiedAddress);
|
||||
if(isValid) {
|
||||
if (isValid) {
|
||||
addressEditText.setText(modifiedAddress);
|
||||
} else {
|
||||
Toast.makeText(getActivity(), getString(R.string.send_address_invalid), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -5,25 +5,20 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
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 androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.m2049r.xmrwallet.MainActivity;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.adapter.TransactionInfoAdapter;
|
||||
@ -32,20 +27,16 @@ import com.m2049r.xmrwallet.fragment.dialog.SendBottomSheetDialog;
|
||||
import com.m2049r.xmrwallet.model.TransactionInfo;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.service.AddressService;
|
||||
import com.m2049r.xmrwallet.service.BalanceService;
|
||||
import com.m2049r.xmrwallet.service.BlockchainService;
|
||||
import com.m2049r.xmrwallet.service.HistoryService;
|
||||
import com.m2049r.xmrwallet.service.PrefService;
|
||||
import com.m2049r.xmrwallet.service.TxService;
|
||||
import com.m2049r.xmrwallet.util.Constants;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
public class HomeFragment extends Fragment implements TransactionInfoAdapter.TxInfoAdapterListener {
|
||||
|
||||
private HomeViewModel mViewModel;
|
||||
long startHeight = 0;
|
||||
private HomeViewModel mViewModel;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@ -56,7 +47,7 @@ public class HomeFragment extends Fragment implements TransactionInfoAdapter.TxI
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
MainActivity mainActivity = (MainActivity)getActivity();
|
||||
MainActivity mainActivity = (MainActivity) getActivity();
|
||||
mViewModel = new ViewModelProvider(this).get(HomeViewModel.class);
|
||||
bindObservers(view);
|
||||
bindListeners(view);
|
||||
@ -96,7 +87,7 @@ public class HomeFragment extends Fragment implements TransactionInfoAdapter.TxI
|
||||
HistoryService historyService = HistoryService.getInstance();
|
||||
BlockchainService blockchainService = BlockchainService.getInstance();
|
||||
|
||||
if(balanceService != null) {
|
||||
if (balanceService != null) {
|
||||
balanceService.balance.observe(getViewLifecycleOwner(), balance -> {
|
||||
unlockedBalanceTextView.setText(getString(R.string.wallet_balance_text, Wallet.getDisplayAmount(balance)));
|
||||
});
|
||||
@ -112,7 +103,7 @@ public class HomeFragment extends Fragment implements TransactionInfoAdapter.TxI
|
||||
}
|
||||
|
||||
ProgressBar progressBar = view.findViewById(R.id.sync_progress_bar);
|
||||
if(blockchainService != null) {
|
||||
if (blockchainService != null) {
|
||||
blockchainService.height.observe(getViewLifecycleOwner(), height -> {
|
||||
Wallet wallet = WalletManager.getInstance().getWallet();
|
||||
if (!wallet.isSynchronized()) {
|
||||
@ -135,13 +126,13 @@ public class HomeFragment extends Fragment implements TransactionInfoAdapter.TxI
|
||||
TransactionInfoAdapter adapter = new TransactionInfoAdapter(this);
|
||||
txHistoryRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||
txHistoryRecyclerView.setAdapter(adapter);
|
||||
if(historyService != null) {
|
||||
if (historyService != null) {
|
||||
historyService.history.observe(getViewLifecycleOwner(), history -> {
|
||||
if (history.isEmpty()) {
|
||||
txHistoryRecyclerView.setVisibility(View.GONE);
|
||||
} else {
|
||||
Collections.sort(history);
|
||||
if(history.size() > 100) {
|
||||
if (history.size() > 100) {
|
||||
adapter.submitList(history.subList(0, 99));
|
||||
} else {
|
||||
adapter.submitList(history);
|
||||
|
@ -1,21 +1,7 @@
|
||||
package com.m2049r.xmrwallet.fragment.home;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.EncodeHintType;
|
||||
import com.google.zxing.WriterException;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class HomeViewModel extends ViewModel {
|
||||
|
||||
}
|
@ -17,7 +17,6 @@ import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
|
||||
@ -55,7 +54,7 @@ public class OnboardingFragment extends Fragment {
|
||||
|
||||
createWalletButton.setOnClickListener(view1 -> {
|
||||
String walletPassword = walletPasswordEditText.getText().toString();
|
||||
if(!walletPassword.isEmpty()) {
|
||||
if (!walletPassword.isEmpty()) {
|
||||
PrefService.getInstance().edit().putBoolean(Constants.PREF_USES_PASSWORD, true).apply();
|
||||
}
|
||||
String walletSeed = walletSeedEditText.getText().toString().trim();
|
||||
@ -63,14 +62,14 @@ public class OnboardingFragment extends Fragment {
|
||||
long restoreHeight = -1;
|
||||
File walletFile = new File(getActivity().getApplicationInfo().dataDir, Constants.WALLET_NAME);
|
||||
Wallet wallet = null;
|
||||
if(walletSeed.isEmpty()) {
|
||||
if (walletSeed.isEmpty()) {
|
||||
wallet = WalletManager.getInstance().createWallet(walletFile, walletPassword, Constants.MNEMONIC_LANGUAGE, restoreHeight);
|
||||
} else {
|
||||
if(!checkMnemonic(walletSeed)) {
|
||||
if (!checkMnemonic(walletSeed)) {
|
||||
Toast.makeText(getContext(), getString(R.string.invalid_mnemonic_code), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
if(!restoreHeightText.isEmpty()) {
|
||||
if (!restoreHeightText.isEmpty()) {
|
||||
restoreHeight = Long.parseLong(restoreHeightText);
|
||||
}
|
||||
wallet = WalletManager.getInstance().recoveryWallet(walletFile, walletPassword, walletSeed, "", restoreHeight);
|
||||
@ -78,21 +77,24 @@ public class OnboardingFragment extends Fragment {
|
||||
boolean ok = wallet.getStatus().isOk();
|
||||
walletFile.delete(); // cache is broken for some reason when recovering wallets. delete the file here. this happens in monerujo too.
|
||||
|
||||
if(ok) {
|
||||
((MainActivity)getActivity()).init(walletFile, walletPassword);
|
||||
if (ok) {
|
||||
((MainActivity) getActivity()).init(walletFile, walletPassword);
|
||||
getActivity().onBackPressed();
|
||||
}
|
||||
});
|
||||
walletSeedEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
String text = editable.toString();
|
||||
if(text.isEmpty()) {
|
||||
if (text.isEmpty()) {
|
||||
createWalletButton.setText(R.string.create_wallet);
|
||||
} else {
|
||||
createWalletButton.setText(R.string.menu_restore);
|
||||
@ -101,7 +103,7 @@ public class OnboardingFragment extends Fragment {
|
||||
});
|
||||
|
||||
mViewModel.showMoreOptions.observe(getViewLifecycleOwner(), show -> {
|
||||
if(show) {
|
||||
if (show) {
|
||||
moreOptionsChevronImageView.setImageResource(R.drawable.ic_keyboard_arrow_up);
|
||||
walletSeedEditText.setVisibility(View.VISIBLE);
|
||||
walletRestoreHeightEditText.setVisibility(View.VISIBLE);
|
||||
|
@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
public class OnboardingViewModel extends ViewModel {
|
||||
private MutableLiveData<Boolean> _showMoreOptions = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<Boolean> _showMoreOptions = new MutableLiveData<>(false);
|
||||
public LiveData<Boolean> showMoreOptions = _showMoreOptions;
|
||||
|
||||
public void onMoreOptionsClicked() {
|
||||
|
@ -5,8 +5,6 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@ -18,9 +16,7 @@ import androidx.lifecycle.ViewModelProvider;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.fragment.dialog.InformationBottomSheetDialog;
|
||||
import com.m2049r.xmrwallet.fragment.dialog.PasswordBottomSheetDialog;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.service.BlockchainService;
|
||||
import com.m2049r.xmrwallet.service.PrefService;
|
||||
import com.m2049r.xmrwallet.util.Constants;
|
||||
import com.m2049r.xmrwallet.util.DayNightMode;
|
||||
@ -46,7 +42,7 @@ public class SettingsFragment extends Fragment implements PasswordBottomSheetDia
|
||||
|
||||
nightModeSwitch.setChecked(NightmodeHelper.getPreferredNightmode() == DayNightMode.NIGHT);
|
||||
nightModeSwitch.setOnCheckedChangeListener((compoundButton, b) -> {
|
||||
if(b) {
|
||||
if (b) {
|
||||
NightmodeHelper.setAndSavePreferredNightmode(DayNightMode.NIGHT);
|
||||
} else {
|
||||
NightmodeHelper.setAndSavePreferredNightmode(DayNightMode.DAY);
|
||||
@ -65,7 +61,7 @@ public class SettingsFragment extends Fragment implements PasswordBottomSheetDia
|
||||
|
||||
displaySeedButton.setOnClickListener(view1 -> {
|
||||
boolean usesPassword = PrefService.getInstance().getBoolean(Constants.PREF_USES_PASSWORD, false);
|
||||
if(usesPassword) {
|
||||
if (usesPassword) {
|
||||
PasswordBottomSheetDialog passwordDialog = new PasswordBottomSheetDialog();
|
||||
passwordDialog.listener = this;
|
||||
passwordDialog.show(getActivity().getSupportFragmentManager(), "password_dialog");
|
||||
|
@ -1,155 +0,0 @@
|
||||
/*
|
||||
* 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.ledger;
|
||||
|
||||
public enum Instruction {
|
||||
|
||||
INS_NONE(0x00),
|
||||
INS_RESET(0x02),
|
||||
INS_GET_KEY(0x20),
|
||||
INS_DISPLAY_ADDRESS(0x21),
|
||||
INS_PUT_KEY(0x22),
|
||||
INS_GET_CHACHA8_PREKEY(0x24),
|
||||
INS_VERIFY_KEY(0x26),
|
||||
INS_MANAGE_SEEDWORDS(0x28),
|
||||
|
||||
INS_SECRET_KEY_TO_PUBLIC_KEY(0x30),
|
||||
INS_GEN_KEY_DERIVATION(0x32),
|
||||
INS_DERIVATION_TO_SCALAR(0x34),
|
||||
INS_DERIVE_PUBLIC_KEY(0x36),
|
||||
INS_DERIVE_SECRET_KEY(0x38),
|
||||
INS_GEN_KEY_IMAGE(0x3A),
|
||||
|
||||
INS_SECRET_KEY_ADD(0x3C),
|
||||
INS_SECRET_KEY_SUB(0x3E),
|
||||
INS_GENERATE_KEYPAIR(0x40),
|
||||
INS_SECRET_SCAL_MUL_KEY(0x42),
|
||||
INS_SECRET_SCAL_MUL_BASE(0x44),
|
||||
|
||||
INS_DERIVE_SUBADDRESS_PUBLIC_KEY(0x46),
|
||||
INS_GET_SUBADDRESS(0x48),
|
||||
INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY(0x4A),
|
||||
INS_GET_SUBADDRESS_SECRET_KEY(0x4C),
|
||||
|
||||
INS_OPEN_TX(0x70),
|
||||
INS_SET_SIGNATURE_MODE(0x72),
|
||||
INS_GET_ADDITIONAL_KEY(0x74),
|
||||
INS_STEALTH(0x76),
|
||||
INS_GEN_COMMITMENT_MASK(0x77),
|
||||
INS_BLIND(0x78),
|
||||
INS_UNBLIND(0x7A),
|
||||
INS_GEN_TXOUT_KEYS(0x7B),
|
||||
INS_VALIDATE(0x7C),
|
||||
INS_PREFIX_HASH(0x7D),
|
||||
INS_MLSAG(0x7E),
|
||||
INS_CLOSE_TX(0x80),
|
||||
|
||||
INS_GET_TX_PROOF(0xA0),
|
||||
|
||||
INS_GET_RESPONSE(0xC0),
|
||||
|
||||
INS_UNDEFINED(0xFF);
|
||||
|
||||
public static Instruction fromByte(byte n) {
|
||||
switch (n & 0xFF) {
|
||||
case 0x00:
|
||||
return INS_NONE;
|
||||
case 0x02:
|
||||
return INS_RESET;
|
||||
|
||||
case 0x20:
|
||||
return INS_GET_KEY;
|
||||
case 0x22:
|
||||
return INS_PUT_KEY;
|
||||
case 0x24:
|
||||
return INS_GET_CHACHA8_PREKEY;
|
||||
case 0x26:
|
||||
return INS_VERIFY_KEY;
|
||||
|
||||
case 0x30:
|
||||
return INS_SECRET_KEY_TO_PUBLIC_KEY;
|
||||
case 0x32:
|
||||
return INS_GEN_KEY_DERIVATION;
|
||||
case 0x34:
|
||||
return INS_DERIVATION_TO_SCALAR;
|
||||
case 0x36:
|
||||
return INS_DERIVE_PUBLIC_KEY;
|
||||
case 0x38:
|
||||
return INS_DERIVE_SECRET_KEY;
|
||||
case 0x3A:
|
||||
return INS_GEN_KEY_IMAGE;
|
||||
case 0x3C:
|
||||
return INS_SECRET_KEY_ADD;
|
||||
case 0x3E:
|
||||
return INS_SECRET_KEY_SUB;
|
||||
case 0x40:
|
||||
return INS_GENERATE_KEYPAIR;
|
||||
case 0x42:
|
||||
return INS_SECRET_SCAL_MUL_KEY;
|
||||
case 0x44:
|
||||
return INS_SECRET_SCAL_MUL_BASE;
|
||||
|
||||
case 0x46:
|
||||
return INS_DERIVE_SUBADDRESS_PUBLIC_KEY;
|
||||
case 0x48:
|
||||
return INS_GET_SUBADDRESS;
|
||||
case 0x4A:
|
||||
return INS_GET_SUBADDRESS_SPEND_PUBLIC_KEY;
|
||||
case 0x4C:
|
||||
return INS_GET_SUBADDRESS_SECRET_KEY;
|
||||
|
||||
case 0x70:
|
||||
return INS_OPEN_TX;
|
||||
case 0x72:
|
||||
return INS_SET_SIGNATURE_MODE;
|
||||
case 0x74:
|
||||
return INS_GET_ADDITIONAL_KEY;
|
||||
case 0x76:
|
||||
return INS_STEALTH;
|
||||
case 0x78:
|
||||
return INS_BLIND;
|
||||
case 0x7A:
|
||||
return INS_UNBLIND;
|
||||
case 0x7C:
|
||||
return INS_VALIDATE;
|
||||
case 0x7E:
|
||||
return INS_MLSAG;
|
||||
case 0x80:
|
||||
return INS_CLOSE_TX;
|
||||
|
||||
case 0xc0:
|
||||
return INS_GET_RESPONSE;
|
||||
|
||||
default:
|
||||
return INS_UNDEFINED;
|
||||
}
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public byte getByteValue() {
|
||||
return (byte) (value & 0xFF);
|
||||
}
|
||||
|
||||
private final int value;
|
||||
|
||||
Instruction(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
@ -1,240 +0,0 @@
|
||||
/*
|
||||
*******************************************************************************
|
||||
* BTChip Bitcoin Hardware Wallet Java API
|
||||
* (c) 2014 BTChip - 1BTChip7VfTnrPra5jqci7ejnMguuHogTn
|
||||
* 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.ledger;
|
||||
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbManager;
|
||||
|
||||
import com.btchip.BTChipException;
|
||||
import com.btchip.comm.BTChipTransport;
|
||||
import com.btchip.comm.android.BTChipTransportAndroidHID;
|
||||
import com.m2049r.xmrwallet.BuildConfig;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.Helper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class Ledger {
|
||||
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;
|
||||
public static final int SW_INS_NOT_SUPPORTED = 0x6D00;
|
||||
public static final int[] OK = {SW_OK};
|
||||
public static final int MINIMUM_LEDGER_VERSION = (1 << 16) + (8 << 8) + (0); // 1.6.0
|
||||
|
||||
public static UsbDevice findDevice(UsbManager usbManager) {
|
||||
if (!ENABLED) return null;
|
||||
return BTChipTransportAndroidHID.getDevice(usbManager);
|
||||
}
|
||||
|
||||
static private Ledger Instance = null;
|
||||
|
||||
static public String connect(UsbManager usbManager, UsbDevice usbDevice) throws IOException {
|
||||
if (Instance != null) {
|
||||
disconnect();
|
||||
}
|
||||
Instance = new Ledger(usbManager, usbDevice);
|
||||
return Name();
|
||||
}
|
||||
|
||||
static public void disconnect() {
|
||||
// this is not synchronized so as to close immediately
|
||||
if (Instance != null) {
|
||||
Instance.close();
|
||||
Instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
static public boolean isConnected() {
|
||||
//TODO synchronize with connect/disconnect?
|
||||
return Instance != null;
|
||||
}
|
||||
|
||||
static public String Name() {
|
||||
if (Instance != null) {
|
||||
return Instance.name;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static public byte[] Exchange(byte[] apdu) {
|
||||
if (Instance != null) {
|
||||
Timber.d("INS: %s", Instruction.fromByte(apdu[1]));
|
||||
return Instance.exchangeRaw(apdu);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static public boolean check() {
|
||||
if (Instance == null) return false;
|
||||
byte[] moneroVersion = WalletManager.moneroVersion().getBytes(StandardCharsets.US_ASCII);
|
||||
|
||||
try {
|
||||
byte[] resp = Instance.exchangeApduNoOpt(Instruction.INS_RESET, moneroVersion, OK);
|
||||
int deviceVersion = (resp[0] << 16) + (resp[1] << 8) + (resp[2]);
|
||||
if (deviceVersion < MINIMUM_LEDGER_VERSION)
|
||||
return false;
|
||||
} catch (BTChipException ex) { // comm error - probably wrong app started on device
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
final private BTChipTransport transport;
|
||||
final private String name;
|
||||
private int lastSW = 0;
|
||||
|
||||
private Ledger(UsbManager usbManager, UsbDevice usbDevice) throws IOException {
|
||||
final BTChipTransport transport = BTChipTransportAndroidHID.open(usbManager, usbDevice);
|
||||
Timber.d("transport opened = %s", transport.toString());
|
||||
transport.setDebug(BuildConfig.DEBUG);
|
||||
this.transport = transport;
|
||||
this.name = usbDevice.getManufacturerName() + " " + usbDevice.getProductName();
|
||||
initKey();
|
||||
}
|
||||
|
||||
synchronized private void close() {
|
||||
initKey(); // don't leak key after we disconnect
|
||||
transport.close();
|
||||
Timber.d("transport closed");
|
||||
lastSW = 0;
|
||||
}
|
||||
|
||||
synchronized private byte[] exchangeRaw(byte[] apdu) {
|
||||
if (transport == null)
|
||||
throw new IllegalStateException("No transport (probably closed previously)");
|
||||
Timber.d("exchangeRaw %02x", apdu[1]);
|
||||
Instruction ins = Instruction.fromByte(apdu[1]);
|
||||
if (listener != null) listener.onInstructionSend(ins, apdu);
|
||||
sniffOut(ins, apdu);
|
||||
byte[] data = transport.exchange(apdu);
|
||||
if (listener != null) listener.onInstructionReceive(ins, data);
|
||||
sniffIn(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
private byte[] exchange(byte[] apdu) throws BTChipException {
|
||||
byte[] response = exchangeRaw(apdu);
|
||||
if (response.length < 2) {
|
||||
throw new BTChipException("Truncated response");
|
||||
}
|
||||
lastSW = ((response[response.length - 2] & 0xff) << 8) |
|
||||
response[response.length - 1] & 0xff;
|
||||
byte[] result = new byte[response.length - 2];
|
||||
System.arraycopy(response, 0, result, 0, response.length - 2);
|
||||
return result;
|
||||
}
|
||||
|
||||
private byte[] exchangeCheck(byte[] apdu, int[] acceptedSW) throws BTChipException {
|
||||
byte[] response = exchange(apdu);
|
||||
if (acceptedSW == null) {
|
||||
return response;
|
||||
}
|
||||
for (int SW : acceptedSW) {
|
||||
if (lastSW == SW) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
throw new BTChipException("Invalid status", lastSW);
|
||||
}
|
||||
|
||||
private byte[] exchangeApduNoOpt(Instruction instruction, byte[] data, int[] acceptedSW)
|
||||
throws BTChipException {
|
||||
byte[] apdu = new byte[data.length + 6];
|
||||
apdu[0] = PROTOCOL_VERSION;
|
||||
apdu[1] = instruction.getByteValue();
|
||||
apdu[2] = 0; // p1
|
||||
apdu[3] = 0; // p2
|
||||
apdu[4] = (byte) (data.length + 1); // +1 because the opt byte is part of the data
|
||||
apdu[5] = 0; // opt
|
||||
System.arraycopy(data, 0, apdu, 6, data.length);
|
||||
return exchangeCheck(apdu, acceptedSW);
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onInstructionSend(Instruction ins, byte[] apdu);
|
||||
|
||||
void onInstructionReceive(Instruction ins, byte[] data);
|
||||
}
|
||||
|
||||
Listener listener;
|
||||
|
||||
static public void setListener(Listener listener) {
|
||||
if (Instance != null) {
|
||||
Instance.listener = listener;
|
||||
}
|
||||
}
|
||||
|
||||
static public void unsetListener(Listener listener) {
|
||||
if ((Instance != null) && (Instance.listener == listener))
|
||||
Instance.listener = null;
|
||||
}
|
||||
|
||||
// very stupid hack to extract the view key
|
||||
// without messing around with monero core code
|
||||
// NB: as all the ledger comm can be sniffed off the USB cable - there is no security issue here
|
||||
private boolean snoopKey = false;
|
||||
private byte[] key;
|
||||
|
||||
private void initKey() {
|
||||
key = Helper.hexToBytes("0000000000000000000000000000000000000000000000000000000000000000");
|
||||
}
|
||||
|
||||
static public String Key() {
|
||||
if (Instance != null) {
|
||||
return Helper.bytesToHex(Instance.key).toLowerCase();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void sniffOut(Instruction ins, byte[] apdu) {
|
||||
if (ins == Instruction.INS_GET_KEY) {
|
||||
snoopKey = (apdu[2] == 2);
|
||||
}
|
||||
}
|
||||
|
||||
private void sniffIn(byte[] data) {
|
||||
// stupid hack to extract the view key
|
||||
// without messing around with monero core code
|
||||
if (snoopKey) {
|
||||
if (data.length == 34) { // 32 key + result code 9000
|
||||
long sw = ((data[data.length - 2] & 0xff) << 8) |
|
||||
(data[data.length - 1] & 0xff);
|
||||
Timber.e("WS %d", sw);
|
||||
if (sw == SW_OK) {
|
||||
System.arraycopy(data, 0, key, 0, 32);
|
||||
}
|
||||
}
|
||||
snoopKey = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -21,6 +21,12 @@ public enum NetworkType {
|
||||
NetworkType_Testnet(1),
|
||||
NetworkType_Stagenet(2);
|
||||
|
||||
private final int value;
|
||||
|
||||
NetworkType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static NetworkType fromInteger(int n) {
|
||||
switch (n) {
|
||||
case 0:
|
||||
@ -36,10 +42,4 @@ public enum NetworkType {
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
private final int value;
|
||||
|
||||
NetworkType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
@ -27,46 +27,6 @@ public class PendingTransaction {
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
Status_Ok,
|
||||
Status_Error,
|
||||
Status_Critical
|
||||
}
|
||||
|
||||
public enum Priority {
|
||||
Priority_Default(0),
|
||||
Priority_Low(1),
|
||||
Priority_Medium(2),
|
||||
Priority_High(3),
|
||||
Priority_Last(4);
|
||||
|
||||
public static Priority fromInteger(int n) {
|
||||
switch (n) {
|
||||
case 0:
|
||||
return Priority_Default;
|
||||
case 1:
|
||||
return Priority_Low;
|
||||
case 2:
|
||||
return Priority_Medium;
|
||||
case 3:
|
||||
return Priority_High;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
private final int value;
|
||||
|
||||
Priority(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return Status.values()[getStatusJ()];
|
||||
}
|
||||
@ -95,4 +55,44 @@ public class PendingTransaction {
|
||||
|
||||
public native long getTxCount();
|
||||
|
||||
public enum Status {
|
||||
Status_Ok,
|
||||
Status_Error,
|
||||
Status_Critical
|
||||
}
|
||||
|
||||
public enum Priority {
|
||||
Priority_Default(0),
|
||||
Priority_Low(1),
|
||||
Priority_Medium(2),
|
||||
Priority_High(3),
|
||||
Priority_Last(4);
|
||||
|
||||
private final int value;
|
||||
|
||||
Priority(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public static Priority fromInteger(int n) {
|
||||
switch (n) {
|
||||
case 0:
|
||||
return Priority_Default;
|
||||
case 1:
|
||||
return Priority_Low;
|
||||
case 2:
|
||||
return Priority_Medium;
|
||||
case 3:
|
||||
return Priority_High;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -30,6 +30,12 @@ public class TransactionHistory {
|
||||
private final long handle;
|
||||
|
||||
int accountIndex;
|
||||
private List<TransactionInfo> transactions = new ArrayList<>();
|
||||
|
||||
public TransactionHistory(long handle, int accountIndex) {
|
||||
this.handle = handle;
|
||||
this.accountIndex = accountIndex;
|
||||
}
|
||||
|
||||
public void setAccountFor(Wallet wallet) {
|
||||
if (accountIndex != wallet.getAccountIndex()) {
|
||||
@ -38,29 +44,22 @@ public class TransactionHistory {
|
||||
}
|
||||
}
|
||||
|
||||
public TransactionHistory(long handle, int accountIndex) {
|
||||
this.handle = handle;
|
||||
this.accountIndex = accountIndex;
|
||||
}
|
||||
|
||||
private void loadNotes(Wallet wallet) {
|
||||
for (TransactionInfo info : transactions) {
|
||||
info.notes = wallet.getUserNote(info.hash);
|
||||
}
|
||||
}
|
||||
|
||||
public native int getCount(); // over all accounts/subaddresses
|
||||
|
||||
//private native long getTransactionByIndexJ(int i);
|
||||
|
||||
//private native long getTransactionByIdJ(String id);
|
||||
|
||||
public native int getCount(); // over all accounts/subaddresses
|
||||
|
||||
public List<TransactionInfo> getAll() {
|
||||
return transactions;
|
||||
}
|
||||
|
||||
private List<TransactionInfo> transactions = new ArrayList<>();
|
||||
|
||||
void refreshWithNotes(Wallet wallet) {
|
||||
refresh();
|
||||
loadNotes(wallet);
|
||||
|
@ -30,26 +30,15 @@ import lombok.RequiredArgsConstructor;
|
||||
// this is a POJO for the TransactionInfoAdapter
|
||||
public class TransactionInfo implements Parcelable, Comparable<TransactionInfo> {
|
||||
public static final int CONFIRMATION = 10; // blocks
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public enum Direction {
|
||||
Direction_In(0),
|
||||
Direction_Out(1);
|
||||
|
||||
public static Direction fromInteger(int n) {
|
||||
switch (n) {
|
||||
case 0:
|
||||
return Direction_In;
|
||||
case 1:
|
||||
return Direction_Out;
|
||||
}
|
||||
return null;
|
||||
public static final Parcelable.Creator<TransactionInfo> CREATOR = new Parcelable.Creator<TransactionInfo>() {
|
||||
public TransactionInfo createFromParcel(Parcel in) {
|
||||
return new TransactionInfo(in);
|
||||
}
|
||||
|
||||
@Getter
|
||||
private final int value;
|
||||
public TransactionInfo[] newArray(int size) {
|
||||
return new TransactionInfo[size];
|
||||
}
|
||||
|
||||
};
|
||||
public Direction direction;
|
||||
public boolean isPending;
|
||||
public boolean isFailed;
|
||||
@ -100,6 +89,26 @@ public class TransactionInfo implements Parcelable, Comparable<TransactionInfo>
|
||||
this.transfers = transfers;
|
||||
}
|
||||
|
||||
private TransactionInfo(Parcel in) {
|
||||
direction = Direction.fromInteger(in.readInt());
|
||||
isPending = in.readByte() != 0;
|
||||
isFailed = in.readByte() != 0;
|
||||
amount = in.readLong();
|
||||
fee = in.readLong();
|
||||
blockheight = in.readLong();
|
||||
hash = in.readString();
|
||||
timestamp = in.readLong();
|
||||
paymentId = in.readString();
|
||||
accountIndex = in.readInt();
|
||||
addressIndex = in.readInt();
|
||||
confirmations = in.readLong();
|
||||
subaddressLabel = in.readString();
|
||||
transfers = in.readArrayList(Transfer.class.getClassLoader());
|
||||
txKey = in.readString();
|
||||
notes = in.readString();
|
||||
address = in.readString();
|
||||
}
|
||||
|
||||
public boolean isConfirmed() {
|
||||
return confirmations >= CONFIRMATION;
|
||||
}
|
||||
@ -136,36 +145,6 @@ public class TransactionInfo implements Parcelable, Comparable<TransactionInfo>
|
||||
out.writeString(address);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<TransactionInfo> CREATOR = new Parcelable.Creator<TransactionInfo>() {
|
||||
public TransactionInfo createFromParcel(Parcel in) {
|
||||
return new TransactionInfo(in);
|
||||
}
|
||||
|
||||
public TransactionInfo[] newArray(int size) {
|
||||
return new TransactionInfo[size];
|
||||
}
|
||||
};
|
||||
|
||||
private TransactionInfo(Parcel in) {
|
||||
direction = Direction.fromInteger(in.readInt());
|
||||
isPending = in.readByte() != 0;
|
||||
isFailed = in.readByte() != 0;
|
||||
amount = in.readLong();
|
||||
fee = in.readLong();
|
||||
blockheight = in.readLong();
|
||||
hash = in.readString();
|
||||
timestamp = in.readLong();
|
||||
paymentId = in.readString();
|
||||
accountIndex = in.readInt();
|
||||
addressIndex = in.readInt();
|
||||
confirmations = in.readLong();
|
||||
subaddressLabel = in.readString();
|
||||
transfers = in.readArrayList(Transfer.class.getClassLoader());
|
||||
txKey = in.readString();
|
||||
notes = in.readString();
|
||||
address = in.readString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
@ -183,4 +162,23 @@ public class TransactionInfo implements Parcelable, Comparable<TransactionInfo>
|
||||
return this.hash.compareTo(another.hash);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public enum Direction {
|
||||
Direction_In(0),
|
||||
Direction_Out(1);
|
||||
|
||||
@Getter
|
||||
private final int value;
|
||||
|
||||
public static Direction fromInteger(int n) {
|
||||
switch (n) {
|
||||
case 0:
|
||||
return Direction_In;
|
||||
case 1:
|
||||
return Direction_Out;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -20,20 +20,6 @@ import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
public class Transfer implements Parcelable {
|
||||
public long amount;
|
||||
public String address;
|
||||
|
||||
public Transfer(long amount, String address) {
|
||||
this.amount = amount;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeLong(amount);
|
||||
out.writeString(address);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<Transfer> CREATOR = new Parcelable.Creator<Transfer>() {
|
||||
public Transfer createFromParcel(Parcel in) {
|
||||
return new Transfer(in);
|
||||
@ -43,12 +29,25 @@ public class Transfer implements Parcelable {
|
||||
return new Transfer[size];
|
||||
}
|
||||
};
|
||||
public long amount;
|
||||
public String address;
|
||||
|
||||
public Transfer(long amount, String address) {
|
||||
this.amount = amount;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
private Transfer(Parcel in) {
|
||||
amount = in.readLong();
|
||||
address = in.readString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeLong(amount);
|
||||
out.writeString(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
|
@ -33,53 +33,47 @@ import timber.log.Timber;
|
||||
|
||||
public class Wallet {
|
||||
final static public long SWEEP_ALL = Long.MAX_VALUE;
|
||||
private static final String NEW_ACCOUNT_NAME = "Untitled account"; // src/wallet/wallet2.cpp:941
|
||||
|
||||
static {
|
||||
System.loadLibrary("monerujo");
|
||||
}
|
||||
|
||||
static public class Status {
|
||||
Status(int status, String errorString) {
|
||||
this.status = StatusEnum.values()[status];
|
||||
this.errorString = errorString;
|
||||
}
|
||||
|
||||
final private StatusEnum status;
|
||||
final private String errorString;
|
||||
@Nullable
|
||||
private ConnectionStatus connectionStatus; // optional
|
||||
|
||||
public StatusEnum getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getErrorString() {
|
||||
return errorString;
|
||||
}
|
||||
|
||||
public void setConnectionStatus(@Nullable ConnectionStatus connectionStatus) {
|
||||
this.connectionStatus = connectionStatus;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ConnectionStatus getConnectionStatus() {
|
||||
return connectionStatus;
|
||||
}
|
||||
|
||||
public boolean isOk() {
|
||||
return (getStatus() == StatusEnum.Status_Ok)
|
||||
&& ((getConnectionStatus() == null) ||
|
||||
(getConnectionStatus() == ConnectionStatus.ConnectionStatus_Connected));
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
return "Wallet.Status: " + status + "/" + errorString + "/" + connectionStatus;
|
||||
}
|
||||
}
|
||||
|
||||
boolean synced = false;
|
||||
private int accountIndex = 0;
|
||||
private long handle = 0;
|
||||
private long listenerHandle = 0;
|
||||
private PendingTransaction pendingTransaction = null;
|
||||
private TransactionHistory history = null;
|
||||
|
||||
Wallet(long handle) {
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
Wallet(long handle, int accountIndex) {
|
||||
this.handle = handle;
|
||||
this.accountIndex = accountIndex;
|
||||
}
|
||||
|
||||
public static native String getDisplayAmount(long amount);
|
||||
|
||||
public static native long getAmountFromString(String amount);
|
||||
|
||||
public static native long getAmountFromDouble(double amount);
|
||||
|
||||
public static native String generatePaymentId();
|
||||
|
||||
public static native boolean isPaymentIdValid(String payment_id);
|
||||
|
||||
public static boolean isAddressValid(String address) {
|
||||
return isAddressValid(address, WalletManager.getInstance().getNetworkType().getValue());
|
||||
}
|
||||
|
||||
public static native boolean isAddressValid(String address, int networkType);
|
||||
|
||||
public static native String getPaymentIdFromAddress(String address, int networkType);
|
||||
|
||||
public static native long getMaximumAllowedAmount();
|
||||
|
||||
public int getAccountIndex() {
|
||||
return accountIndex;
|
||||
@ -95,40 +89,6 @@ public class Wallet {
|
||||
return new File(getPath()).getName();
|
||||
}
|
||||
|
||||
private long handle = 0;
|
||||
private long listenerHandle = 0;
|
||||
|
||||
Wallet(long handle) {
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
Wallet(long handle, int accountIndex) {
|
||||
this.handle = handle;
|
||||
this.accountIndex = accountIndex;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public enum Device {
|
||||
Device_Undefined(0, 0),
|
||||
Device_Software(50, 200),
|
||||
Device_Ledger(5, 20);
|
||||
private final int accountLookahead;
|
||||
private final int subaddressLookahead;
|
||||
}
|
||||
|
||||
public enum StatusEnum {
|
||||
Status_Ok,
|
||||
Status_Error,
|
||||
Status_Critical
|
||||
}
|
||||
|
||||
public enum ConnectionStatus {
|
||||
ConnectionStatus_Disconnected,
|
||||
ConnectionStatus_Connected,
|
||||
ConnectionStatus_WrongVersion
|
||||
}
|
||||
|
||||
public native String getSeed(String offset);
|
||||
|
||||
public native String getSeedLanguage();
|
||||
@ -153,6 +113,9 @@ public class Wallet {
|
||||
return getAddress(accountIndex);
|
||||
}
|
||||
|
||||
//TODO virtual void hardForkInfo(uint8_t &version, uint64_t &earliest_height) const = 0;
|
||||
//TODO virtual bool useForkRules(uint8_t version, int64_t early_blocks) const = 0;
|
||||
|
||||
public String getAddress(int accountIndex) {
|
||||
return getAddressJ(accountIndex, 0);
|
||||
}
|
||||
@ -193,19 +156,25 @@ public class Wallet {
|
||||
|
||||
public native int nettype();
|
||||
|
||||
//TODO virtual void hardForkInfo(uint8_t &version, uint64_t &earliest_height) const = 0;
|
||||
//TODO virtual bool useForkRules(uint8_t version, int64_t early_blocks) const = 0;
|
||||
// virtual bool createWatchOnly(const std::string &path, const std::string &password, const std::string &language) const = 0;
|
||||
// virtual void setRefreshFromBlockHeight(uint64_t refresh_from_block_height) = 0;
|
||||
|
||||
public native String getIntegratedAddress(String payment_id);
|
||||
|
||||
public native String getSecretViewKey();
|
||||
|
||||
// virtual void setRecoveringFromSeed(bool recoveringFromSeed) = 0;
|
||||
// virtual bool connectToDaemon() = 0;
|
||||
|
||||
public native String getSecretSpendKey();
|
||||
|
||||
public boolean store() {
|
||||
return store("");
|
||||
}
|
||||
|
||||
//TODO virtual void setTrustedDaemon(bool arg) = 0;
|
||||
//TODO virtual bool trustedDaemon() const = 0;
|
||||
|
||||
public native synchronized boolean store(String path);
|
||||
|
||||
public boolean close() {
|
||||
@ -225,15 +194,9 @@ public class Wallet {
|
||||
private native boolean initJ(String daemon_address, long upper_transaction_size_limit,
|
||||
String daemon_username, String daemon_password, String proxy);
|
||||
|
||||
// virtual bool createWatchOnly(const std::string &path, const std::string &password, const std::string &language) const = 0;
|
||||
// virtual void setRefreshFromBlockHeight(uint64_t refresh_from_block_height) = 0;
|
||||
|
||||
public native void setRestoreHeight(long height);
|
||||
|
||||
public native long getRestoreHeight();
|
||||
|
||||
// virtual void setRecoveringFromSeed(bool recoveringFromSeed) = 0;
|
||||
// virtual bool connectToDaemon() = 0;
|
||||
public native void setRestoreHeight(long height);
|
||||
|
||||
public ConnectionStatus getConnectionStatus() {
|
||||
int s = getConnectionStatusJ();
|
||||
@ -242,9 +205,6 @@ public class Wallet {
|
||||
|
||||
private native int getConnectionStatusJ();
|
||||
|
||||
//TODO virtual void setTrustedDaemon(bool arg) = 0;
|
||||
//TODO virtual bool trustedDaemon() const = 0;
|
||||
|
||||
public native boolean setProxy(String address);
|
||||
|
||||
public long getBalance() {
|
||||
@ -273,8 +233,6 @@ public class Wallet {
|
||||
|
||||
public native long getDaemonBlockChainTargetHeight();
|
||||
|
||||
boolean synced = false;
|
||||
|
||||
public boolean isSynchronized() {
|
||||
return synced;
|
||||
}
|
||||
@ -283,26 +241,6 @@ public class Wallet {
|
||||
this.synced = true;
|
||||
}
|
||||
|
||||
public static native String getDisplayAmount(long amount);
|
||||
|
||||
public static native long getAmountFromString(String amount);
|
||||
|
||||
public static native long getAmountFromDouble(double amount);
|
||||
|
||||
public static native String generatePaymentId();
|
||||
|
||||
public static native boolean isPaymentIdValid(String payment_id);
|
||||
|
||||
public static boolean isAddressValid(String address) {
|
||||
return isAddressValid(address, WalletManager.getInstance().getNetworkType().getValue());
|
||||
}
|
||||
|
||||
public static native boolean isAddressValid(String address, int networkType);
|
||||
|
||||
public static native String getPaymentIdFromAddress(String address, int networkType);
|
||||
|
||||
public static native long getMaximumAllowedAmount();
|
||||
|
||||
public native void startRefresh();
|
||||
|
||||
public native void pauseRefresh();
|
||||
@ -318,16 +256,13 @@ public class Wallet {
|
||||
rescanBlockchainAsyncJ();
|
||||
}
|
||||
|
||||
//TODO virtual void setAutoRefreshInterval(int millis) = 0;
|
||||
//TODO virtual int autoRefreshInterval() const = 0;
|
||||
|
||||
|
||||
private PendingTransaction pendingTransaction = null;
|
||||
|
||||
public PendingTransaction getPendingTransaction() {
|
||||
return pendingTransaction;
|
||||
}
|
||||
|
||||
//TODO virtual void setAutoRefreshInterval(int millis) = 0;
|
||||
//TODO virtual int autoRefreshInterval() const = 0;
|
||||
|
||||
public void disposePendingTransaction() {
|
||||
if (pendingTransaction != null) {
|
||||
disposeTransaction(pendingTransaction);
|
||||
@ -366,7 +301,6 @@ public class Wallet {
|
||||
int mixin_count,
|
||||
int priority, int accountIndex);
|
||||
|
||||
|
||||
public PendingTransaction createSweepUnmixableTransaction() {
|
||||
disposePendingTransaction();
|
||||
long txHandle = createSweepUnmixableTransactionJ();
|
||||
@ -376,19 +310,8 @@ public class Wallet {
|
||||
|
||||
private native long createSweepUnmixableTransactionJ();
|
||||
|
||||
//virtual UnsignedTransaction * loadUnsignedTx(const std::string &unsigned_filename) = 0;
|
||||
//virtual bool submitTransaction(const std::string &fileName) = 0;
|
||||
|
||||
public native void disposeTransaction(PendingTransaction pendingTransaction);
|
||||
|
||||
//virtual bool exportKeyImages(const std::string &filename) = 0;
|
||||
//virtual bool importKeyImages(const std::string &filename) = 0;
|
||||
|
||||
|
||||
//virtual TransactionHistory * history() const = 0;
|
||||
|
||||
private TransactionHistory history = null;
|
||||
|
||||
public TransactionHistory getHistory() {
|
||||
if (history == null) {
|
||||
history = new TransactionHistory(getHistoryJ(), accountIndex);
|
||||
@ -396,15 +319,21 @@ public class Wallet {
|
||||
return history;
|
||||
}
|
||||
|
||||
//virtual UnsignedTransaction * loadUnsignedTx(const std::string &unsigned_filename) = 0;
|
||||
//virtual bool submitTransaction(const std::string &fileName) = 0;
|
||||
|
||||
private native long getHistoryJ();
|
||||
|
||||
//virtual bool exportKeyImages(const std::string &filename) = 0;
|
||||
//virtual bool importKeyImages(const std::string &filename) = 0;
|
||||
|
||||
|
||||
//virtual TransactionHistory * history() const = 0;
|
||||
|
||||
public void refreshHistory() {
|
||||
getHistory().refreshWithNotes(this);
|
||||
}
|
||||
|
||||
//virtual AddressBook * addressBook() const = 0;
|
||||
//virtual void setListener(WalletListener *) = 0;
|
||||
|
||||
private native long setListenerJ(WalletListener listener);
|
||||
|
||||
public void setListener(WalletListener listener) {
|
||||
@ -413,6 +342,9 @@ public class Wallet {
|
||||
|
||||
public native int getDefaultMixin();
|
||||
|
||||
//virtual AddressBook * addressBook() const = 0;
|
||||
//virtual void setListener(WalletListener *) = 0;
|
||||
|
||||
public native void setDefaultMixin(int mixin);
|
||||
|
||||
public native boolean setUserNote(String txid, String note);
|
||||
@ -421,14 +353,6 @@ public class Wallet {
|
||||
|
||||
public native String getTxKey(String txid);
|
||||
|
||||
//virtual std::string signMessage(const std::string &message) = 0;
|
||||
//virtual bool verifySignedMessage(const std::string &message, const std::string &addres, const std::string &signature) const = 0;
|
||||
|
||||
//virtual bool parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &tvAmount, std::string &tx_description, std::string &recipient_name, std::vector<std::string> &unknown_parameters, std::string &error) = 0;
|
||||
//virtual bool rescanSpent() = 0;
|
||||
|
||||
private static final String NEW_ACCOUNT_NAME = "Untitled account"; // src/wallet/wallet2.cpp:941
|
||||
|
||||
public void addAccount() {
|
||||
addAccount(NEW_ACCOUNT_NAME);
|
||||
}
|
||||
@ -439,6 +363,16 @@ public class Wallet {
|
||||
return getAccountLabel(accountIndex);
|
||||
}
|
||||
|
||||
//virtual std::string signMessage(const std::string &message) = 0;
|
||||
//virtual bool verifySignedMessage(const std::string &message, const std::string &addres, const std::string &signature) const = 0;
|
||||
|
||||
//virtual bool parse_uri(const std::string &uri, std::string &address, std::string &payment_id, uint64_t &tvAmount, std::string &tx_description, std::string &recipient_name, std::vector<std::string> &unknown_parameters, std::string &error) = 0;
|
||||
//virtual bool rescanSpent() = 0;
|
||||
|
||||
public void setAccountLabel(String label) {
|
||||
setAccountLabel(accountIndex, label);
|
||||
}
|
||||
|
||||
public String getAccountLabel(int accountIndex) {
|
||||
String label = getSubaddressLabel(accountIndex, 0);
|
||||
if (label.equals(NEW_ACCOUNT_NAME)) {
|
||||
@ -456,10 +390,6 @@ public class Wallet {
|
||||
|
||||
public native String getSubaddressLabel(int accountIndex, int addressIndex);
|
||||
|
||||
public void setAccountLabel(String label) {
|
||||
setAccountLabel(accountIndex, label);
|
||||
}
|
||||
|
||||
public void setAccountLabel(int accountIndex, String label) {
|
||||
setSubaddressLabel(accountIndex, 0, label);
|
||||
}
|
||||
@ -504,4 +434,66 @@ public class Wallet {
|
||||
|
||||
private native int getDeviceTypeJ();
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
public enum Device {
|
||||
Device_Undefined(0, 0),
|
||||
Device_Software(50, 200),
|
||||
Device_Ledger(5, 20);
|
||||
private final int accountLookahead;
|
||||
private final int subaddressLookahead;
|
||||
}
|
||||
|
||||
public enum StatusEnum {
|
||||
Status_Ok,
|
||||
Status_Error,
|
||||
Status_Critical
|
||||
}
|
||||
|
||||
public enum ConnectionStatus {
|
||||
ConnectionStatus_Disconnected,
|
||||
ConnectionStatus_Connected,
|
||||
ConnectionStatus_WrongVersion
|
||||
}
|
||||
|
||||
static public class Status {
|
||||
final private StatusEnum status;
|
||||
final private String errorString;
|
||||
@Nullable
|
||||
private ConnectionStatus connectionStatus; // optional
|
||||
Status(int status, String errorString) {
|
||||
this.status = StatusEnum.values()[status];
|
||||
this.errorString = errorString;
|
||||
}
|
||||
|
||||
public StatusEnum getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getErrorString() {
|
||||
return errorString;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ConnectionStatus getConnectionStatus() {
|
||||
return connectionStatus;
|
||||
}
|
||||
|
||||
public void setConnectionStatus(@Nullable ConnectionStatus connectionStatus) {
|
||||
this.connectionStatus = connectionStatus;
|
||||
}
|
||||
|
||||
public boolean isOk() {
|
||||
return (getStatus() == StatusEnum.Status_Ok)
|
||||
&& ((getConnectionStatus() == null) ||
|
||||
(getConnectionStatus() == ConnectionStatus.ConnectionStatus_Connected));
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String toString() {
|
||||
return "Wallet.Status: " + status + "/" + errorString + "/" + connectionStatus;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,7 +17,6 @@
|
||||
package com.m2049r.xmrwallet.model;
|
||||
|
||||
import com.m2049r.xmrwallet.data.Node;
|
||||
import com.m2049r.xmrwallet.ledger.Ledger;
|
||||
import com.m2049r.xmrwallet.util.RestoreHeight;
|
||||
|
||||
import java.io.File;
|
||||
@ -31,12 +30,26 @@ import timber.log.Timber;
|
||||
|
||||
public class WalletManager {
|
||||
|
||||
//TODO: maybe put these in an enum like in monero core - but why?
|
||||
static public int LOGLEVEL_SILENT = -1;
|
||||
static public int LOGLEVEL_WARN = 0;
|
||||
static public int LOGLEVEL_INFO = 1;
|
||||
static public int LOGLEVEL_DEBUG = 2;
|
||||
static public int LOGLEVEL_TRACE = 3;
|
||||
static public int LOGLEVEL_MAX = 4;
|
||||
// no need to keep a reference to the REAL WalletManager (we get it every tvTime we need it)
|
||||
private static WalletManager Instance = null;
|
||||
|
||||
static {
|
||||
System.loadLibrary("monerujo");
|
||||
}
|
||||
|
||||
// no need to keep a reference to the REAL WalletManager (we get it every tvTime we need it)
|
||||
private static WalletManager Instance = null;
|
||||
private final NetworkType networkType = NetworkType.NetworkType_Mainnet;
|
||||
private Wallet managedWallet = null;
|
||||
private String daemonAddress = null;
|
||||
private String daemonUsername = "";
|
||||
private String daemonPassword = "";
|
||||
private String proxy = "";
|
||||
|
||||
public static synchronized WalletManager getInstance() {
|
||||
if (WalletManager.Instance == null) {
|
||||
@ -46,10 +59,6 @@ public class WalletManager {
|
||||
return WalletManager.Instance;
|
||||
}
|
||||
|
||||
public String addressPrefix() {
|
||||
return addressPrefix(getNetworkType());
|
||||
}
|
||||
|
||||
static public String addressPrefix(NetworkType networkType) {
|
||||
switch (networkType) {
|
||||
case NetworkType_Testnet:
|
||||
@ -63,7 +72,23 @@ public class WalletManager {
|
||||
}
|
||||
}
|
||||
|
||||
private Wallet managedWallet = null;
|
||||
static public native void initLogger(String argv0, String defaultLogBaseName);
|
||||
|
||||
static public native void setLogLevel(int level);
|
||||
|
||||
static public native void logDebug(String category, String message);
|
||||
|
||||
static public native void logInfo(String category, String message);
|
||||
|
||||
static public native void logWarning(String category, String message);
|
||||
|
||||
static public native void logError(String category, String message);
|
||||
|
||||
static public native String moneroVersion();
|
||||
|
||||
public String addressPrefix() {
|
||||
return addressPrefix(getNetworkType());
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return managedWallet;
|
||||
@ -108,6 +133,8 @@ public class WalletManager {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
//public native List<String> findWallets(String path); // this does not work - some error in boost
|
||||
|
||||
private native long createWalletJ(String path, String password, String language, int networkType);
|
||||
|
||||
public Wallet openAccount(String path, int accountIndex, String password) {
|
||||
@ -117,6 +144,8 @@ public class WalletManager {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
//TODO virtual bool checkPayment(const std::string &address, const std::string &txid, const std::string &txkey, const std::string &daemon_address, uint64_t &received, uint64_t &height, std::string &error) const = 0;
|
||||
|
||||
public Wallet openWallet(String path, String password) {
|
||||
long walletHandle = openWalletJ(path, password, getNetworkType().getValue());
|
||||
Wallet wallet = new Wallet(walletHandle);
|
||||
@ -163,7 +192,7 @@ public class WalletManager {
|
||||
String deviceName) {
|
||||
long walletHandle = createWalletFromDeviceJ(aFile.getAbsolutePath(), password,
|
||||
getNetworkType().getValue(), deviceName, restoreHeight,
|
||||
Ledger.SUBADDRESS_LOOKAHEAD);
|
||||
"5:20");
|
||||
Wallet wallet = new Wallet(walletHandle);
|
||||
manageWallet(wallet);
|
||||
return wallet;
|
||||
@ -175,7 +204,6 @@ public class WalletManager {
|
||||
long restoreHeight,
|
||||
String subaddressLookahead);
|
||||
|
||||
|
||||
public native boolean closeJ(Wallet wallet);
|
||||
|
||||
public boolean close(Wallet wallet) {
|
||||
@ -208,25 +236,6 @@ public class WalletManager {
|
||||
|
||||
private native int queryWalletDeviceJ(String keys_file_name, String password);
|
||||
|
||||
//public native List<String> findWallets(String path); // this does not work - some error in boost
|
||||
|
||||
public class WalletInfo implements Comparable<WalletInfo> {
|
||||
@Getter
|
||||
final private File path;
|
||||
@Getter
|
||||
final private String name;
|
||||
|
||||
public WalletInfo(File wallet) {
|
||||
path = wallet.getParentFile();
|
||||
name = wallet.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(WalletInfo another) {
|
||||
return name.toLowerCase().compareTo(another.name.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
public List<WalletInfo> findWallets(File path) {
|
||||
List<WalletInfo> wallets = new ArrayList<>();
|
||||
Timber.d("Scanning: %s", path.getAbsolutePath());
|
||||
@ -243,11 +252,6 @@ public class WalletManager {
|
||||
return wallets;
|
||||
}
|
||||
|
||||
//TODO virtual bool checkPayment(const std::string &address, const std::string &txid, const std::string &txkey, const std::string &daemon_address, uint64_t &received, uint64_t &height, std::string &error) const = 0;
|
||||
|
||||
private String daemonAddress = null;
|
||||
private final NetworkType networkType = NetworkType.NetworkType_Mainnet;
|
||||
|
||||
public NetworkType getNetworkType() {
|
||||
return networkType;
|
||||
}
|
||||
@ -279,20 +283,18 @@ public class WalletManager {
|
||||
|
||||
private native void setDaemonAddressJ(String address);
|
||||
|
||||
private String daemonUsername = "";
|
||||
|
||||
public String getDaemonUsername() {
|
||||
return daemonUsername;
|
||||
}
|
||||
|
||||
private String daemonPassword = "";
|
||||
|
||||
public String getDaemonPassword() {
|
||||
return daemonPassword;
|
||||
}
|
||||
|
||||
public native int getDaemonVersion();
|
||||
|
||||
//TODO static std::tuple<bool, std::string, std::string, std::string, std::string> checkUpdates(const std::string &software, const std::string &subdir);
|
||||
|
||||
public native long getBlockchainHeight();
|
||||
|
||||
public native long getBlockchainTargetHeight();
|
||||
@ -311,8 +313,6 @@ public class WalletManager {
|
||||
|
||||
public native String resolveOpenAlias(String address, boolean dnssec_valid);
|
||||
|
||||
private String proxy = "";
|
||||
|
||||
public String getProxy() {
|
||||
return proxy;
|
||||
}
|
||||
@ -324,27 +324,20 @@ public class WalletManager {
|
||||
|
||||
public native boolean setProxyJ(String address);
|
||||
|
||||
//TODO static std::tuple<bool, std::string, std::string, std::string, std::string> checkUpdates(const std::string &software, const std::string &subdir);
|
||||
public class WalletInfo implements Comparable<WalletInfo> {
|
||||
@Getter
|
||||
final private File path;
|
||||
@Getter
|
||||
final private String name;
|
||||
|
||||
static public native void initLogger(String argv0, String defaultLogBaseName);
|
||||
public WalletInfo(File wallet) {
|
||||
path = wallet.getParentFile();
|
||||
name = wallet.getName();
|
||||
}
|
||||
|
||||
//TODO: maybe put these in an enum like in monero core - but why?
|
||||
static public int LOGLEVEL_SILENT = -1;
|
||||
static public int LOGLEVEL_WARN = 0;
|
||||
static public int LOGLEVEL_INFO = 1;
|
||||
static public int LOGLEVEL_DEBUG = 2;
|
||||
static public int LOGLEVEL_TRACE = 3;
|
||||
static public int LOGLEVEL_MAX = 4;
|
||||
|
||||
static public native void setLogLevel(int level);
|
||||
|
||||
static public native void logDebug(String category, String message);
|
||||
|
||||
static public native void logInfo(String category, String message);
|
||||
|
||||
static public native void logWarning(String category, String message);
|
||||
|
||||
static public native void logError(String category, String message);
|
||||
|
||||
static public native String moneroVersion();
|
||||
@Override
|
||||
public int compareTo(WalletInfo another) {
|
||||
return name.toLowerCase().compareTo(another.name.toLowerCase());
|
||||
}
|
||||
}
|
||||
}
|
@ -5,16 +5,8 @@ import com.m2049r.xmrwallet.model.TransactionInfo;
|
||||
import com.m2049r.xmrwallet.model.Wallet;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class AddressService extends ServiceBase {
|
||||
public static AddressService instance = null;
|
||||
|
||||
public static AddressService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private int latestAddressIndex = 1;
|
||||
|
||||
public AddressService(MoneroHandlerThread thread) {
|
||||
@ -22,9 +14,13 @@ public class AddressService extends ServiceBase {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
public static AddressService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void refreshAddresses() {
|
||||
for (TransactionInfo info : HistoryService.getInstance().getHistory()) {
|
||||
if(info.addressIndex >= latestAddressIndex) {
|
||||
if (info.addressIndex >= latestAddressIndex) {
|
||||
latestAddressIndex = info.addressIndex + 1;
|
||||
}
|
||||
}
|
||||
|
@ -3,26 +3,23 @@ package com.m2049r.xmrwallet.service;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.m2049r.xmrwallet.MainActivity;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
|
||||
public class BalanceService extends ServiceBase {
|
||||
public static BalanceService instance = null;
|
||||
|
||||
public static BalanceService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final MutableLiveData<Long> _balance = new MutableLiveData<>(0L);
|
||||
public LiveData<Long> balance = _balance;
|
||||
private final MutableLiveData<Long> _lockedBalance = new MutableLiveData<>(0L);
|
||||
public LiveData<Long> balance = _balance;
|
||||
public LiveData<Long> lockedBalance = _lockedBalance;
|
||||
|
||||
public BalanceService(MoneroHandlerThread thread) {
|
||||
super(thread);
|
||||
instance = this;
|
||||
}
|
||||
|
||||
public static BalanceService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void refreshBalance() {
|
||||
_balance.postValue(getUnlockedBalanceRaw());
|
||||
_lockedBalance.postValue(getLockedBalanceRaw());
|
||||
|
@ -3,26 +3,23 @@ package com.m2049r.xmrwallet.service;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.m2049r.xmrwallet.MainActivity;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
|
||||
public class BlockchainService extends ServiceBase {
|
||||
public static BlockchainService instance = null;
|
||||
|
||||
public static BlockchainService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private long daemonHeight = 0;
|
||||
private long lastDaemonHeightUpdateTimeMs = 0;
|
||||
private final MutableLiveData<Long> _currentHeight = new MutableLiveData<>(0L);
|
||||
public LiveData<Long> height = _currentHeight;
|
||||
|
||||
private long daemonHeight = 0;
|
||||
private long lastDaemonHeightUpdateTimeMs = 0;
|
||||
public BlockchainService(MoneroHandlerThread thread) {
|
||||
super(thread);
|
||||
instance = this;
|
||||
}
|
||||
|
||||
public static BlockchainService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void refreshBlockchain() {
|
||||
_currentHeight.postValue(getCurrentHeight());
|
||||
}
|
||||
@ -37,11 +34,11 @@ public class BlockchainService extends ServiceBase {
|
||||
|
||||
public void setDaemonHeight(long height) {
|
||||
long t = System.currentTimeMillis();
|
||||
if(height > 0) {
|
||||
if (height > 0) {
|
||||
daemonHeight = height;
|
||||
lastDaemonHeightUpdateTimeMs = t;
|
||||
} else {
|
||||
if(t - lastDaemonHeightUpdateTimeMs > 120000) {
|
||||
if (t - lastDaemonHeightUpdateTimeMs > 120000) {
|
||||
daemonHeight = WalletManager.getInstance().getWallet().getDaemonBlockChainHeight();
|
||||
lastDaemonHeightUpdateTimeMs = t;
|
||||
}
|
||||
|
@ -2,25 +2,25 @@ package com.m2049r.xmrwallet.service;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import com.m2049r.xmrwallet.model.TransactionInfo;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class HistoryService extends ServiceBase {
|
||||
public static HistoryService instance = null;
|
||||
|
||||
public static HistoryService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private final MutableLiveData<List<TransactionInfo>> _history = new MutableLiveData<>();
|
||||
public LiveData<List<TransactionInfo>> history = _history;
|
||||
|
||||
public HistoryService(MoneroHandlerThread thread) {
|
||||
super(thread);
|
||||
instance = this;
|
||||
}
|
||||
|
||||
public static HistoryService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void refreshHistory() {
|
||||
_history.postValue(getHistory());
|
||||
}
|
||||
|
@ -28,8 +28,6 @@ import com.m2049r.xmrwallet.model.WalletListener;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
import com.m2049r.xmrwallet.util.Constants;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
|
||||
/**
|
||||
* Handy class for starting a new thread that has a looper. The looper can then be
|
||||
@ -37,10 +35,11 @@ import java.io.File;
|
||||
* The started Thread has a stck size of STACK_SIZE (=5MB)
|
||||
*/
|
||||
public class MoneroHandlerThread extends Thread implements WalletListener {
|
||||
private Listener listener = null;
|
||||
// from src/cryptonote_config.h
|
||||
static public final long THREAD_STACK_SIZE = 5 * 1024 * 1024;
|
||||
private Wallet wallet;
|
||||
int triesLeft = 5;
|
||||
private Listener listener = null;
|
||||
private final Wallet wallet;
|
||||
|
||||
public MoneroHandlerThread(String name, Listener listener, Wallet wallet) {
|
||||
super(null, null, name, THREAD_STACK_SIZE);
|
||||
@ -57,7 +56,7 @@ public class MoneroHandlerThread extends Thread implements WalletListener {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean usesTor = PrefService.getInstance().getBoolean(Constants.PREF_USES_TOR, false);
|
||||
if(usesTor) {
|
||||
if (usesTor) {
|
||||
String proxy = "127.0.0.1:9050";
|
||||
WalletManager.getInstance().setProxy(proxy);
|
||||
WalletManager.getInstance().setDaemon(Node.fromString(DefaultNodes.boldsuck.getUri()));
|
||||
@ -93,13 +92,11 @@ public class MoneroHandlerThread extends Thread implements WalletListener {
|
||||
refresh();
|
||||
}
|
||||
|
||||
int triesLeft = 5;
|
||||
|
||||
@Override
|
||||
public void refreshed() {
|
||||
Wallet.ConnectionStatus status = wallet.getFullStatus().getConnectionStatus();
|
||||
if(status == Wallet.ConnectionStatus.ConnectionStatus_Disconnected || status == null) {
|
||||
if(triesLeft > 0) {
|
||||
if (status == Wallet.ConnectionStatus.ConnectionStatus_Disconnected || status == null) {
|
||||
if (triesLeft > 0) {
|
||||
wallet.startRefresh();
|
||||
triesLeft--;
|
||||
} else {
|
||||
@ -129,6 +126,7 @@ public class MoneroHandlerThread extends Thread implements WalletListener {
|
||||
|
||||
public interface Listener {
|
||||
void onRefresh();
|
||||
|
||||
void onConnectionFail();
|
||||
}
|
||||
}
|
||||
|
@ -3,18 +3,17 @@ package com.m2049r.xmrwallet.service;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.m2049r.xmrwallet.MainActivity;
|
||||
import com.m2049r.xmrwallet.MoneroApplication;
|
||||
|
||||
public class PrefService extends ServiceBase {
|
||||
public static SharedPreferences instance = null;
|
||||
|
||||
public static SharedPreferences getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public PrefService(MoneroApplication application) {
|
||||
super(null);
|
||||
instance = application.getSharedPreferences(application.getApplicationInfo().packageName, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public static SharedPreferences getInstance() {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,19 @@
|
||||
package com.m2049r.xmrwallet.service;
|
||||
|
||||
import com.m2049r.xmrwallet.MainActivity;
|
||||
import com.m2049r.xmrwallet.livedata.SingleLiveEvent;
|
||||
import com.m2049r.xmrwallet.model.PendingTransaction;
|
||||
|
||||
public class TxService extends ServiceBase {
|
||||
public static TxService instance = null;
|
||||
|
||||
public static TxService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public TxService(MoneroHandlerThread thread) {
|
||||
super(thread);
|
||||
instance = this;
|
||||
}
|
||||
|
||||
public static TxService getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public PendingTransaction createTx(String address, String amount, boolean sendAll, PendingTransaction.Priority feePriority) {
|
||||
return this.getThread().createTx(address, amount, sendAll, feePriority);
|
||||
}
|
||||
|
@ -1,68 +0,0 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class CrazyPassEncoder {
|
||||
static final String BASE = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
|
||||
static final int PW_CHARS = 52;
|
||||
|
||||
// this takes a 32 byte buffer and converts it to 52 alphnumeric characters
|
||||
// separated by blanks every 4 characters = 13 groups of 4
|
||||
// always (padding by Xs if need be
|
||||
static public String encode(byte[] data) {
|
||||
if (data.length != 32) throw new IllegalArgumentException("data[] is not 32 bytes long");
|
||||
BigInteger rest = new BigInteger(1, data);
|
||||
BigInteger remainder;
|
||||
final StringBuilder result = new StringBuilder();
|
||||
final BigInteger base = BigInteger.valueOf(BASE.length());
|
||||
int i = 0;
|
||||
do {
|
||||
if ((i > 0) && (i % 4 == 0)) result.append(' ');
|
||||
i++;
|
||||
remainder = rest.remainder(base);
|
||||
rest = rest.divide(base);
|
||||
result.append(BASE.charAt(remainder.intValue()));
|
||||
} while (!BigInteger.ZERO.equals(rest));
|
||||
// pad it
|
||||
while (i < PW_CHARS) {
|
||||
if ((i > 0) && (i % 4 == 0)) result.append(' ');
|
||||
result.append('2');
|
||||
i++;
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
static public String reformat(String password) {
|
||||
// maybe this is a CrAzYpass without blanks? or lowercase letters
|
||||
String noBlanks = password.toUpperCase().replaceAll(" ", "");
|
||||
if (noBlanks.length() == PW_CHARS) { // looks like a CrAzYpass
|
||||
// insert blanks every 4 characters
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < PW_CHARS; i++) {
|
||||
if ((i > 0) && (i % 4 == 0)) sb.append(' ');
|
||||
char c = noBlanks.charAt(i);
|
||||
if (BASE.indexOf(c) < 0) return null; // invalid character found
|
||||
sb.append(c);
|
||||
}
|
||||
return sb.toString();
|
||||
} else {
|
||||
return null; // not a CrAzYpass
|
||||
}
|
||||
}
|
||||
}
|
@ -31,10 +31,6 @@ public enum DayNightMode {
|
||||
this.nightMode = nightMode;
|
||||
}
|
||||
|
||||
public int getNightMode() {
|
||||
return nightMode;
|
||||
}
|
||||
|
||||
static public DayNightMode getValue(int nightMode) {
|
||||
for (DayNightMode mode : DayNightMode.values()) {
|
||||
if (mode.nightMode == nightMode)
|
||||
@ -42,4 +38,8 @@ public enum DayNightMode {
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
public int getNightMode() {
|
||||
return nightMode;
|
||||
}
|
||||
}
|
||||
|
@ -30,10 +30,6 @@ import android.graphics.Canvas;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.VectorDrawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.StrictMode;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.Animation;
|
||||
@ -42,12 +38,9 @@ import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.m2049r.xmrwallet.BuildConfig;
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.data.Crypto;
|
||||
import com.m2049r.xmrwallet.model.WalletManager;
|
||||
|
||||
import java.io.File;
|
||||
@ -67,17 +60,18 @@ 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 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 final int PERMISSIONS_REQUEST_CAMERA = 7;
|
||||
static final int HTTP_TIMEOUT = 5000;
|
||||
static private final String WALLET_DIR = "wallets";
|
||||
static private final String MONERO_DIR = "monero";
|
||||
|
||||
private final static char[] HexArray = "0123456789ABCDEF".toCharArray();
|
||||
static public boolean ALLOW_SHIFT = false;
|
||||
static public int DISPLAY_DIGITS_INFO = 5;
|
||||
static private Animation ShakeAnimation;
|
||||
|
||||
static public File getWalletRoot(Context context) {
|
||||
return getStorage(context, WALLET_DIR);
|
||||
@ -97,8 +91,6 @@ public class Helper {
|
||||
return dir;
|
||||
}
|
||||
|
||||
static public final int PERMISSIONS_REQUEST_CAMERA = 7;
|
||||
|
||||
static public boolean getCameraPermission(Activity context) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
if (context.checkSelfPermission(Manifest.permission.CAMERA)
|
||||
@ -227,8 +219,6 @@ public class Helper {
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
static final int HTTP_TIMEOUT = 5000;
|
||||
|
||||
static public String getUrl(String httpsUrl) {
|
||||
HttpsURLConnection urlConnection = null;
|
||||
try {
|
||||
@ -261,7 +251,7 @@ public class Helper {
|
||||
}
|
||||
|
||||
static public void clipBoardCopy(Context context, String label, String text) {
|
||||
if(context != null) {
|
||||
if (context != null) {
|
||||
ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText(label, text);
|
||||
clipboardManager.setPrimaryClip(clip);
|
||||
@ -284,8 +274,6 @@ public class Helper {
|
||||
return null;
|
||||
}
|
||||
|
||||
static private Animation ShakeAnimation;
|
||||
|
||||
static public Animation getShakeAnimation(Context context) {
|
||||
if (ShakeAnimation == null) {
|
||||
synchronized (Helper.class) {
|
||||
@ -297,8 +285,6 @@ public class Helper {
|
||||
return ShakeAnimation;
|
||||
}
|
||||
|
||||
private final static char[] HexArray = "0123456789ABCDEF".toCharArray();
|
||||
|
||||
public static String bytesToHex(byte[] data) {
|
||||
if ((data != null) && (data.length > 0))
|
||||
return String.format("%0" + (data.length * 2) + "X", new BigInteger(1, data));
|
||||
|
@ -82,16 +82,6 @@ public class KeyStoreHelper {
|
||||
}
|
||||
}
|
||||
|
||||
static public class BrokenPasswordStoreException extends Exception {
|
||||
BrokenPasswordStoreException() {
|
||||
super();
|
||||
}
|
||||
|
||||
BrokenPasswordStoreException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a public and private key and stores it using the Android Key
|
||||
* Store, so that only this application will be able to access the keys.
|
||||
@ -267,4 +257,14 @@ public class KeyStoreHelper {
|
||||
String WALLET_PASS_PREFS_NAME = "wallet";
|
||||
String WALLET_PASS_KEY_PREFIX = "walletKey-";
|
||||
}
|
||||
|
||||
static public class BrokenPasswordStoreException extends Exception {
|
||||
BrokenPasswordStoreException() {
|
||||
super();
|
||||
}
|
||||
|
||||
BrokenPasswordStoreException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
}
|
@ -24,6 +24,8 @@ import timber.log.Timber;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class LegacyStorageHelper {
|
||||
private static final Pattern WALLET_PATTERN = Pattern.compile("^(.+) \\(([0-9]+)\\).keys$");
|
||||
private static final String MIGRATED_KEY = "migrated_legacy_storage";
|
||||
final private File srcDir;
|
||||
final private File dstDir;
|
||||
|
||||
@ -49,6 +51,70 @@ public class LegacyStorageHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isExternalStorageWritable() {
|
||||
String state = Environment.getExternalStorageState();
|
||||
return Environment.MEDIA_MOUNTED.equals(state);
|
||||
}
|
||||
|
||||
private static File getWalletRoot() {
|
||||
if (!isExternalStorageWritable())
|
||||
throw new IllegalStateException();
|
||||
|
||||
// wallet folder for legacy (pre-Q) installations
|
||||
final String FLAVOR_SUFFIX =
|
||||
(BuildConfig.FLAVOR.startsWith("prod") ? "" : "." + BuildConfig.FLAVOR)
|
||||
+ (BuildConfig.DEBUG ? "-debug" : "");
|
||||
final String WALLET_DIR = "monerujo" + FLAVOR_SUFFIX;
|
||||
|
||||
File dir = new File(Environment.getExternalStorageDirectory(), WALLET_DIR);
|
||||
if (!dir.exists() || !dir.isDirectory())
|
||||
throw new IllegalStateException();
|
||||
return dir;
|
||||
}
|
||||
|
||||
private static boolean hasReadPermission(Context context) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
return context.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_DENIED;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getUniqueName(File root, String name) {
|
||||
if (!(new File(root, name + ".keys")).exists()) // <name> does not exist => it's ok to use
|
||||
return name;
|
||||
|
||||
File[] wallets = root.listFiles(
|
||||
(dir, filename) -> {
|
||||
Matcher m = WALLET_PATTERN.matcher(filename);
|
||||
if (m.find())
|
||||
return m.group(1).equals(name);
|
||||
else return false;
|
||||
});
|
||||
if (wallets.length == 0) return name + " (1)";
|
||||
int maxIndex = 0;
|
||||
for (File wallet : wallets) {
|
||||
try {
|
||||
final Matcher m = WALLET_PATTERN.matcher(wallet.getName());
|
||||
if (!m.find())
|
||||
throw new IllegalStateException("this must match as it did before");
|
||||
final int index = Integer.parseInt(m.group(2));
|
||||
if (index > maxIndex) maxIndex = index;
|
||||
} catch (NumberFormatException ex) {
|
||||
// this cannot happen & we can ignore it if it does
|
||||
}
|
||||
}
|
||||
return name + " (" + (maxIndex + 1) + ")";
|
||||
}
|
||||
|
||||
public static boolean isStorageMigrated(Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(MIGRATED_KEY, false);
|
||||
}
|
||||
|
||||
public static void setStorageMigrated(Context context) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(MIGRATED_KEY, true).apply();
|
||||
}
|
||||
|
||||
public void migrate() {
|
||||
String addressPrefix = WalletManager.getInstance().addressPrefix();
|
||||
File[] wallets = srcDir.listFiles((dir, filename) -> filename.endsWith(".keys"));
|
||||
@ -99,72 +165,4 @@ public class LegacyStorageHelper {
|
||||
inChannel.transferTo(0, inChannel.size(), outChannel);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isExternalStorageWritable() {
|
||||
String state = Environment.getExternalStorageState();
|
||||
return Environment.MEDIA_MOUNTED.equals(state);
|
||||
}
|
||||
|
||||
private static File getWalletRoot() {
|
||||
if (!isExternalStorageWritable())
|
||||
throw new IllegalStateException();
|
||||
|
||||
// wallet folder for legacy (pre-Q) installations
|
||||
final String FLAVOR_SUFFIX =
|
||||
(BuildConfig.FLAVOR.startsWith("prod") ? "" : "." + BuildConfig.FLAVOR)
|
||||
+ (BuildConfig.DEBUG ? "-debug" : "");
|
||||
final String WALLET_DIR = "monerujo" + FLAVOR_SUFFIX;
|
||||
|
||||
File dir = new File(Environment.getExternalStorageDirectory(), WALLET_DIR);
|
||||
if (!dir.exists() || !dir.isDirectory())
|
||||
throw new IllegalStateException();
|
||||
return dir;
|
||||
}
|
||||
|
||||
private static boolean hasReadPermission(Context context) {
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
|
||||
return context.checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_DENIED;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Pattern WALLET_PATTERN = Pattern.compile("^(.+) \\(([0-9]+)\\).keys$");
|
||||
|
||||
private static String getUniqueName(File root, String name) {
|
||||
if (!(new File(root, name + ".keys")).exists()) // <name> does not exist => it's ok to use
|
||||
return name;
|
||||
|
||||
File[] wallets = root.listFiles(
|
||||
(dir, filename) -> {
|
||||
Matcher m = WALLET_PATTERN.matcher(filename);
|
||||
if (m.find())
|
||||
return m.group(1).equals(name);
|
||||
else return false;
|
||||
});
|
||||
if (wallets.length == 0) return name + " (1)";
|
||||
int maxIndex = 0;
|
||||
for (File wallet : wallets) {
|
||||
try {
|
||||
final Matcher m = WALLET_PATTERN.matcher(wallet.getName());
|
||||
if (!m.find())
|
||||
throw new IllegalStateException("this must match as it did before");
|
||||
final int index = Integer.parseInt(m.group(2));
|
||||
if (index > maxIndex) maxIndex = index;
|
||||
} catch (NumberFormatException ex) {
|
||||
// this cannot happen & we can ignore it if it does
|
||||
}
|
||||
}
|
||||
return name + " (" + (maxIndex + 1) + ")";
|
||||
}
|
||||
|
||||
private static final String MIGRATED_KEY = "migrated_legacy_storage";
|
||||
|
||||
public static boolean isStorageMigrated(Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(MIGRATED_KEY, false);
|
||||
}
|
||||
|
||||
public static void setStorageMigrated(Context context) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(MIGRATED_KEY, true).apply();
|
||||
}
|
||||
}
|
||||
|
@ -28,11 +28,11 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
|
||||
public class MoneroThreadPoolExecutor {
|
||||
public static final Executor MONERO_THREAD_POOL_EXECUTOR;
|
||||
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
|
||||
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
|
||||
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
|
||||
private static final int KEEP_ALIVE_SECONDS = 30;
|
||||
|
||||
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
|
||||
private final AtomicInteger mCount = new AtomicInteger(1);
|
||||
|
||||
@ -40,12 +40,9 @@ public class MoneroThreadPoolExecutor {
|
||||
return new Thread(null, r, "MoneroTask #" + mCount.getAndIncrement(), MoneroHandlerThread.THREAD_STACK_SIZE);
|
||||
}
|
||||
};
|
||||
|
||||
private static final BlockingQueue<Runnable> sPoolWorkQueue =
|
||||
new LinkedBlockingQueue<>(128);
|
||||
|
||||
public static final Executor MONERO_THREAD_POOL_EXECUTOR;
|
||||
|
||||
static {
|
||||
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
|
||||
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
|
||||
|
@ -1,393 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2021 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.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
|
||||
import com.burgstaller.okhttp.AuthenticationCacheInterceptor;
|
||||
import com.burgstaller.okhttp.CachingAuthenticatorDecorator;
|
||||
import com.burgstaller.okhttp.digest.CachingAuthenticator;
|
||||
import com.burgstaller.okhttp.digest.Credentials;
|
||||
import com.burgstaller.okhttp.digest.DigestAuthenticator;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
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.SignatureUtils;
|
||||
import info.guardianproject.netcipher.proxy.StatusCallback;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.ToString;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Callback;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
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 = 1000; //ms
|
||||
public static final int TOR_TIMEOUT_CONNECT = 5000; //ms
|
||||
public static final int TOR_TIMEOUT = 2000; //ms
|
||||
|
||||
public interface OnStatusChangedListener {
|
||||
void connected();
|
||||
|
||||
void disconnected();
|
||||
|
||||
void notInstalled();
|
||||
|
||||
void notEnabled();
|
||||
}
|
||||
|
||||
final private Context context;
|
||||
final private OrbotHelper orbot;
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private static NetCipherHelper Instance;
|
||||
|
||||
public static void createInstance(Context context) {
|
||||
if (Instance == null) {
|
||||
synchronized (NetCipherHelper.class) {
|
||||
if (Instance == null) {
|
||||
final Context applicationContext = context.getApplicationContext();
|
||||
Instance = new NetCipherHelper(applicationContext, OrbotHelper.get(context).statusTimeout(5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static NetCipherHelper getInstance() {
|
||||
if (Instance == null) throw new IllegalStateException("NetCipherHelper is null");
|
||||
return Instance;
|
||||
}
|
||||
|
||||
private OkHttpClient client;
|
||||
|
||||
private void createTorClient(Intent statusIntent) {
|
||||
String orbotStatus = statusIntent.getStringExtra(OrbotHelper.EXTRA_STATUS);
|
||||
if (orbotStatus == null) throw new IllegalStateException("status is null");
|
||||
if (!orbotStatus.equals(OrbotHelper.STATUS_ON))
|
||||
throw new IllegalStateException("Orbot is not ON");
|
||||
try {
|
||||
final OkHttpClient.Builder okBuilder = new OkHttpClient.Builder()
|
||||
.connectTimeout(TOR_TIMEOUT_CONNECT, TimeUnit.MILLISECONDS)
|
||||
.writeTimeout(TOR_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(TOR_TIMEOUT, TimeUnit.MILLISECONDS);
|
||||
client = new StrongOkHttpClientBuilder(context)
|
||||
.withSocksProxy()
|
||||
.applyTo(okBuilder, statusIntent)
|
||||
.build();
|
||||
Helper.ALLOW_SHIFT = false; // no shifting with Tor
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void createClearnetClient() {
|
||||
try {
|
||||
client = new OkHttpClient.Builder()
|
||||
.connectTimeout(HTTP_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.writeTimeout(HTTP_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(HTTP_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||
.build();
|
||||
Helper.ALLOW_SHIFT = true;
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private OnStatusChangedListener onStatusChangedListener;
|
||||
|
||||
public static void deregister() {
|
||||
getInstance().onStatusChangedListener = null;
|
||||
}
|
||||
|
||||
public static void register(OnStatusChangedListener listener) {
|
||||
final NetCipherHelper me = getInstance();
|
||||
me.onStatusChangedListener = listener;
|
||||
|
||||
// NOT_INSTALLED is dealt with through the callbacks
|
||||
me.orbot.removeStatusCallback(me) // make sure we are registered just once
|
||||
.addStatusCallback(me);
|
||||
|
||||
// deal with org.torproject.android.intent.action.STATUS = STARTS_DISABLED
|
||||
me.context.registerReceiver(orbotStatusReceiver, new IntentFilter(OrbotHelper.ACTION_STATUS));
|
||||
|
||||
me.startTor();
|
||||
}
|
||||
|
||||
// for StatusCallback
|
||||
public enum Status {
|
||||
STARTING,
|
||||
ENABLED,
|
||||
STOPPING,
|
||||
DISABLED,
|
||||
NOT_INSTALLED,
|
||||
NOT_ENABLED,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
private Status status = Status.UNKNOWN;
|
||||
|
||||
@Override
|
||||
public void onStarting() {
|
||||
Timber.d("onStarting");
|
||||
status = Status.STARTING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnabled(Intent statusIntent) {
|
||||
Timber.d("onEnabled");
|
||||
if (getTorPref() != Status.ENABLED) return; // do we want Tor?
|
||||
createTorClient(statusIntent);
|
||||
status = Status.ENABLED;
|
||||
if (onStatusChangedListener != null) {
|
||||
new Thread(() -> onStatusChangedListener.connected()).start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopping() {
|
||||
Timber.d("onStopping");
|
||||
status = Status.STOPPING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisabled() {
|
||||
Timber.d("onDisabled");
|
||||
createClearnetClient();
|
||||
status = Status.DISABLED;
|
||||
if (onStatusChangedListener != null) {
|
||||
new Thread(() -> onStatusChangedListener.disconnected()).start();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusTimeout() {
|
||||
Timber.d("onStatusTimeout");
|
||||
createClearnetClient();
|
||||
// (timeout does not not change the status)
|
||||
if (onStatusChangedListener != null) {
|
||||
new Thread(() -> onStatusChangedListener.disconnected()).start();
|
||||
}
|
||||
orbotInit = false; // do init() next time we try to open Tor
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotYetInstalled() {
|
||||
Timber.d("onNotYetInstalled");
|
||||
// never mind then
|
||||
orbot.removeStatusCallback(this);
|
||||
createClearnetClient();
|
||||
status = Status.NOT_INSTALLED;
|
||||
if (onStatusChangedListener != null) {
|
||||
new Thread(() -> onStatusChangedListener.notInstalled()).start();
|
||||
}
|
||||
}
|
||||
|
||||
// user has not enabled background Orbot starts
|
||||
public void onNotEnabled() {
|
||||
Timber.d("onNotEnabled");
|
||||
// keep the callback in case they turn it on manually
|
||||
setTorPref(Status.DISABLED);
|
||||
createClearnetClient();
|
||||
status = Status.NOT_ENABLED;
|
||||
if (onStatusChangedListener != null) {
|
||||
new Thread(() -> onStatusChangedListener.notEnabled()).start();
|
||||
}
|
||||
}
|
||||
|
||||
static public Status getStatus() {
|
||||
return getInstance().status;
|
||||
}
|
||||
|
||||
public void toggle() {
|
||||
switch (getStatus()) {
|
||||
case ENABLED:
|
||||
onDisabled();
|
||||
setTorPref(Status.DISABLED);
|
||||
break;
|
||||
case DISABLED:
|
||||
setTorPref(Status.ENABLED);
|
||||
startTor();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean orbotInit = false;
|
||||
|
||||
private void startTor() {
|
||||
if (!isOrbotInstalled()) {
|
||||
onNotYetInstalled();
|
||||
} else if (getTorPref() == Status.DISABLED) {
|
||||
onDisabled();
|
||||
} else if (!orbotInit) {
|
||||
orbotInit = orbot.init();
|
||||
} else {
|
||||
orbot.requestStart(context);
|
||||
}
|
||||
}
|
||||
|
||||
// extracted from OrbotHelper
|
||||
private boolean isOrbotInstalled() {
|
||||
ArrayList<String> hashes = new ArrayList<>();
|
||||
// Tor Project signing key
|
||||
hashes.add("A4:54:B8:7A:18:47:A8:9E:D7:F5:E7:0F:BA:6B:BA:96:F3:EF:29:C2:6E:09:81:20:4F:E3:47:BF:23:1D:FD:5B");
|
||||
// f-droid.org signing key
|
||||
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),
|
||||
hashes, false);
|
||||
}
|
||||
|
||||
|
||||
static public boolean hasClient() {
|
||||
return getInstance().client != null;
|
||||
}
|
||||
|
||||
static public boolean isTor() {
|
||||
return getStatus() == Status.ENABLED;
|
||||
}
|
||||
|
||||
static public String getProxy() {
|
||||
if (!isTor()) return "";
|
||||
final Proxy proxy = getInstance().client.proxy();
|
||||
if (proxy == null) return "";
|
||||
return proxy.address().toString().substring(1);
|
||||
}
|
||||
|
||||
@ToString
|
||||
static public class Request {
|
||||
final HttpUrl url;
|
||||
final String json;
|
||||
final String username;
|
||||
final String password;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public Request(final HttpUrl url, final JSONObject json) {
|
||||
this(url, json == null ? null : json.toString(), null, null);
|
||||
}
|
||||
|
||||
public Request(final HttpUrl url) {
|
||||
this(url, null, null, null);
|
||||
}
|
||||
|
||||
public void enqueue(Callback callback) {
|
||||
newCall().enqueue(callback);
|
||||
}
|
||||
|
||||
public Response execute() throws IOException {
|
||||
return newCall().execute();
|
||||
}
|
||||
|
||||
private Call newCall() {
|
||||
return getClient().newCall(getRequest());
|
||||
}
|
||||
|
||||
private OkHttpClient getClient() {
|
||||
if (mockClient != null) return mockClient; // Unit-test mode
|
||||
final OkHttpClient client = getInstance().client;
|
||||
if ((username != null) && (!username.isEmpty())) {
|
||||
final DigestAuthenticator authenticator = new DigestAuthenticator(new Credentials(username, password));
|
||||
final Map<String, CachingAuthenticator> authCache = new ConcurrentHashMap<>();
|
||||
return client.newBuilder()
|
||||
.authenticator(new CachingAuthenticatorDecorator(authenticator, authCache))
|
||||
.addInterceptor(new AuthenticationCacheInterceptor(authCache))
|
||||
.build();
|
||||
// TODO: maybe cache & reuse the client for these credentials?
|
||||
} else {
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
private okhttp3.Request getRequest() {
|
||||
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();
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
// for unit tests only
|
||||
static public OkHttpClient mockClient = null;
|
||||
}
|
||||
|
||||
private static final String PREFS_NAME = "tor";
|
||||
private static final String PREFS_STATUS = "status";
|
||||
private Status currentPref = Status.UNKNOWN;
|
||||
|
||||
private Status getTorPref() {
|
||||
if (currentPref != Status.UNKNOWN) return currentPref;
|
||||
currentPref = Status.valueOf(context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.getString(PREFS_STATUS, "DISABLED"));
|
||||
return currentPref;
|
||||
}
|
||||
|
||||
private void setTorPref(Status status) {
|
||||
if (getTorPref() == status) return; // no change
|
||||
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
.edit()
|
||||
.putString(PREFS_STATUS, status.name())
|
||||
.apply();
|
||||
currentPref = status;
|
||||
}
|
||||
|
||||
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))) {
|
||||
getInstance().onNotEnabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public void installOrbot(Activity host) {
|
||||
host.startActivity(OrbotHelper.getOrbotInstallIntent(context));
|
||||
}
|
||||
}
|
@ -17,12 +17,9 @@
|
||||
package com.m2049r.xmrwallet.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
|
||||
import com.m2049r.xmrwallet.R;
|
||||
import com.m2049r.xmrwallet.service.PrefService;
|
||||
|
||||
public class NightmodeHelper {
|
||||
|
@ -1,53 +0,0 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import com.m2049r.xmrwallet.data.NodeInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class NodePinger {
|
||||
static final public int NUM_THREADS = 10;
|
||||
static final public long MAX_TIME = 5L; // seconds
|
||||
|
||||
public interface Listener {
|
||||
void publish(NodeInfo node);
|
||||
}
|
||||
|
||||
static public void execute(Collection<NodeInfo> nodes, final Listener listener) {
|
||||
final ExecutorService exeService = Executors.newFixedThreadPool(NUM_THREADS);
|
||||
List<Callable<Boolean>> taskList = new ArrayList<>();
|
||||
for (NodeInfo node : nodes) {
|
||||
taskList.add(() -> node.testRpcService(listener));
|
||||
}
|
||||
|
||||
try {
|
||||
exeService.invokeAll(taskList, MAX_TIME, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException ex) {
|
||||
Timber.w(ex);
|
||||
}
|
||||
exeService.shutdownNow();
|
||||
}
|
||||
}
|
@ -1,245 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Specs from https://openalias.org/
|
||||
|
||||
package com.m2049r.xmrwallet.util;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import com.m2049r.xmrwallet.data.BarcodeData;
|
||||
import com.m2049r.xmrwallet.data.Crypto;
|
||||
|
||||
import org.jitsi.dnssec.validator.ValidatingResolver;
|
||||
import org.xbill.DNS.DClass;
|
||||
import org.xbill.DNS.Flags;
|
||||
import org.xbill.DNS.Message;
|
||||
import org.xbill.DNS.Name;
|
||||
import org.xbill.DNS.RRset;
|
||||
import org.xbill.DNS.Rcode;
|
||||
import org.xbill.DNS.Record;
|
||||
import org.xbill.DNS.Section;
|
||||
import org.xbill.DNS.SimpleResolver;
|
||||
import org.xbill.DNS.TXTRecord;
|
||||
import org.xbill.DNS.Type;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
public class OpenAliasHelper {
|
||||
public static final String OA1_SCHEME = "oa1:";
|
||||
public static final String OA1_ASSET = "asset";
|
||||
public static final String OA1_ADDRESS = "recipient_address";
|
||||
public static final String OA1_NAME = "recipient_name";
|
||||
public static final String OA1_DESCRIPTION = "tx_description";
|
||||
public static final String OA1_AMOUNT = "tx_amount";
|
||||
|
||||
public static final int DNS_LOOKUP_TIMEOUT = 2500; // ms
|
||||
|
||||
public static void resolve(String name, OnResolvedListener resolvedListener) {
|
||||
new DnsTxtResolver(resolvedListener).execute(name);
|
||||
}
|
||||
|
||||
public static Map<String, String> parse(String oaString) {
|
||||
return new OpenAliasParser(oaString).parse();
|
||||
}
|
||||
|
||||
public interface OnResolvedListener {
|
||||
void onResolved(Map<Crypto, BarcodeData> dataMap);
|
||||
|
||||
void onFailure();
|
||||
}
|
||||
|
||||
private static class DnsTxtResolver extends AsyncTask<String, Void, Boolean> {
|
||||
List<String> txts = new ArrayList<>();
|
||||
boolean dnssec = false;
|
||||
|
||||
private final OnResolvedListener resolvedListener;
|
||||
|
||||
private DnsTxtResolver(OnResolvedListener resolvedListener) {
|
||||
this.resolvedListener = resolvedListener;
|
||||
}
|
||||
|
||||
// trust anchor of the root zone
|
||||
// http://data.iana.org/root-anchors/root-anchors.xml
|
||||
final String ROOT =
|
||||
". IN DS 19036 8 2 49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5\n" +
|
||||
". IN DS 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D";
|
||||
final String[] DNSSEC_SERVERS = {
|
||||
"4.2.2.1", // Level3
|
||||
"4.2.2.2", // Level3
|
||||
"4.2.2.6", // Level3
|
||||
"1.1.1.1", // cloudflare
|
||||
"9.9.9.9", // quad9
|
||||
"8.8.4.4", // google
|
||||
"8.8.8.8" // google
|
||||
};
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(String... args) {
|
||||
//main();
|
||||
if (args.length != 1) return false;
|
||||
String name = args[0];
|
||||
if ((name == null) || (name.isEmpty()))
|
||||
return false; //pointless trying to lookup nothing
|
||||
Timber.d("Resolving %s", name);
|
||||
try {
|
||||
SimpleResolver sr = new SimpleResolver(DNSSEC_SERVERS[new Random().nextInt(DNSSEC_SERVERS.length)]);
|
||||
ValidatingResolver vr = new ValidatingResolver(sr);
|
||||
vr.setTimeout(0, DNS_LOOKUP_TIMEOUT);
|
||||
vr.loadTrustAnchors(new ByteArrayInputStream(ROOT.getBytes(StandardCharsets.US_ASCII)));
|
||||
Record qr = Record.newRecord(Name.fromConstantString(name + "."), Type.TXT, DClass.IN);
|
||||
Message response = vr.send(Message.newQuery(qr));
|
||||
final int rcode = response.getRcode();
|
||||
if (rcode != Rcode.NOERROR) {
|
||||
Timber.i("Rcode: %s", Rcode.string(rcode));
|
||||
for (RRset set : response.getSectionRRsets(Section.ADDITIONAL)) {
|
||||
if (set.getName().equals(Name.root) && set.getType() == Type.TXT
|
||||
&& set.getDClass() == ValidatingResolver.VALIDATION_REASON_QCLASS) {
|
||||
Timber.i("Reason: %s", ((TXTRecord) set.first()).getStrings().get(0));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
dnssec = response.getHeader().getFlag(Flags.AD);
|
||||
for (Record record : response.getSectionArray(Section.ANSWER)) {
|
||||
if (record.getType() == Type.TXT) {
|
||||
txts.addAll(((TXTRecord) record).getStrings());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException | IllegalArgumentException ex) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(Boolean success) {
|
||||
if (resolvedListener != null)
|
||||
if (success) {
|
||||
Map<Crypto, BarcodeData> dataMap = new HashMap<>();
|
||||
for (String txt : txts) {
|
||||
BarcodeData bc = BarcodeData.parseOpenAlias(txt, dnssec);
|
||||
if (bc != null) {
|
||||
if (!dataMap.containsKey(bc.asset)) {
|
||||
dataMap.put(bc.asset, bc);
|
||||
}
|
||||
}
|
||||
}
|
||||
resolvedListener.onResolved(dataMap);
|
||||
} else {
|
||||
resolvedListener.onFailure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class OpenAliasParser {
|
||||
int currentPos = 0;
|
||||
final String oaString;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
OpenAliasParser(String oaString) {
|
||||
this.oaString = oaString;
|
||||
}
|
||||
|
||||
Map<String, String> parse() {
|
||||
if ((oaString == null) || !oaString.startsWith(OA1_SCHEME)) return null;
|
||||
if (oaString.charAt(oaString.length() - 1) != ';') return null;
|
||||
|
||||
Map<String, String> oaAttributes = new HashMap<>();
|
||||
|
||||
final int assetEnd = oaString.indexOf(' ');
|
||||
if (assetEnd > 20) return null; // random sanity check
|
||||
String asset = oaString.substring(OA1_SCHEME.length(), assetEnd);
|
||||
oaAttributes.put(OA1_ASSET, asset);
|
||||
|
||||
boolean inQuote = false;
|
||||
boolean inKey = true;
|
||||
String key = null;
|
||||
for (currentPos = assetEnd; currentPos < oaString.length() - 1; currentPos++) {
|
||||
char c = currentChar();
|
||||
if (inKey) {
|
||||
if ((sb.length() == 0) && Character.isWhitespace(c)) continue;
|
||||
if ((c == '\\') || (c == ';')) return null;
|
||||
if (c == '=') {
|
||||
key = sb.toString();
|
||||
if (oaAttributes.containsKey(key)) return null; // no duplicate keys allowed
|
||||
sb.setLength(0);
|
||||
inKey = false;
|
||||
} else {
|
||||
sb.append(c);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// now we are in the value
|
||||
if ((sb.length() == 0) && (c == '"')) {
|
||||
inQuote = true;
|
||||
continue;
|
||||
}
|
||||
if ((!inQuote || ((sb.length() > 0) && (c == '"'))) && (nextChar() == ';')) {
|
||||
if (!inQuote) appendCurrentEscapedChar();
|
||||
oaAttributes.put(key, sb.toString());
|
||||
sb.setLength(0);
|
||||
currentPos++; // skip the next ;
|
||||
inQuote = false;
|
||||
inKey = true;
|
||||
key = null;
|
||||
continue;
|
||||
}
|
||||
appendCurrentEscapedChar();
|
||||
}
|
||||
if (inQuote) return null;
|
||||
|
||||
if (key != null) {
|
||||
oaAttributes.put(key, sb.toString());
|
||||
}
|
||||
|
||||
return oaAttributes;
|
||||
}
|
||||
|
||||
char currentChar() {
|
||||
return oaString.charAt(currentPos);
|
||||
}
|
||||
|
||||
char nextChar() throws IndexOutOfBoundsException {
|
||||
int pos = currentPos;
|
||||
char c = oaString.charAt(pos);
|
||||
if (c == '\\') {
|
||||
pos++;
|
||||
}
|
||||
return oaString.charAt(pos + 1);
|
||||
}
|
||||
|
||||
void appendCurrentEscapedChar() throws IndexOutOfBoundsException {
|
||||
char c = oaString.charAt(currentPos);
|
||||
if (c == '\\') {
|
||||
c = oaString.charAt(++currentPos);
|
||||
}
|
||||
sb.append(c);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -29,18 +29,6 @@ public class RestoreHeight {
|
||||
static final int DIFFICULTY_TARGET = 120; // seconds
|
||||
|
||||
static private RestoreHeight Singleton = null;
|
||||
|
||||
static public RestoreHeight getInstance() {
|
||||
if (Singleton == null) {
|
||||
synchronized (RestoreHeight.class) {
|
||||
if (Singleton == null) {
|
||||
Singleton = new RestoreHeight();
|
||||
}
|
||||
}
|
||||
}
|
||||
return Singleton;
|
||||
}
|
||||
|
||||
private final Map<String, Long> blockheight = new HashMap<>();
|
||||
|
||||
RestoreHeight() {
|
||||
@ -141,6 +129,17 @@ public class RestoreHeight {
|
||||
blockheight.put("2022-03-01", 2569711L);
|
||||
}
|
||||
|
||||
static public RestoreHeight getInstance() {
|
||||
if (Singleton == null) {
|
||||
synchronized (RestoreHeight.class) {
|
||||
if (Singleton == null) {
|
||||
Singleton = new RestoreHeight();
|
||||
}
|
||||
}
|
||||
}
|
||||
return Singleton;
|
||||
}
|
||||
|
||||
public long getHeight(String date) {
|
||||
SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd");
|
||||
parser.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
|
@ -1,64 +0,0 @@
|
||||
package com.m2049r.xmrwallet.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class ZipBackup {
|
||||
final private Context context;
|
||||
final private String walletName;
|
||||
|
||||
private ZipOutputStream zip;
|
||||
|
||||
public void writeTo(Uri zipUri) throws IOException {
|
||||
if (zip != null)
|
||||
throw new IllegalStateException("zip already initialized");
|
||||
try {
|
||||
zip = new ZipOutputStream(context.getContentResolver().openOutputStream(zipUri));
|
||||
|
||||
final File walletRoot = Helper.getWalletRoot(context);
|
||||
addFile(new File(walletRoot, walletName + ".keys"));
|
||||
addFile(new File(walletRoot, walletName));
|
||||
|
||||
zip.close();
|
||||
} finally {
|
||||
if (zip != null) zip.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void addFile(File file) throws IOException {
|
||||
if (!file.exists()) return; // ignore missing files (e.g. the cache file might not exist)
|
||||
ZipEntry entry = new ZipEntry(file.getName());
|
||||
zip.putNextEntry(entry);
|
||||
writeFile(file);
|
||||
zip.closeEntry();
|
||||
}
|
||||
|
||||
private void writeFile(File source) throws IOException {
|
||||
try (InputStream is = new FileInputStream(source)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int length;
|
||||
while ((length = is.read(buffer)) > 0) {
|
||||
zip.write(buffer, 0, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final SimpleDateFormat DATETIME_FORMATTER =
|
||||
new SimpleDateFormat("yyyy-MM-dd'T'HH-mm-ss");
|
||||
|
||||
public String getBackupName() {
|
||||
return walletName + " " + DATETIME_FORMATTER.format(new Date());
|
||||
}
|
||||
}
|
@ -1,139 +0,0 @@
|
||||
package com.m2049r.xmrwallet.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import timber.log.Timber;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class ZipRestore {
|
||||
final private Context context;
|
||||
final private Uri zipUri;
|
||||
|
||||
private File walletRoot;
|
||||
|
||||
private ZipInputStream zip;
|
||||
|
||||
public boolean restore() throws IOException {
|
||||
walletRoot = Helper.getWalletRoot(context);
|
||||
String walletName = testArchive();
|
||||
if (walletName == null) return false;
|
||||
|
||||
walletName = getUniqueName(walletName);
|
||||
|
||||
if (zip != null)
|
||||
throw new IllegalStateException("zip already initialized");
|
||||
try {
|
||||
zip = new ZipInputStream(context.getContentResolver().openInputStream(zipUri));
|
||||
for (ZipEntry entry = zip.getNextEntry(); entry != null; zip.closeEntry(), entry = zip.getNextEntry()) {
|
||||
File destination;
|
||||
final String name = entry.getName();
|
||||
if (name.endsWith(".keys")) {
|
||||
destination = new File(walletRoot, walletName + ".keys");
|
||||
} else if (name.endsWith(".address.txt")) {
|
||||
destination = new File(walletRoot, walletName + ".address.txt");
|
||||
} else {
|
||||
destination = new File(walletRoot, walletName);
|
||||
}
|
||||
writeFile(destination);
|
||||
}
|
||||
} finally {
|
||||
if (zip != null) zip.close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void writeFile(File destination) throws IOException {
|
||||
try (OutputStream os = new FileOutputStream(destination)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int length;
|
||||
while ((length = zip.read(buffer)) > 0) {
|
||||
os.write(buffer, 0, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// test the archive to contain files we expect & return the name of the contained wallet or null
|
||||
private String testArchive() {
|
||||
String walletName = null;
|
||||
boolean keys = false;
|
||||
ZipInputStream zipStream = null;
|
||||
try {
|
||||
zipStream = new ZipInputStream(context.getContentResolver().openInputStream(zipUri));
|
||||
for (ZipEntry entry = zipStream.getNextEntry(); entry != null;
|
||||
zipStream.closeEntry(), entry = zipStream.getNextEntry()) {
|
||||
if (entry.isDirectory())
|
||||
return null;
|
||||
final String name = entry.getName();
|
||||
if ((new File(name)).getParentFile() != null)
|
||||
return null;
|
||||
if (walletName == null) {
|
||||
if (name.endsWith(".keys")) {
|
||||
walletName = name.substring(0, name.length() - ".keys".length());
|
||||
keys = true; // we have they keys
|
||||
} else if (name.endsWith(".address.txt")) {
|
||||
walletName = name.substring(0, name.length() - ".address.txt".length());
|
||||
} else {
|
||||
walletName = name;
|
||||
}
|
||||
} else { // we have a wallet name
|
||||
if (name.endsWith(".keys")) {
|
||||
if (!name.equals(walletName + ".keys")) return null;
|
||||
keys = true; // we have they keys
|
||||
} else if (name.endsWith(".address.txt")) {
|
||||
if (!name.equals(walletName + ".address.txt")) return null;
|
||||
} else if (!name.equals(walletName)) return null;
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
return null;
|
||||
} finally {
|
||||
try {
|
||||
if (zipStream != null) zipStream.close();
|
||||
} catch (IOException ex) {
|
||||
Timber.w(ex);
|
||||
}
|
||||
}
|
||||
// we need the keys at least
|
||||
if (keys) return walletName;
|
||||
else return null;
|
||||
}
|
||||
|
||||
final static Pattern WALLET_PATTERN = Pattern.compile("^(.+) \\(([0-9]+)\\).keys$");
|
||||
|
||||
private String getUniqueName(String name) {
|
||||
if (!(new File(walletRoot, name + ".keys")).exists()) // <name> does not exist => it's ok to use
|
||||
return name;
|
||||
|
||||
File[] wallets = walletRoot.listFiles(
|
||||
(dir, filename) -> {
|
||||
Matcher m = WALLET_PATTERN.matcher(filename);
|
||||
if (m.find())
|
||||
return m.group(1).equals(name);
|
||||
else return false;
|
||||
});
|
||||
if (wallets.length == 0) return name + " (1)";
|
||||
int maxIndex = 0;
|
||||
for (File wallet : wallets) {
|
||||
try {
|
||||
final Matcher m = WALLET_PATTERN.matcher(wallet.getName());
|
||||
m.find();
|
||||
final int index = Integer.parseInt(m.group(2));
|
||||
if (index > maxIndex) maxIndex = index;
|
||||
} catch (NumberFormatException ex) {
|
||||
// this cannot happen & we can ignore it if it does
|
||||
}
|
||||
}
|
||||
return name + " (" + (maxIndex + 1) + ")";
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Based on
|
||||
* https://stackoverflow.com/a/19943894
|
||||
*
|
||||
* Curve parameters from
|
||||
* https://en.bitcoin.it/wiki/Secp256k1
|
||||
*
|
||||
* Copyright (c) 2019 m2049r
|
||||
* Copyright (c) 2013 ChiaraHsieh
|
||||
*
|
||||
* 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.ledger;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.spec.ECPoint;
|
||||
|
||||
public class ECsecp256k1 {
|
||||
static private final BigInteger TWO = new BigInteger("2");
|
||||
static private final BigInteger THREE = new BigInteger("3");
|
||||
static public final BigInteger p = new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16);
|
||||
static public final BigInteger a = new BigInteger("0000000000000000000000000000000000000000000000000000000000000000", 16);
|
||||
static public final BigInteger b = new BigInteger("0000000000000000000000000000000000000000000000000000000000000007", 16);
|
||||
static public final BigInteger n = new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16);
|
||||
static public final ECPoint G = new ECPoint(
|
||||
new BigInteger("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798", 16),
|
||||
new BigInteger("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8", 16));
|
||||
|
||||
public static ECPoint scalmult(BigInteger kin, ECPoint P) {
|
||||
ECPoint R = ECPoint.POINT_INFINITY, S = P;
|
||||
BigInteger k = kin.mod(n); // not necessary b/c that's how curves work
|
||||
int length = k.bitLength();
|
||||
byte[] binarray = new byte[length];
|
||||
for (int i = 0; i <= length - 1; i++) {
|
||||
binarray[i] = k.mod(TWO).byteValue();
|
||||
k = k.divide(TWO);
|
||||
}
|
||||
for (int i = length - 1; i >= 0; i--) {
|
||||
// i should start at length-1 not -2 because the MSB of binary may not be 1
|
||||
R = doublePoint(R);
|
||||
if (binarray[i] == 1)
|
||||
R = addPoint(R, S);
|
||||
}
|
||||
return R;
|
||||
}
|
||||
|
||||
public static ECPoint addPoint(ECPoint r, ECPoint s) {
|
||||
if (r.equals(s))
|
||||
return doublePoint(r);
|
||||
else if (r.equals(ECPoint.POINT_INFINITY))
|
||||
return s;
|
||||
else if (s.equals(ECPoint.POINT_INFINITY))
|
||||
return r;
|
||||
BigInteger slope = (r.getAffineY().subtract(s.getAffineY()))
|
||||
.multiply(r.getAffineX().subtract(s.getAffineX()).modInverse(p));
|
||||
BigInteger Xout = (slope.modPow(TWO, p).subtract(r.getAffineX())).subtract(s.getAffineX()).mod(p);
|
||||
BigInteger Yout = s.getAffineY().negate().add(slope.multiply(s.getAffineX().subtract(Xout))).mod(p);
|
||||
return new ECPoint(Xout, Yout);
|
||||
}
|
||||
|
||||
public static ECPoint doublePoint(ECPoint r) {
|
||||
if (r.equals(ECPoint.POINT_INFINITY))
|
||||
return r;
|
||||
BigInteger slope = (r.getAffineX().pow(2)).multiply(THREE).add(a)
|
||||
.multiply((r.getAffineY().multiply(TWO)).modInverse(p));
|
||||
BigInteger Xout = slope.pow(2).subtract(r.getAffineX().multiply(TWO)).mod(p);
|
||||
BigInteger Yout = (r.getAffineY().negate()).add(slope.multiply(r.getAffineX().subtract(Xout))).mod(p);
|
||||
return new ECPoint(Xout, Yout);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -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}};
|
||||
}
|
||||
|
||||
}
|
@ -1,218 +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
|
||||
return (data[data.length - 1 - 6] & ((1 << remainderSize) - 1)) == 0;
|
||||
}
|
||||
}
|
@ -1,64 +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://github.com/ognus/wallet-address-validator/blob/master/src/ethereum_validator.js
|
||||
|
||||
import com.theromus.sha.Keccak;
|
||||
import com.theromus.sha.Parameters;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class EthAddressValidator {
|
||||
static private final Pattern ETH_ADDRESS = Pattern.compile("^0x[0-9a-fA-F]{40}$");
|
||||
static private final Pattern ETH_ALLLOWER = Pattern.compile("^0x[0-9a-f]{40}$");
|
||||
static private final Pattern ETH_ALLUPPER = Pattern.compile("^0x[0-9A-F]{40}$");
|
||||
|
||||
public static boolean validate(String address) {
|
||||
// Check if it has the basic requirements of an address
|
||||
if (!ETH_ADDRESS.matcher(address).matches())
|
||||
return false;
|
||||
|
||||
// If it's all small caps or all all caps, return true
|
||||
if (ETH_ALLLOWER.matcher(address).matches() || ETH_ALLUPPER.matcher(address).matches()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise check each case
|
||||
return validateChecksum(address);
|
||||
}
|
||||
|
||||
private static boolean validateChecksum(String address) {
|
||||
// Check each case
|
||||
address = address.substring(2); // strip 0x
|
||||
|
||||
Keccak keccak = new Keccak();
|
||||
final byte[] addressHash = keccak.getHash(
|
||||
address.toLowerCase().getBytes(StandardCharsets.US_ASCII),
|
||||
Parameters.KECCAK_256);
|
||||
for (int i = 0; i < 40; i++) {
|
||||
boolean upper = (addressHash[i / 2] & ((i % 2) == 0 ? 128 : 8)) != 0;
|
||||
char c = address.charAt(i);
|
||||
if (Character.isAlphabetic(c)) {
|
||||
if (Character.isUpperCase(c) && !upper) return false;
|
||||
if (Character.isLowerCase(c) && upper) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,170 +0,0 @@
|
||||
package com.theromus.sha;
|
||||
|
||||
import static com.theromus.utils.HexUtils.leftRotate64;
|
||||
import static com.theromus.utils.HexUtils.convertToUint;
|
||||
import static com.theromus.utils.HexUtils.convertFromLittleEndianTo64;
|
||||
import static com.theromus.utils.HexUtils.convertFrom64ToLittleEndian;
|
||||
import static java.lang.Math.min;
|
||||
import static java.lang.System.arraycopy;
|
||||
import static java.util.Arrays.fill;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.math.BigInteger;
|
||||
|
||||
|
||||
/**
|
||||
* Keccak implementation.
|
||||
*
|
||||
* @author romus
|
||||
*/
|
||||
public class Keccak {
|
||||
|
||||
private static BigInteger BIT_64 = new BigInteger("18446744073709551615");
|
||||
|
||||
/**
|
||||
* Do hash.
|
||||
*
|
||||
* @param message input data
|
||||
* @param parameter keccak param
|
||||
* @return byte-array result
|
||||
*/
|
||||
public byte[] getHash(final byte[] message, final Parameters parameter) {
|
||||
int[] uState = new int[200];
|
||||
int[] uMessage = convertToUint(message);
|
||||
|
||||
|
||||
int rateInBytes = parameter.getRate() / 8;
|
||||
int blockSize = 0;
|
||||
int inputOffset = 0;
|
||||
|
||||
// Absorbing phase
|
||||
while (inputOffset < uMessage.length) {
|
||||
blockSize = min(uMessage.length - inputOffset, rateInBytes);
|
||||
for (int i = 0; i < blockSize; i++) {
|
||||
uState[i] = uState[i] ^ uMessage[i + inputOffset];
|
||||
}
|
||||
|
||||
inputOffset = inputOffset + blockSize;
|
||||
if (blockSize == rateInBytes) {
|
||||
doKeccakf(uState);
|
||||
blockSize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Padding phase
|
||||
uState[blockSize] = uState[blockSize] ^ parameter.getD();
|
||||
if ((parameter.getD() & 0x80) != 0 && blockSize == (rateInBytes - 1)) {
|
||||
doKeccakf(uState);
|
||||
}
|
||||
|
||||
uState[rateInBytes - 1] = uState[rateInBytes - 1] ^ 0x80;
|
||||
doKeccakf(uState);
|
||||
|
||||
// Squeezing phase
|
||||
ByteArrayOutputStream byteResults = new ByteArrayOutputStream();
|
||||
int tOutputLen = parameter.getOutputLen() / 8;
|
||||
while (tOutputLen > 0) {
|
||||
blockSize = min(tOutputLen, rateInBytes);
|
||||
for (int i = 0; i < blockSize; i++) {
|
||||
byteResults.write((byte) uState[i]);
|
||||
}
|
||||
|
||||
tOutputLen -= blockSize;
|
||||
if (tOutputLen > 0) {
|
||||
doKeccakf(uState);
|
||||
}
|
||||
}
|
||||
|
||||
return byteResults.toByteArray();
|
||||
}
|
||||
|
||||
private void doKeccakf(final int[] uState) {
|
||||
BigInteger[][] lState = new BigInteger[5][5];
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
for (int j = 0; j < 5; j++) {
|
||||
int[] data = new int[8];
|
||||
arraycopy(uState, 8 * (i + 5 * j), data, 0, data.length);
|
||||
lState[i][j] = convertFromLittleEndianTo64(data);
|
||||
}
|
||||
}
|
||||
roundB(lState);
|
||||
|
||||
fill(uState, 0);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
for (int j = 0; j < 5; j++) {
|
||||
int[] data = convertFrom64ToLittleEndian(lState[i][j]);
|
||||
arraycopy(data, 0, uState, 8 * (i + 5 * j), data.length);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Permutation on the given state.
|
||||
*
|
||||
* @param state state
|
||||
*/
|
||||
private void roundB(final BigInteger[][] state) {
|
||||
int LFSRstate = 1;
|
||||
for (int round = 0; round < 24; round++) {
|
||||
BigInteger[] C = new BigInteger[5];
|
||||
BigInteger[] D = new BigInteger[5];
|
||||
|
||||
// θ step
|
||||
for (int i = 0; i < 5; i++) {
|
||||
C[i] = state[i][0].xor(state[i][1]).xor(state[i][2]).xor(state[i][3]).xor(state[i][4]);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
D[i] = C[(i + 4) % 5].xor(leftRotate64(C[(i + 1) % 5], 1));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
for (int j = 0; j < 5; j++) {
|
||||
state[i][j] = state[i][j].xor(D[i]);
|
||||
}
|
||||
}
|
||||
|
||||
//ρ and π steps
|
||||
int x = 1, y = 0;
|
||||
BigInteger current = state[x][y];
|
||||
for (int i = 0; i < 24; i++) {
|
||||
int tX = x;
|
||||
x = y;
|
||||
y = (2 * tX + 3 * y) % 5;
|
||||
|
||||
BigInteger shiftValue = current;
|
||||
current = state[x][y];
|
||||
|
||||
state[x][y] = leftRotate64(shiftValue, (i + 1) * (i + 2) / 2);
|
||||
}
|
||||
|
||||
//χ step
|
||||
for (int j = 0; j < 5; j++) {
|
||||
BigInteger[] t = new BigInteger[5];
|
||||
for (int i = 0; i < 5; i++) {
|
||||
t[i] = state[i][j];
|
||||
}
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
// ~t[(i + 1) % 5]
|
||||
BigInteger invertVal = t[(i + 1) % 5].xor(BIT_64);
|
||||
// t[i] ^ ((~t[(i + 1) % 5]) & t[(i + 2) % 5])
|
||||
state[i][j] = t[i].xor(invertVal.and(t[(i + 2) % 5]));
|
||||
}
|
||||
}
|
||||
|
||||
//ι step
|
||||
for (int i = 0; i < 7; i++) {
|
||||
LFSRstate = ((LFSRstate << 1) ^ ((LFSRstate >> 7) * 0x71)) % 256;
|
||||
// pow(2, i) - 1
|
||||
int bitPosition = (1 << i) - 1;
|
||||
if ((LFSRstate & 2) != 0) {
|
||||
state[0][0] = state[0][0].xor(new BigInteger("1").shiftLeft(bitPosition));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package com.theromus.sha;
|
||||
|
||||
/**
|
||||
* The parameters defining the standard FIPS 202.
|
||||
*
|
||||
* @author romus
|
||||
*/
|
||||
public enum Parameters {
|
||||
KECCAK_224 (1152, 0x01, 224),
|
||||
KECCAK_256 (1088, 0x01, 256),
|
||||
KECCAK_384 (832, 0x01, 384),
|
||||
KECCAK_512 (576, 0x01, 512),
|
||||
|
||||
SHA3_224 (1152, 0x06, 224),
|
||||
SHA3_256 (1088, 0x06, 256),
|
||||
SHA3_384 (832, 0x06, 384),
|
||||
SHA3_512 (576, 0x06, 512),
|
||||
|
||||
SHAKE128 (1344, 0x1F, 256),
|
||||
SHAKE256 (1088, 0x1F, 512);
|
||||
|
||||
private final int rate;
|
||||
|
||||
/**
|
||||
* Delimited suffix.
|
||||
*/
|
||||
public final int d;
|
||||
|
||||
/**
|
||||
* Output length (bits).
|
||||
*/
|
||||
public final int outputLen;
|
||||
|
||||
Parameters(int rate, int d, int outputLen) {
|
||||
this.rate = rate;
|
||||
this.d = d;
|
||||
this.outputLen = outputLen;
|
||||
}
|
||||
|
||||
public int getRate() {
|
||||
return rate;
|
||||
}
|
||||
|
||||
public int getD() {
|
||||
return d;
|
||||
}
|
||||
|
||||
public int getOutputLen() {
|
||||
return outputLen;
|
||||
}
|
||||
}
|
@ -1,97 +0,0 @@
|
||||
package com.theromus.utils;
|
||||
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* Hex-utils.
|
||||
*
|
||||
* @author romus
|
||||
*/
|
||||
public class HexUtils {
|
||||
|
||||
private static final byte[] ENCODE_BYTE_TABLE = {
|
||||
(byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', (byte) '7',
|
||||
(byte) '8', (byte) '9', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f'
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert byte array to unsigned array.
|
||||
*
|
||||
* @param data byte array
|
||||
* @return unsigned array
|
||||
*/
|
||||
public static int[] convertToUint(final byte[] data) {
|
||||
int[] converted = new int[data.length];
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
converted[i] = data[i] & 0xFF;
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert LE to 64-bit value (unsigned long).
|
||||
*
|
||||
* @param data data
|
||||
* @return 64-bit value (unsigned long)
|
||||
*/
|
||||
public static BigInteger convertFromLittleEndianTo64(final int[] data) {
|
||||
BigInteger uLong = new BigInteger("0");
|
||||
for (int i = 0; i < 8; i++) {
|
||||
uLong = uLong.add(new BigInteger(Integer.toString(data[i])).shiftLeft(8 * i));
|
||||
}
|
||||
|
||||
return uLong;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert 64-bit (unsigned long) value to LE.
|
||||
*
|
||||
* @param uLong 64-bit value (unsigned long)
|
||||
* @return LE
|
||||
*/
|
||||
public static int[] convertFrom64ToLittleEndian(final BigInteger uLong) {
|
||||
int[] data = new int[8];
|
||||
BigInteger mod256 = new BigInteger("256");
|
||||
for (int i = 0; i < 8; i++) {
|
||||
data[i] = uLong.shiftRight((8 * i)).mod(mod256).intValue();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bitwise rotate left.
|
||||
*
|
||||
* @param value unsigned long value
|
||||
* @param rotate rotate left
|
||||
* @return result
|
||||
*/
|
||||
public static BigInteger leftRotate64(final BigInteger value, final int rotate) {
|
||||
BigInteger lp = value.shiftRight(64 - (rotate % 64));
|
||||
BigInteger rp = value.shiftLeft(rotate % 64);
|
||||
|
||||
return lp.add(rp).mod(new BigInteger("18446744073709551616"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert bytes to string.
|
||||
*
|
||||
* @param data bytes array
|
||||
* @return string
|
||||
*/
|
||||
public static String convertBytesToString(final byte[] data) {
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
int uVal = data[i] & 0xFF;
|
||||
|
||||
buffer.write(ENCODE_BYTE_TABLE[(uVal >>> 4)]);
|
||||
buffer.write(ENCODE_BYTE_TABLE[uVal & 0xF]);
|
||||
}
|
||||
|
||||
return new String(buffer.toByteArray());
|
||||
}
|
||||
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
/*
|
||||
* Copyright 2012-2016 Nathan Freitas
|
||||
* Copyright 2015 str4d
|
||||
* Portions Copyright (c) 2016 CommonsWare, LLC
|
||||
*
|
||||
* 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 info.guardianproject.netcipher.client;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
|
||||
/**
|
||||
* Creates an OkHttpClient using NetCipher configuration. Use
|
||||
* build() if you have no other OkHttpClient configuration
|
||||
* that you need to perform. Or, use applyTo() to augment an
|
||||
* existing OkHttpClient.Builder with NetCipher.
|
||||
*/
|
||||
public class StrongOkHttpClientBuilder extends
|
||||
StrongBuilderBase<StrongOkHttpClientBuilder, OkHttpClient> {
|
||||
/**
|
||||
* Creates a StrongOkHttpClientBuilder using the strongest set
|
||||
* of options for security. Use this if the strongest set of
|
||||
* options is what you want; otherwise, create a
|
||||
* builder via the constructor and configure it as you see fit.
|
||||
*
|
||||
* @param context any Context will do
|
||||
* @return a configured StrongOkHttpClientBuilder
|
||||
* @throws Exception
|
||||
*/
|
||||
static public StrongOkHttpClientBuilder forMaxSecurity(Context context)
|
||||
throws Exception {
|
||||
return(new StrongOkHttpClientBuilder(context)
|
||||
.withBestProxy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder instance.
|
||||
*
|
||||
* @param context any Context will do; builder will hold onto
|
||||
* Application context
|
||||
*/
|
||||
public StrongOkHttpClientBuilder(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy constructor.
|
||||
*
|
||||
* @param original builder to clone
|
||||
*/
|
||||
public StrongOkHttpClientBuilder(StrongOkHttpClientBuilder original) {
|
||||
super(original);
|
||||
}
|
||||
|
||||
/**
|
||||
* OkHttp3 does not support SOCKS proxies:
|
||||
* https://github.com/square/okhttp/issues/2315
|
||||
*
|
||||
* @return false
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsSocksProxy() {
|
||||
return(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OkHttpClient build(Intent status) {
|
||||
return(applyTo(new OkHttpClient.Builder(), status).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds NetCipher configuration to an existing OkHttpClient.Builder,
|
||||
* in case you have additional configuration that you wish to
|
||||
* perform.
|
||||
*
|
||||
* @param builder a new or partially-configured OkHttpClient.Builder
|
||||
* @return the same builder
|
||||
*/
|
||||
public OkHttpClient.Builder applyTo(OkHttpClient.Builder builder, Intent status) {
|
||||
SSLSocketFactory factory=buildSocketFactory();
|
||||
|
||||
if (factory!=null) {
|
||||
builder.sslSocketFactory(factory);
|
||||
}
|
||||
|
||||
return(builder
|
||||
.proxy(buildProxy(status)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String get(Intent status, OkHttpClient connection,
|
||||
String url) throws Exception {
|
||||
Request request=new Request.Builder().url(url).build();
|
||||
|
||||
return(connection.newCall(request).execute().body().string());
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Color when the row is selected -->
|
||||
<item android:state_enabled="true" android:drawable="@drawable/button_bg_enabled" />
|
||||
<item android:drawable="@drawable/button_bg_enabled" android:state_enabled="true" />
|
||||
<!-- Standard background color -->
|
||||
<item android:drawable="@drawable/button_bg_disabled" />
|
||||
</selector>
|
@ -4,8 +4,6 @@
|
||||
<padding
|
||||
android:left="8dp"
|
||||
android:right="8dp" />
|
||||
<solid
|
||||
android:color="@color/button_disabled_bg_color"/>
|
||||
<corners
|
||||
android:radius="8dp"/>
|
||||
<solid android:color="@color/button_disabled_bg_color" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
|
@ -3,9 +3,7 @@
|
||||
android:shape="rectangle">
|
||||
<padding
|
||||
android:left="8dp"
|
||||
android:right="8dp"/>
|
||||
<solid
|
||||
android:color="@color/oled_colorSecondary"/>
|
||||
<corners
|
||||
android:radius="8dp"/>
|
||||
android:right="8dp" />
|
||||
<solid android:color="@color/oled_colorSecondary" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
|
@ -6,8 +6,6 @@
|
||||
android:left="12dp"
|
||||
android:right="12dp"
|
||||
android:top="12dp" />
|
||||
<solid
|
||||
android:color="@color/edittext_bg_color"/>
|
||||
<corners
|
||||
android:radius="8dp"/>
|
||||
<solid android:color="@color/edittext_bg_color" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
|
@ -5,5 +5,5 @@
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="@color/oled_textColorPrimary"
|
||||
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
|
||||
android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z" />
|
||||
</vector>
|
||||
|
@ -1,17 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval" android:padding="10dp">
|
||||
<shape
|
||||
android:padding="10dp"
|
||||
android:shape="oval">
|
||||
<padding
|
||||
android:top="2dp"
|
||||
android:bottom="2dp"
|
||||
android:left="2dp"
|
||||
android:right="2dp"/>
|
||||
android:right="2dp"
|
||||
android:top="2dp" />
|
||||
<stroke
|
||||
android:color="#fff"
|
||||
android:width="1dp"/>
|
||||
<solid
|
||||
android:color="#fff"/>
|
||||
android:width="1dp"
|
||||
android:color="#fff" />
|
||||
<solid android:color="#fff" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:drawable="@drawable/ic_monero" />
|
||||
|
@ -2,17 +2,14 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background">
|
||||
<shape>
|
||||
<solid
|
||||
android:color="@android:color/white" />
|
||||
<solid android:color="@android:color/white" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:id="@android:id/progress">
|
||||
<item android:id="@android:id/progress">
|
||||
<clip>
|
||||
<shape>
|
||||
<solid
|
||||
android:color="@color/oled_colorSecondary" />
|
||||
<solid android:color="@color/oled_colorSecondary" />
|
||||
</shape>
|
||||
</clip>
|
||||
</item>
|
||||
|
@ -1,19 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/main_nav" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -23,14 +23,14 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="100.000000000000 XMR"
|
||||
app:layout_constraintEnd_toStartOf="@id/settings_imageview"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="100.000000000000 XMR" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/balance_locked_textview"
|
||||
@ -38,79 +38,80 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
tools:text="+ 100.000000000000 confirming"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/settings_imageview" />
|
||||
app:layout_constraintTop_toBottomOf="@id/settings_imageview"
|
||||
tools:text="+ 100.000000000000 confirming" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/transaction_history_recyclerview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingBottom="128dp"
|
||||
android:clipToPadding="false"
|
||||
app:layout_constraintTop_toBottomOf="@id/balance_locked_textview"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/balance_locked_textview"
|
||||
app:layout_constraintVertical_bias="0.0" />
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:src="@drawable/gradient_bottom"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/receive_send_buttons_layout"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/receive_send_buttons_layout" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/receive_send_buttons_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingTop="128dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:paddingTop="128dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/receive_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/button_bg"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@drawable/button_bg"
|
||||
android:text="@string/receive"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/send_button"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/send_button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/button_bg"
|
||||
android:layout_marginStart="4dp"
|
||||
android:background="@drawable/button_bg"
|
||||
android:text="@string/send"
|
||||
app:layout_constraintStart_toEndOf="@id/receive_button"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
app:layout_constraintStart_toEndOf="@id/receive_button" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/settings_imageview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_settings"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:padding="8dp"
|
||||
android:minHeight="24dp"
|
||||
android:minWidth="24dp"
|
||||
app:layout_constraintTop_toTopOf="@id/balance_unlocked_textview"
|
||||
android:minHeight="24dp"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_settings"
|
||||
app:layout_constraintBottom_toBottomOf="@id/balance_unlocked_textview"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/balance_unlocked_textview" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -4,20 +4,22 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".fragment.settings.SettingsFragment"
|
||||
android:padding="24dp">
|
||||
android:padding="24dp"
|
||||
tools:context=".fragment.settings.SettingsFragment">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/create_wallet_textview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/create_wallet"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/create_wallet"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/wallet_password_edittext"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/wallet_password_edittext"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/wallet_password_edittext"
|
||||
android:layout_width="0dp"
|
||||
@ -26,31 +28,34 @@
|
||||
android:background="@drawable/edittext_bg"
|
||||
android:hint="@string/password_optional"
|
||||
android:inputType="textPassword"
|
||||
app:layout_constraintTop_toBottomOf="@id/create_wallet_textview"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/advanced_settings_dropdown_textview"
|
||||
tools:visibility="visible"/>
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/create_wallet_textview"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/advanced_settings_dropdown_textview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/more_options"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/more_options"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toTopOf="@id/wallet_seed_edittext"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/wallet_password_edittext"
|
||||
app:layout_constraintBottom_toTopOf="@id/wallet_seed_edittext"/>
|
||||
app:layout_constraintTop_toBottomOf="@id/wallet_password_edittext" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/advanced_settings_chevron_imageview"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_keyboard_arrow_down"
|
||||
app:layout_constraintBottom_toBottomOf="@id/advanced_settings_dropdown_textview"
|
||||
app:layout_constraintStart_toEndOf="@id/advanced_settings_dropdown_textview"
|
||||
app:layout_constraintTop_toTopOf="@id/advanced_settings_dropdown_textview"
|
||||
app:layout_constraintBottom_toBottomOf="@id/advanced_settings_dropdown_textview"/>
|
||||
app:layout_constraintTop_toTopOf="@id/advanced_settings_dropdown_textview" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/wallet_seed_edittext"
|
||||
android:layout_width="0dp"
|
||||
@ -59,24 +64,26 @@
|
||||
android:background="@drawable/edittext_bg"
|
||||
android:hint="@string/recovery_phrase_optional"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/advanced_settings_dropdown_textview"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/wallet_restore_height_edittext"
|
||||
tools:visibility="visible"/>
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/advanced_settings_dropdown_textview"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/wallet_restore_height_edittext"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/restore_height_optional"
|
||||
android:background="@drawable/edittext_bg"
|
||||
android:hint="@string/restore_height_optional"
|
||||
android:inputType="number"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/wallet_seed_edittext"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/create_wallet_button"
|
||||
tools:visibility="visible"/>
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/wallet_seed_edittext"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/create_wallet_button"
|
||||
android:layout_width="match_parent"
|
||||
@ -85,6 +92,6 @@
|
||||
android:background="@drawable/button_bg"
|
||||
android:text="@string/create_wallet"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/wallet_restore_height_edittext"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/wallet_restore_height_edittext" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -4,8 +4,9 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".fragment.settings.SettingsFragment"
|
||||
android:padding="24dp">
|
||||
android:padding="24dp"
|
||||
tools:context=".fragment.settings.SettingsFragment">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/settings_textview"
|
||||
android:layout_width="match_parent"
|
||||
@ -13,49 +14,49 @@
|
||||
android:text="@string/settings"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/wallet_settings_textview"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/wallet_settings_textview"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/wallet_settings_textview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/wallet"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginTop="32dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/display_seed_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/settings_textview"
|
||||
app:layout_constraintBottom_toTopOf="@id/display_seed_button"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/settings_textview" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/display_seed_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/display_recovery_phrase"
|
||||
android:layout_marginTop="16dp"
|
||||
android:background="@drawable/button_bg"
|
||||
android:text="@string/display_recovery_phrase"
|
||||
app:layout_constraintBottom_toTopOf="@id/appearance_settings_textview"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/appearance_settings_textview"
|
||||
app:layout_constraintTop_toBottomOf="@id/wallet_settings_textview" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/appearance_settings_textview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/appearance"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginTop="32dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/day_night_switch"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/display_seed_button"
|
||||
app:layout_constraintBottom_toTopOf="@id/day_night_switch"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/display_seed_button" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/day_night_textview"
|
||||
@ -63,10 +64,10 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/night_mode"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintTop_toTopOf="@id/day_night_switch"
|
||||
app:layout_constraintBottom_toBottomOf="@id/day_night_switch"
|
||||
app:layout_constraintEnd_toStartOf="@id/day_night_switch"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/day_night_switch"/>
|
||||
app:layout_constraintTop_toTopOf="@id/day_night_switch" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/day_night_switch"
|
||||
@ -74,21 +75,21 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/network_settings_textview"
|
||||
app:layout_constraintTop_toBottomOf="@id/appearance_settings_textview"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/appearance_settings_textview" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/network_settings_textview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/network"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginTop="32dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/tor_switch"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/day_night_switch"
|
||||
app:layout_constraintBottom_toTopOf="@id/tor_switch"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/day_night_switch" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tor_textview"
|
||||
@ -96,16 +97,16 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/tor_switch_label"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintTop_toTopOf="@id/tor_switch"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tor_switch"
|
||||
app:layout_constraintEnd_toStartOf="@id/tor_switch"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/tor_switch"/>
|
||||
app:layout_constraintTop_toTopOf="@id/tor_switch" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/tor_switch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/network_settings_textview"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/network_settings_textview" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -21,23 +21,24 @@
|
||||
android:textAlignment="center"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="INFORMATION"
|
||||
app:layout_constraintEnd_toStartOf="@id/copy_information_imagebutton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="INFORMATION" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/copy_information_imagebutton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:padding="8dp"
|
||||
android:minHeight="24dp"
|
||||
android:minWidth="24dp"
|
||||
android:minHeight="24dp"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_content_copy_24dp"
|
||||
app:layout_constraintTop_toTopOf="@id/information_textview"
|
||||
app:layout_constraintBottom_toBottomOf="@id/information_textview"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/information_textview"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
app:layout_constraintTop_toTopOf="@id/information_textview" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
@ -12,44 +11,48 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/enter_password_textview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/enter_password"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/enter_password"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/wallet_password_edittext"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/wallet_password_edittext"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/wallet_password_edittext"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:background="@drawable/edittext_bg"
|
||||
android:hint="@string/password"
|
||||
android:inputType="textPassword"
|
||||
android:background="@drawable/edittext_bg"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/unlock_wallet_button"
|
||||
app:layout_constraintEnd_toStartOf="@id/paste_password_imagebutton"
|
||||
app:layout_constraintTop_toBottomOf="@id/enter_password_textview"
|
||||
app:layout_constraintBottom_toTopOf="@id/unlock_wallet_button"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/enter_password_textview" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/paste_password_imagebutton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:src="@drawable/ic_content_paste_24dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:minWidth="24dp"
|
||||
android:minHeight="24dp"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintStart_toEndOf="@id/wallet_password_edittext"
|
||||
app:layout_constraintTop_toTopOf="@id/wallet_password_edittext"
|
||||
android:src="@drawable/ic_content_paste_24dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/wallet_password_edittext"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/wallet_password_edittext"
|
||||
app:layout_constraintTop_toTopOf="@id/wallet_password_edittext" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/unlock_wallet_button"
|
||||
android:layout_width="match_parent"
|
||||
@ -57,8 +60,8 @@
|
||||
android:layout_marginTop="32dp"
|
||||
android:background="@drawable/button_bg"
|
||||
android:text="@string/unlock"
|
||||
app:layout_constraintTop_toBottomOf="@id/wallet_password_edittext"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/wallet_password_edittext" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
@ -12,27 +12,29 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recv_monero_textview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/recv_monero"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/recv_monero"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/monero_qr_imageview"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/monero_qr_imageview"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/monero_qr_imageview"
|
||||
android:layout_width="256dp"
|
||||
android:layout_height="256dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:src="@drawable/ic_fingerprint"
|
||||
app:layout_constraintTop_toBottomOf="@id/recv_monero_textview"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/recv_monero_textview" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/monero_logo_imageview"
|
||||
@ -41,8 +43,8 @@
|
||||
android:src="@drawable/ic_monero_qr"
|
||||
app:layout_constraintBottom_toBottomOf="@id/monero_qr_imageview"
|
||||
app:layout_constraintEnd_toEndOf="@id/monero_qr_imageview"
|
||||
app:layout_constraintTop_toTopOf="@id/monero_qr_imageview"
|
||||
app:layout_constraintStart_toStartOf="@id/monero_qr_imageview"/>
|
||||
app:layout_constraintStart_toStartOf="@id/monero_qr_imageview"
|
||||
app:layout_constraintTop_toTopOf="@id/monero_qr_imageview" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/address_textview"
|
||||
@ -53,23 +55,24 @@
|
||||
android:textAlignment="center"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
tools:text="ADDRESS"
|
||||
app:layout_constraintEnd_toStartOf="@id/copy_address_imagebutton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/monero_qr_imageview" />
|
||||
app:layout_constraintTop_toBottomOf="@id/monero_qr_imageview"
|
||||
tools:text="ADDRESS" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/copy_address_imagebutton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:padding="8dp"
|
||||
android:minHeight="24dp"
|
||||
android:minWidth="24dp"
|
||||
android:minHeight="24dp"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_content_copy_24dp"
|
||||
app:layout_constraintTop_toTopOf="@id/address_textview"
|
||||
app:layout_constraintBottom_toBottomOf="@id/address_textview"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/address_textview"
|
||||
app:layout_constraintEnd_toEndOf="parent"/>
|
||||
app:layout_constraintTop_toTopOf="@id/address_textview" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
@ -17,14 +17,14 @@
|
||||
android:id="@+id/send_monero_textview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/send_monero"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:text="@string/send_monero"
|
||||
android:textSize="32sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/address_edittext"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/address_edittext"/>
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/address_edittext"
|
||||
@ -32,24 +32,24 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:background="@drawable/edittext_bg"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="middle"
|
||||
android:hint="@string/address"
|
||||
android:singleLine="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/amount_edittext"
|
||||
app:layout_constraintEnd_toStartOf="@id/paste_address_imagebutton"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/send_monero_textview"
|
||||
app:layout_constraintEnd_toStartOf="@id/paste_address_imagebutton"
|
||||
app:layout_constraintBottom_toTopOf="@id/amount_edittext"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/paste_address_imagebutton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:minWidth="24dp"
|
||||
android:minHeight="24dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_content_paste_24dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/address_edittext"
|
||||
@ -58,6 +58,7 @@
|
||||
app:layout_constraintTop_toTopOf="@id/address_edittext"
|
||||
tools:ignore="SpeakableTextPresentCheck"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/scan_address_imagebutton"
|
||||
android:layout_width="wrap_content"
|
||||
@ -73,6 +74,7 @@
|
||||
app:layout_constraintTop_toTopOf="@id/address_edittext"
|
||||
tools:ignore="SpeakableTextPresentCheck"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/amount_edittext"
|
||||
android:layout_width="0dp"
|
||||
@ -81,10 +83,11 @@
|
||||
android:background="@drawable/edittext_bg"
|
||||
android:hint="@string/amount"
|
||||
android:inputType="numberDecimal"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/send_max_button"
|
||||
app:layout_constraintBottom_toTopOf="@id/create_tx_button"
|
||||
tools:visibility="gone"/>
|
||||
app:layout_constraintEnd_toStartOf="@id/send_max_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sending_all_textview"
|
||||
android:layout_width="0dp"
|
||||
@ -94,22 +97,24 @@
|
||||
android:text="@string/sending_all"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="@id/amount_edittext"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/send_max_button"
|
||||
app:layout_constraintBottom_toBottomOf="@id/amount_edittext"
|
||||
tools:visibility="gone"/>
|
||||
app:layout_constraintEnd_toStartOf="@id/send_max_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/amount_edittext"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/send_max_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/button_bg"
|
||||
android:text="@string/send_max"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/amount_edittext"
|
||||
app:layout_constraintBottom_toBottomOf="@id/amount_edittext"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/amount_edittext"
|
||||
tools:visibility="gone"/>
|
||||
app:layout_constraintTop_toTopOf="@id/amount_edittext"
|
||||
tools:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/create_tx_button"
|
||||
android:layout_width="match_parent"
|
||||
@ -117,12 +122,9 @@
|
||||
android:layout_marginTop="32dp"
|
||||
android:background="@drawable/button_bg"
|
||||
android:text="@string/create"
|
||||
app:layout_constraintTop_toBottomOf="@id/amount_edittext"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="gone"/>
|
||||
|
||||
|
||||
|
||||
app:layout_constraintTop_toBottomOf="@id/amount_edittext"
|
||||
tools:visibility="gone" />
|
||||
|
||||
|
||||
<!-- SEND LAYOUT -->
|
||||
@ -130,46 +132,48 @@
|
||||
android:id="@+id/address_pending_textview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/tx_address_text"
|
||||
android:layout_marginTop="32dp"
|
||||
android:ellipsize="middle"
|
||||
android:singleLine="true"
|
||||
android:text="@string/tx_address_text"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="middle"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/send_monero_textview"
|
||||
app:layout_constraintBottom_toTopOf="@id/amount_pending_textview"
|
||||
tools:visibility="visible"/>
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/send_monero_textview"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/amount_pending_textview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/tx_amount_text"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/tx_amount_text"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/address_pending_textview"
|
||||
app:layout_constraintBottom_toTopOf="@id/fee_textview"
|
||||
tools:visibility="visible"/>
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/address_pending_textview"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/fee_textview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/tx_fee_text"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/tx_fee_text"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/amount_pending_textview"
|
||||
app:layout_constraintBottom_toTopOf="@id/send_tx_button"
|
||||
tools:visibility="visible"/>
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/amount_pending_textview"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/send_tx_button"
|
||||
@ -179,9 +183,9 @@
|
||||
android:background="@drawable/button_bg"
|
||||
android:text="@string/send"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/fee_textview"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:visibility="visible"/>
|
||||
app:layout_constraintTop_toBottomOf="@id/fee_textview"
|
||||
tools:visibility="visible" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
@ -10,10 +10,10 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp">
|
||||
android:paddingTop="12dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
@ -28,12 +28,10 @@
|
||||
android:id="@+id/settings_fragment"
|
||||
android:name="com.m2049r.xmrwallet.fragment.settings.SettingsFragment"
|
||||
android:label="fragment_send_amount"
|
||||
tools:layout="@layout/fragment_settings">
|
||||
</fragment>
|
||||
tools:layout="@layout/fragment_settings"></fragment>
|
||||
<fragment
|
||||
android:id="@+id/onboarding_fragment"
|
||||
android:name="com.m2049r.xmrwallet.fragment.onboarding.OnboardingFragment"
|
||||
android:label="fragment_onboarding"
|
||||
tools:layout="@layout/fragment_settings">
|
||||
</fragment>
|
||||
tools:layout="@layout/fragment_settings"></fragment>
|
||||
</navigation>
|
@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="MyMaterialTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="windowActionBar">false</item>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="visible" type="id"/>
|
||||
<item name="masked" type="id"/>
|
||||
<item name="visible" type="id" />
|
||||
<item name="masked" type="id" />
|
||||
</resources>
|
@ -22,8 +22,6 @@
|
||||
<string name="node_updated_hours">Last Block: %1$d hours ago</string>
|
||||
<string name="node_updated_days">Last Block: %1$d days ago</string>
|
||||
|
||||
<!-- order must be the same as in com.m2049r.xmrwallet.data.Crypto-->
|
||||
|
||||
<string name="tx_details_notes" translatable="false"><span style=\"background-color: #%1$s; color: #%2$s;\"> %3$s </span>%4$s</string>
|
||||
|
||||
<string name="menu_restore">Import wallet</string>
|
||||
|
@ -43,7 +43,9 @@
|
||||
<style name="BottomSheetDialog_Rounded" parent="MyMaterialThemeOled">
|
||||
<item name="android:windowBackground">@android:color/transparent</item>
|
||||
<item name="android:windowIsFloating">false</item>
|
||||
<item name="android:windowAnimationStyle">@style/Animation.MaterialComponents.BottomSheetDialog</item>
|
||||
<item name="android:windowAnimationStyle">
|
||||
@style/Animation.MaterialComponents.BottomSheetDialog
|
||||
</item>
|
||||
<item name="enableEdgeToEdge">true</item>
|
||||
<item name="paddingBottomSystemWindowInsets">true</item>
|
||||
<item name="paddingLeftSystemWindowInsets">true</item>
|
||||
|
@ -1,301 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 m2049r et al.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.m2049r.xmrwallet.service.exchange.ecb;
|
||||
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeException;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
|
||||
import net.jodah.concurrentunit.Waiter;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
|
||||
public class ExchangeRateTest {
|
||||
|
||||
private MockWebServer mockWebServer;
|
||||
|
||||
private ExchangeApi exchangeApi;
|
||||
|
||||
private Waiter waiter;
|
||||
|
||||
@Mock
|
||||
ExchangeCallback mockExchangeCallback;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
mockWebServer = new MockWebServer();
|
||||
mockWebServer.start();
|
||||
|
||||
waiter = new Waiter();
|
||||
|
||||
MockitoAnnotations.initMocks(this);
|
||||
NetCipherHelper.Request.mockClient = new OkHttpClient();
|
||||
exchangeApi = new ExchangeApiImpl( mockWebServer.url("/"));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
mockWebServer.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_shouldBeGetMethod()
|
||||
throws InterruptedException, TimeoutException {
|
||||
|
||||
exchangeApi.queryExchangeRate("EUR", "USD", mockExchangeCallback);
|
||||
|
||||
RecordedRequest request = mockWebServer.takeRequest();
|
||||
assertEquals("GET", request.getMethod());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_shouldBeEUR()
|
||||
throws InterruptedException, TimeoutException {
|
||||
|
||||
exchangeApi.queryExchangeRate("CHF", "USD", new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.assertTrue(e instanceof IllegalArgumentException);
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_shouldBeOneForEur()
|
||||
throws InterruptedException, TimeoutException {
|
||||
|
||||
exchangeApi.queryExchangeRate("EUR", "EUR", new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.assertEquals(1.0, exchangeRate.getRate());
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_wasSuccessfulShouldRespondWithUsdRate()
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
final String base = "EUR";
|
||||
final String quote = "USD";
|
||||
final double rate = 1.1043;
|
||||
|
||||
MockResponse jsonMockResponse = new MockResponse().setBody(createMockExchangeRateResponse());
|
||||
mockWebServer.enqueue(jsonMockResponse);
|
||||
|
||||
exchangeApi.queryExchangeRate(base, quote, new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.assertEquals(base, exchangeRate.getBaseCurrency());
|
||||
waiter.assertEquals(quote, exchangeRate.getQuoteCurrency());
|
||||
waiter.assertEquals(rate, exchangeRate.getRate());
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.fail(e);
|
||||
waiter.resume();
|
||||
}
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_wasSuccessfulShouldRespondWithAudRate()
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
final String base = "EUR";
|
||||
final String quote = "AUD";
|
||||
final double rate = 1.6246;
|
||||
|
||||
MockResponse jsonMockResponse = new MockResponse().setBody(createMockExchangeRateResponse());
|
||||
mockWebServer.enqueue(jsonMockResponse);
|
||||
|
||||
exchangeApi.queryExchangeRate(base, quote, new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.assertEquals(base, exchangeRate.getBaseCurrency());
|
||||
waiter.assertEquals(quote, exchangeRate.getQuoteCurrency());
|
||||
waiter.assertEquals(rate, exchangeRate.getRate());
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.fail(e);
|
||||
waiter.resume();
|
||||
}
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_wasSuccessfulShouldRespondWithZarRate()
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
final String base = "EUR";
|
||||
final String quote = "ZAR";
|
||||
final double rate = 16.3978;
|
||||
|
||||
MockResponse jsonMockResponse = new MockResponse().setBody(createMockExchangeRateResponse());
|
||||
mockWebServer.enqueue(jsonMockResponse);
|
||||
|
||||
exchangeApi.queryExchangeRate(base, quote, new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.assertEquals(base, exchangeRate.getBaseCurrency());
|
||||
waiter.assertEquals(quote, exchangeRate.getQuoteCurrency());
|
||||
waiter.assertEquals(rate, exchangeRate.getRate());
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.fail(e);
|
||||
waiter.resume();
|
||||
}
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_wasNotSuccessfulShouldCallOnError()
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
mockWebServer.enqueue(new MockResponse().setResponseCode(500));
|
||||
exchangeApi.queryExchangeRate("EUR", "USD", new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.assertTrue(e instanceof ExchangeException);
|
||||
waiter.assertTrue(((ExchangeException) e).getCode() == 500);
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_unknownAssetShouldCallOnError()
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
MockResponse jsonMockResponse = new MockResponse().setBody(createMockExchangeRateResponse());
|
||||
mockWebServer.enqueue(jsonMockResponse);
|
||||
exchangeApi.queryExchangeRate("EUR", "ABC", new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.assertTrue(e instanceof ExchangeException);
|
||||
ExchangeException ex = (ExchangeException) e;
|
||||
waiter.assertTrue(ex.getCode() == 404);
|
||||
waiter.assertEquals(ex.getErrorMsg(), "Currency not supported: ABC");
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
static public String createMockExchangeRateResponse() {
|
||||
return "<gesmes:Envelope xmlns:gesmes=\"http://www.gesmes.org/xml/2002-08-01\" xmlns=\"http://www.ecb.int/vocabulary/2002-08-01/eurofxref\"><script xmlns=\"\"/>\n" +
|
||||
"\t<gesmes:subject>Reference rates</gesmes:subject>\n" +
|
||||
"\t<gesmes:Sender>\n" +
|
||||
"\t\t<gesmes:name>European Central Bank</gesmes:name>\n" +
|
||||
"\t</gesmes:Sender>\n" +
|
||||
"\t<Cube>\n" +
|
||||
"\t\t<Cube time=\"2019-10-11\">\n" +
|
||||
"\t\t\t<Cube currency=\"USD\" rate=\"1.1043\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"JPY\" rate=\"119.75\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"BGN\" rate=\"1.9558\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"CZK\" rate=\"25.807\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"DKK\" rate=\"7.4688\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"GBP\" rate=\"0.87518\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"HUF\" rate=\"331.71\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"PLN\" rate=\"4.3057\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"RON\" rate=\"4.7573\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"SEK\" rate=\"10.8448\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"CHF\" rate=\"1.1025\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"ISK\" rate=\"137.70\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"NOK\" rate=\"10.0375\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"HRK\" rate=\"7.4280\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"RUB\" rate=\"70.8034\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"TRY\" rate=\"6.4713\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"AUD\" rate=\"1.6246\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"BRL\" rate=\"4.5291\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"CAD\" rate=\"1.4679\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"CNY\" rate=\"7.8417\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"HKD\" rate=\"8.6614\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"IDR\" rate=\"15601.55\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"ILS\" rate=\"3.8673\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"INR\" rate=\"78.4875\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"KRW\" rate=\"1308.61\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"MXN\" rate=\"21.3965\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"MYR\" rate=\"4.6220\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"NZD\" rate=\"1.7419\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"PHP\" rate=\"56.927\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"SGD\" rate=\"1.5177\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"THB\" rate=\"33.642\"/>\n" +
|
||||
"\t\t\t<Cube currency=\"ZAR\" rate=\"16.3978\"/>\n" +
|
||||
"\t\t</Cube>\n" +
|
||||
"\t</Cube>\n" +
|
||||
"</gesmes:Envelope>";
|
||||
}
|
||||
}
|
@ -1,186 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 m2049r et al.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.m2049r.xmrwallet.service.exchange.kraken;
|
||||
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeApi;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeCallback;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeException;
|
||||
import com.m2049r.xmrwallet.service.exchange.api.ExchangeRate;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
|
||||
import net.jodah.concurrentunit.Waiter;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
|
||||
public class ExchangeRateTest {
|
||||
|
||||
private MockWebServer mockWebServer;
|
||||
|
||||
private ExchangeApi exchangeApi;
|
||||
|
||||
private Waiter waiter;
|
||||
|
||||
@Mock
|
||||
ExchangeCallback mockExchangeCallback;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
mockWebServer = new MockWebServer();
|
||||
mockWebServer.start();
|
||||
|
||||
waiter = new Waiter();
|
||||
|
||||
MockitoAnnotations.initMocks(this);
|
||||
NetCipherHelper.Request.mockClient = new OkHttpClient();
|
||||
exchangeApi = new ExchangeApiImpl(mockWebServer.url("/"));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
mockWebServer.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_shouldBeGetMethod()
|
||||
throws InterruptedException, TimeoutException {
|
||||
|
||||
exchangeApi.queryExchangeRate("XMR", "USD", mockExchangeCallback);
|
||||
|
||||
RecordedRequest request = mockWebServer.takeRequest();
|
||||
assertEquals("GET", request.getMethod());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_shouldHavePairInUrl()
|
||||
throws InterruptedException, TimeoutException {
|
||||
|
||||
exchangeApi.queryExchangeRate("XMR", "USD", mockExchangeCallback);
|
||||
|
||||
RecordedRequest request = mockWebServer.takeRequest();
|
||||
assertEquals("/?pair=XMRUSD", request.getPath());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_wasSuccessfulShouldRespondWithRate()
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
final String base = "XMR";
|
||||
final String quote = "USD";
|
||||
final double rate = 100;
|
||||
MockResponse jsonMockResponse = new MockResponse().setBody(
|
||||
createMockExchangeRateResponse(base, quote, rate));
|
||||
mockWebServer.enqueue(jsonMockResponse);
|
||||
|
||||
exchangeApi.queryExchangeRate(base, quote, new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.assertEquals(exchangeRate.getBaseCurrency(), base);
|
||||
waiter.assertEquals(exchangeRate.getQuoteCurrency(), quote);
|
||||
waiter.assertEquals(exchangeRate.getRate(), rate);
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.fail(e);
|
||||
waiter.resume();
|
||||
}
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_wasNotSuccessfulShouldCallOnError()
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
mockWebServer.enqueue(new MockResponse().setResponseCode(500));
|
||||
exchangeApi.queryExchangeRate("XMR", "USD", new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.assertTrue(e instanceof ExchangeException);
|
||||
waiter.assertTrue(((ExchangeException) e).getCode() == 500);
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryExchangeRate_unknownAssetShouldCallOnError()
|
||||
throws InterruptedException, JSONException, TimeoutException {
|
||||
mockWebServer.enqueue(new MockResponse().
|
||||
setResponseCode(200).
|
||||
setBody("{\"error\":[\"EQuery:Unknown asset pair\"]}"));
|
||||
exchangeApi.queryExchangeRate("XMR", "ABC", new ExchangeCallback() {
|
||||
@Override
|
||||
public void onSuccess(final ExchangeRate exchangeRate) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.assertTrue(e instanceof ExchangeException);
|
||||
ExchangeException ex = (ExchangeException) e;
|
||||
waiter.assertTrue(ex.getCode() == 200);
|
||||
waiter.assertEquals(ex.getErrorMsg(), "EQuery:Unknown asset pair");
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
static public String createMockExchangeRateResponse(final String base, final String quote, final double rate) {
|
||||
return "{\n" +
|
||||
" \"error\":[],\n" +
|
||||
" \"result\":{\n" +
|
||||
" \"X" + base + "Z" + quote + "\":{\n" +
|
||||
" \"a\":[\"" + rate + "\",\"322\",\"322.000\"],\n" +
|
||||
" \"b\":[\"" + rate + "\",\"76\",\"76.000\"],\n" +
|
||||
" \"c\":[\"" + rate + "\",\"2.90000000\"],\n" +
|
||||
" \"v\":[\"4559.03962053\",\"5231.33235586\"],\n" +
|
||||
" \"p\":[\"" + rate + "\",\"" + rate + "\"],\n" +
|
||||
" \"t\":[801,1014],\n" +
|
||||
" \"l\":[\"" + (rate * 0.8) + "\",\"" + rate + "\"],\n" +
|
||||
" \"h\":[\"" + (rate * 1.2) + "\",\"" + rate + "\"],\n" +
|
||||
" \"o\":\"" + rate + "\"\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
"}";
|
||||
}
|
||||
}
|
@ -1,213 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 m2049r et al.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.m2049r.xmrwallet.service.shift.sideshift;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import com.m2049r.xmrwallet.service.shift.ShiftCallback;
|
||||
import com.m2049r.xmrwallet.service.shift.ShiftException;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.CreateOrder;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.api.SideShiftApi;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
import com.m2049r.xmrwallet.util.ServiceHelper;
|
||||
|
||||
import net.jodah.concurrentunit.Waiter;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
|
||||
public class SideShiftApiCreateOrderTest {
|
||||
|
||||
private MockWebServer mockWebServer;
|
||||
|
||||
private SideShiftApi xmrToApi;
|
||||
|
||||
private Waiter waiter;
|
||||
|
||||
@Mock
|
||||
ShiftCallback<CreateOrder> mockOrderXmrToCallback;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
mockWebServer = new MockWebServer();
|
||||
mockWebServer.start();
|
||||
|
||||
waiter = new Waiter();
|
||||
|
||||
MockitoAnnotations.initMocks(this);
|
||||
NetCipherHelper.Request.mockClient = new OkHttpClient();
|
||||
xmrToApi = new SideShiftApiImpl(mockWebServer.url("/"));
|
||||
ServiceHelper.ASSET = "btc"; // all tests run with BTC
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
mockWebServer.shutdown();
|
||||
ServiceHelper.ASSET = null;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createOrder_shouldBePostMethod()
|
||||
throws InterruptedException {
|
||||
|
||||
xmrToApi.createOrder("01234567-89ab-cdef-0123-456789abcdef", "19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW", mockOrderXmrToCallback);
|
||||
|
||||
RecordedRequest request = mockWebServer.takeRequest();
|
||||
assertEquals("POST", request.getMethod());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createOrder_shouldBeContentTypeJson()
|
||||
throws InterruptedException {
|
||||
|
||||
xmrToApi.createOrder("01234567-89ab-cdef-0123-456789abcdef", "19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW", mockOrderXmrToCallback);
|
||||
|
||||
RecordedRequest request = mockWebServer.takeRequest();
|
||||
assertEquals("application/json; charset=utf-8", request.getHeader("Content-Type"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createOrder_shouldContainValidBody()
|
||||
throws InterruptedException {
|
||||
|
||||
final String validBody = "{\"settleAddress\":\"19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW\",\"type\":\"fixed\",\"quoteId\":\"01234567-89ab-cdef-0123-456789abcdef\"}";
|
||||
|
||||
xmrToApi.createOrder("01234567-89ab-cdef-0123-456789abcdef", "19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW", mockOrderXmrToCallback);
|
||||
|
||||
RecordedRequest request = mockWebServer.takeRequest();
|
||||
String body = request.getBody().readUtf8();
|
||||
assertEquals(validBody, body);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createOrder_wasSuccessfulShouldRespondWithOrder()
|
||||
throws TimeoutException, InterruptedException {
|
||||
final double btcAmount = 1.23456789;
|
||||
final String btcAddress = "19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW";
|
||||
final double xmrAmount = 0.6;
|
||||
final String quoteId = "01234567-89ab-cdef-0123-456789abcdef";
|
||||
final String orderId = "09090909090909090911";
|
||||
MockResponse jsonMockResponse = new MockResponse().setBody(
|
||||
createMockCreateOrderResponse(btcAmount, btcAddress, xmrAmount, quoteId, orderId));
|
||||
mockWebServer.enqueue(jsonMockResponse);
|
||||
xmrToApi.createOrder(quoteId, btcAddress, new ShiftCallback<CreateOrder>() {
|
||||
@Override
|
||||
public void onSuccess(final CreateOrder order) {
|
||||
waiter.assertEquals(order.getBtcAmount(), btcAmount);
|
||||
waiter.assertEquals(order.getBtcAddress(), btcAddress);
|
||||
waiter.assertEquals(order.getQuoteId(), quoteId);
|
||||
waiter.assertEquals(order.getOrderId(), orderId);
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.fail(e);
|
||||
waiter.resume();
|
||||
}
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createOrder_wasNotSuccessfulShouldCallOnError()
|
||||
throws TimeoutException, InterruptedException {
|
||||
mockWebServer.enqueue(new MockResponse().setResponseCode(500));
|
||||
xmrToApi.createOrder("01234567-89ab-cdef-0123-456789abcdef", "19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW", new ShiftCallback<CreateOrder>() {
|
||||
@Override
|
||||
public void onSuccess(final CreateOrder order) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.assertTrue(e instanceof ShiftException);
|
||||
waiter.assertTrue(((ShiftException) e).getCode() == 500);
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createOrder_malformedAddressShouldCallOnError()
|
||||
throws TimeoutException, InterruptedException {
|
||||
mockWebServer.enqueue(new MockResponse().
|
||||
setResponseCode(500).
|
||||
setBody("{\"error\":{\"message\":\"Invalid settleDestination\"}}"));
|
||||
xmrToApi.createOrder("01234567-89ab-cdef-0123-456789abcdef", "xxx", new ShiftCallback<CreateOrder>() {
|
||||
@Override
|
||||
public void onSuccess(final CreateOrder order) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.assertTrue(e instanceof ShiftException);
|
||||
ShiftException xmrEx = (ShiftException) e;
|
||||
waiter.assertTrue(xmrEx.getCode() == 500);
|
||||
waiter.assertNotNull(xmrEx.getError());
|
||||
waiter.assertEquals(xmrEx.getError().getErrorMsg(), "Invalid settleDestination");
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
private String createMockCreateOrderResponse(final double btcAmount, final String btcAddress,
|
||||
final double xmrAmount,
|
||||
final String quoteId, final String orderId) {
|
||||
|
||||
return "{\"createdAt\":\"1612705584613\"," +
|
||||
"\"createdAtISO\":\"2021-02-07T13:46:24.613Z\"," +
|
||||
"\"expiresAt\":\"1612706453080\"," +
|
||||
"\"expiresAtISO\":\"2021-02-07T14:00:53.080Z\"," +
|
||||
"\"depositAddress\":{" +
|
||||
"\"address\":\"4Bh68jCUZGHbVu45zCVvtcMYesHuduwgajoQcdYRjUQcY6MNa8qd67vTfSNWdtrc33dDECzbPCJeQ8HbiopdeM7Ej8MBVLCYz6cVqy6utz\"," +
|
||||
"\"paymentId\":\"dbe876f0374db1ff\"," +
|
||||
"\"integratedAddress\":\"4Bh68jCUZGHbVu45zCVvtcMYesHuduwgajoQcdYRjUQcY6MNa8qd67vTfSNWdtrc33dDECzbPCJeQ8HbiopdeM7Ej8MBVLCYz6cVqy6utz\"" +
|
||||
"}," +
|
||||
"\"depositMethodId\":\"xmr\"," +
|
||||
"\"id\":\"" + orderId + "\"," +
|
||||
"\"orderId\":\"" + orderId + "\"," +
|
||||
"\"settleAddress\":{" +
|
||||
"\"address\":\"" + btcAddress + "\"" +
|
||||
"}," +
|
||||
"\"settleMethodId\":\"btc\"," +
|
||||
"\"depositMax\":\"" + xmrAmount + "\"," +
|
||||
"\"depositMin\":\"" + xmrAmount + "\"," +
|
||||
"\"quoteId\":\"" + quoteId + "\"," +
|
||||
"\"settleAmount\":\"" + btcAmount + "\"," +
|
||||
"\"depositAmount\":\"" + xmrAmount + "\"," +
|
||||
"\"deposits\":[]}";
|
||||
}
|
||||
}
|
@ -1,169 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 m2049r et al.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.m2049r.xmrwallet.service.shift.sideshift;
|
||||
|
||||
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.ShiftCallback;
|
||||
import com.m2049r.xmrwallet.service.shift.sideshift.network.SideShiftApiImpl;
|
||||
import com.m2049r.xmrwallet.util.NetCipherHelper;
|
||||
|
||||
import net.jodah.concurrentunit.Waiter;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class SideShiftApiOrderParameterTest {
|
||||
|
||||
private MockWebServer mockWebServer;
|
||||
|
||||
private SideShiftApi xmrToApi;
|
||||
|
||||
private Waiter waiter;
|
||||
|
||||
@Mock
|
||||
ShiftCallback<QueryOrderParameters> mockParametersXmrToCallback;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
mockWebServer = new MockWebServer();
|
||||
mockWebServer.start();
|
||||
|
||||
waiter = new Waiter();
|
||||
|
||||
MockitoAnnotations.initMocks(this);
|
||||
NetCipherHelper.Request.mockClient = new OkHttpClient();
|
||||
xmrToApi = new SideShiftApiImpl(mockWebServer.url("/"));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
mockWebServer.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void orderParameter_shouldBeGetMethod()
|
||||
throws InterruptedException {
|
||||
|
||||
xmrToApi.queryOrderParameters(mockParametersXmrToCallback);
|
||||
|
||||
RecordedRequest request = mockWebServer.takeRequest();
|
||||
assertEquals("GET", request.getMethod());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void orderParameter_wasSuccessfulShouldRespondWithParameters()
|
||||
throws TimeoutException, InterruptedException {
|
||||
final double rate = 0.015537;
|
||||
final double upperLimit = 20.0;
|
||||
final double lowerLimit = 0.001;
|
||||
|
||||
MockResponse jsonMockResponse = new MockResponse().setBody(
|
||||
createMockOrderParameterResponse(rate, upperLimit, lowerLimit));
|
||||
mockWebServer.enqueue(jsonMockResponse);
|
||||
|
||||
xmrToApi.queryOrderParameters(new ShiftCallback<QueryOrderParameters>() {
|
||||
@Override
|
||||
public void onSuccess(final QueryOrderParameters orderParameter) {
|
||||
waiter.assertEquals(orderParameter.getLowerLimit(), lowerLimit);
|
||||
waiter.assertEquals(orderParameter.getUpperLimit(), upperLimit);
|
||||
waiter.assertEquals(orderParameter.getPrice(), rate);
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.fail(e);
|
||||
waiter.resume();
|
||||
}
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void orderParameter_wasNotSuccessfulShouldCallOnError()
|
||||
throws TimeoutException, InterruptedException {
|
||||
mockWebServer.enqueue(new MockResponse().setResponseCode(500));
|
||||
xmrToApi.queryOrderParameters(new ShiftCallback<QueryOrderParameters>() {
|
||||
@Override
|
||||
public void onSuccess(final QueryOrderParameters orderParameter) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.assertTrue(e instanceof ShiftException);
|
||||
waiter.assertTrue(((ShiftException) e).getCode() == 500);
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void orderParameter_SettleMethodInvalidShouldCallOnError()
|
||||
throws TimeoutException, InterruptedException {
|
||||
mockWebServer.enqueue(new MockResponse().
|
||||
setResponseCode(500).
|
||||
setBody("{\"error\":{\"message\":\"Settle method not found\"}}"));
|
||||
xmrToApi.queryOrderParameters(new ShiftCallback<QueryOrderParameters>() {
|
||||
@Override
|
||||
public void onSuccess(final QueryOrderParameters orderParameter) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.assertTrue(e instanceof ShiftException);
|
||||
ShiftException xmrEx = (ShiftException) e;
|
||||
waiter.assertTrue(xmrEx.getCode() == 500);
|
||||
waiter.assertNotNull(xmrEx.getError());
|
||||
waiter.assertEquals(xmrEx.getError().getErrorMsg(), "Settle method not found");
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
private String createMockOrderParameterResponse(
|
||||
final double rate,
|
||||
final double upperLimit,
|
||||
final double lowerLimit) {
|
||||
return "{\n" +
|
||||
" \"rate\": \"" + rate + "\",\n" +
|
||||
" \"min\": \"" + lowerLimit + "\",\n" +
|
||||
" \"max\": \"" + upperLimit + "\"\n" +
|
||||
"}";
|
||||
}
|
||||
}
|
@ -1,192 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2017 m2049r et al.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.m2049r.xmrwallet.service.shift.sideshift;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
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.util.NetCipherHelper;
|
||||
|
||||
import net.jodah.concurrentunit.Waiter;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
|
||||
public class SideShiftApiQueryOrderStatusTest {
|
||||
private MockWebServer mockWebServer;
|
||||
|
||||
private SideShiftApi xmrToApi;
|
||||
|
||||
private Waiter waiter;
|
||||
|
||||
@Mock
|
||||
ShiftCallback<QueryOrderStatus> mockQueryXmrToCallback;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
mockWebServer = new MockWebServer();
|
||||
mockWebServer.start();
|
||||
|
||||
waiter = new Waiter();
|
||||
|
||||
MockitoAnnotations.initMocks(this);
|
||||
NetCipherHelper.Request.mockClient = new OkHttpClient();
|
||||
xmrToApi = new SideShiftApiImpl(mockWebServer.url("/"));
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() throws Exception {
|
||||
mockWebServer.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void orderStatus_shouldBePostMethod()
|
||||
throws InterruptedException {
|
||||
|
||||
xmrToApi.queryOrderStatus("09090909090909090911", mockQueryXmrToCallback);
|
||||
|
||||
RecordedRequest request = mockWebServer.takeRequest();
|
||||
assertEquals("GET", request.getMethod());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void orderStatus_wasSuccessfulShouldRespondWithOrder()
|
||||
throws TimeoutException, InterruptedException {
|
||||
|
||||
final String state = "settled";
|
||||
final String orderId = "09090909090909090911";
|
||||
|
||||
MockResponse jsonMockResponse = new MockResponse().setBody(
|
||||
createMockQueryOrderResponse(
|
||||
state,
|
||||
orderId));
|
||||
mockWebServer.enqueue(jsonMockResponse);
|
||||
|
||||
xmrToApi.queryOrderStatus(orderId, new ShiftCallback<QueryOrderStatus>() {
|
||||
@Override
|
||||
public void onSuccess(final QueryOrderStatus orderStatus) {
|
||||
waiter.assertEquals(orderStatus.getOrderId(), orderId);
|
||||
waiter.assertEquals(orderStatus.getState(), QueryOrderStatus.State.SETTLED);
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.fail(e);
|
||||
waiter.resume();
|
||||
}
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void orderStatus_wasNotSuccessfulShouldCallOnError()
|
||||
throws TimeoutException, InterruptedException {
|
||||
mockWebServer.enqueue(new MockResponse().setResponseCode(500));
|
||||
xmrToApi.queryOrderStatus("09090909090909090911", new ShiftCallback<QueryOrderStatus>() {
|
||||
@Override
|
||||
public void onSuccess(final QueryOrderStatus orderStatus) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.assertTrue(e instanceof ShiftException);
|
||||
waiter.assertTrue(((ShiftException) e).getCode() == 500);
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void orderStatus_orderNotFoundShouldCallOnError()
|
||||
throws TimeoutException, InterruptedException {
|
||||
mockWebServer.enqueue(new MockResponse().
|
||||
setResponseCode(500).
|
||||
setBody("{\"error\":{\"message\":\"Order not found\"}}"));
|
||||
xmrToApi.queryOrderStatus("09090909090909090911", new ShiftCallback<QueryOrderStatus>() {
|
||||
@Override
|
||||
public void onSuccess(final QueryOrderStatus orderStatus) {
|
||||
waiter.fail();
|
||||
waiter.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final Exception e) {
|
||||
waiter.assertTrue(e instanceof ShiftException);
|
||||
ShiftException xmrEx = (ShiftException) e;
|
||||
waiter.assertTrue(xmrEx.getCode() == 500);
|
||||
waiter.assertNotNull(xmrEx.getError());
|
||||
waiter.assertEquals(xmrEx.getError().getErrorMsg(), "Order not found");
|
||||
waiter.resume();
|
||||
}
|
||||
});
|
||||
waiter.await();
|
||||
}
|
||||
|
||||
private String createMockQueryOrderResponse(
|
||||
final String state,
|
||||
final String orderId) {
|
||||
return "{\"createdAt\":\"1612700947550\"," +
|
||||
"\"createdAtISO\":\"2021-02-07T12:29:07.550Z\"," +
|
||||
"\"expiresAt\":\"1612701817682\"," +
|
||||
"\"expiresAtISO\":\"2021-02-07T12:43:37.682Z\"," +
|
||||
"\"depositAddress\":" +
|
||||
"{\"address\":\"4Bh68jCUZGHbVu45zCVvtcMYesHuduwgajoQcdYRjUQcY6MNa8qd67vTfSNWdtrc33dDECzbPCJeQ8HbiopdeM7Ej2DVsSohug9QxMJnN2\",\"paymentId\":\"3a151908242c6ed4\",\"integratedAddress\":\"4Bh68jCUZGHbVu45zCVvtcMYesHuduwgajoQcdYRjUQcY6MNa8qd67vTfSNWdtrc33dDECzbPCJeQ8HbiopdeM7Ej2DVsSohug9QxMJnN2\"}," +
|
||||
"\"depositMethodId\":\"xmr\"," +
|
||||
"\"id\":\"" + orderId + "\"," +
|
||||
"\"orderId\":\"" + orderId + "\"," +
|
||||
"\"settleAddress\":{\"address\":\"19y91nJyzXsLEuR7Nj9pc3o5SeHNc8A9RW\"}," +
|
||||
"\"settleMethodId\":\"btc\"," +
|
||||
"\"depositMax\":\"0.01\",\"depositMin\":\"0.01\"," +
|
||||
"\"quoteId\":\"01234567-89ab-cdef-0123-456789abcdef\"," +
|
||||
"\"settleAmount\":\"0.008108\"," +
|
||||
"\"depositAmount\":\"0.01\"," +
|
||||
"\"deposits\":[" +
|
||||
"{\"createdAt\":\"1612701112634\",\"createdAtISO\":\"2021-02-07T12:31:52.634Z\"," +
|
||||
"\"depositAmount\":\"0.01\"," +
|
||||
"\"depositTx\":{\"type\":\"monero\",\"txHash\":\"a0b674f6033f5f5398dacea9dddedf8d12e35f46c29dfeaf5fac724d7c678fb7\",\"transferIndex\":0}," +
|
||||
"\"depositId\":\"3e700e108fb31a4b1f7f\"," +
|
||||
"\"id\":\"3e700e108fb31a4b1f7f\"," +
|
||||
"\"status\":\"" + state + "\"," +
|
||||
"\"refundAddress\":null,\"refundTx\":null," +
|
||||
"\"settleAmount\":\"0.008108\"," +
|
||||
"\"settleRate\":\"0.810756\"," +
|
||||
"\"settleTx\":{\"type\":\"bitcoin\",\"txHash\":\"7bd5d0c3daac6a087ddf81411c8135fae55078334780debe47df775d596d4561\"}," +
|
||||
"\"orderId\":\"" + orderId + "\"" +
|
||||
"}" + // deposits[0]
|
||||
"]" + // deposits
|
||||
"}";
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user