Remove a lot of classes and cleanup code

This commit is contained in:
pokkst 2022-09-16 20:19:01 -05:00
parent 460f6bd1b2
commit 209984417b
No known key found for this signature in database
GPG key ID: 90C2ED85E67A50FF
105 changed files with 1047 additions and 10100 deletions

View file

@ -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"

View file

@ -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>&apos;Poppins&apos; 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>&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-<br />
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007<br />
&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-</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>&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-<br/>
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007<br/>
&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;-
</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/>
&#8220;Font Software&#8221; 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>&#8220;Author&#8221; refers to any designer, engineer, programmer, technical writer or other
person who contributed to the Font Software.</p>
<p>PERMISSION &amp; CONDITIONS<br />
<p>PERMISSION &amp; 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 &#8220;AS IS&#8221;, 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

View file

@ -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;
@ -510,8 +508,8 @@ Java_com_m2049r_xmrwallet_model_WalletManager_startMining(JNIEnv *env, jobject i
const char *_address = env->GetStringUTFChars(address, nullptr);
bool success =
Monero::WalletManagerFactory::getWalletManager()->startMining(std::string(_address),
background_mining,
ignore_battery);
background_mining,
ignore_battery);
env->ReleaseStringUTFChars(address, _address);
return static_cast<jboolean>(success);
}
@ -537,7 +535,7 @@ Java_com_m2049r_xmrwallet_model_WalletManager_resolveOpenAlias(JNIEnv *env, jobj
JNIEXPORT jboolean JNICALL
Java_com_m2049r_xmrwallet_model_WalletManager_setProxyJ(JNIEnv *env, jobject instance,
jstring address) {
jstring address) {
const char *_address = env->GetStringUTFChars(address, nullptr);
bool rc =
Monero::WalletManagerFactory::getWalletManager()->setProxy(std::string(_address));
@ -553,7 +551,7 @@ Java_com_m2049r_xmrwallet_model_WalletManager_closeJ(JNIEnv *env, jobject instan
jobject walletInstance) {
Monero::Wallet *wallet = getHandle<Monero::Wallet>(env, walletInstance);
bool closeSuccess = Monero::WalletManagerFactory::getWalletManager()->closeWallet(wallet,
false);
false);
if (closeSuccess) {
MyWalletListener *walletListener = getHandle<MyWalletListener>(env, walletInstance,
"listenerHandle");
@ -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);
@ -968,9 +967,9 @@ Java_com_m2049r_xmrwallet_model_Wallet_createTransactionJ(JNIEnv *env, jobject i
Monero::Wallet *wallet = getHandle<Monero::Wallet>(env, instance);
Monero::PendingTransaction *tx = wallet->createTransaction(_dst_addr, _payment_id,
amount, (uint32_t) mixin_count,
_priority,
(uint32_t) accountIndex);
amount, (uint32_t) mixin_count,
_priority,
(uint32_t) accountIndex);
env->ReleaseStringUTFChars(dst_addr, _dst_addr);
env->ReleaseStringUTFChars(payment_id, _payment_id);
@ -993,9 +992,9 @@ Java_com_m2049r_xmrwallet_model_Wallet_createSweepTransaction(JNIEnv *env, jobje
Monero::optional<uint64_t> empty;
Monero::PendingTransaction *tx = wallet->createTransaction(_dst_addr, _payment_id,
empty, (uint32_t) mixin_count,
_priority,
(uint32_t) accountIndex);
empty, (uint32_t) mixin_count,
_priority,
(uint32_t) accountIndex);
env->ReleaseStringUTFChars(dst_addr, _dst_addr);
env->ReleaseStringUTFChars(payment_id, _payment_id);
@ -1205,7 +1204,7 @@ Java_com_m2049r_xmrwallet_model_Wallet_getLastSubaddress(JNIEnv *env, jobject in
JNIEXPORT jint JNICALL
Java_com_m2049r_xmrwallet_model_TransactionHistory_getCount(JNIEnv *env, jobject instance) {
Monero::TransactionHistory *history = getHandle<Monero::TransactionHistory>(env,
instance);
instance);
return history->count();
}
@ -1291,7 +1290,7 @@ jobject cpp2java(JNIEnv *env, const std::vector<Monero::TransactionInfo *> &vect
JNIEXPORT jobject JNICALL
Java_com_m2049r_xmrwallet_model_TransactionHistory_refreshJ(JNIEnv *env, jobject instance) {
Monero::TransactionHistory *history = getHandle<Monero::TransactionHistory>(env,
instance);
instance);
history->refresh();
return cpp2java(env, history->getAll());
}

View file

@ -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

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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();
}
}

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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() ? "" : ("&nbsp; " + 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() ? "" : ("&nbsp; " + 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));
}
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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());
private Address(InetAddress address, String onion) {
this.inet = address;
this.onion = onion;
}
return DEFAULT_LEVIN_PORT;
}
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;
}
}

View file

@ -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() ? "&nbsp;.onion&nbsp;&nbsp;" : ""), " " + 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);
}
}

View file

@ -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);

View file

@ -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;

View file

@ -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);
}
}
}

View file

@ -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);

View file

@ -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();
}
}

View file

@ -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();
}
}
}

View file

@ -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);

View file

@ -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 {
}

View file

@ -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);

View file

@ -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() {

View file

@ -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");

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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);

View file

@ -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;
}
}
}

View file

@ -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;

View file

@ -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;
}
boolean synced = false;
private int accountIndex = 0;
private long handle = 0;
private long listenerHandle = 0;
private PendingTransaction pendingTransaction = null;
private TransactionHistory history = null;
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;
}
Wallet(long handle) {
this.handle = handle;
}
private int accountIndex = 0;
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;
}
}
}

View file

@ -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());
}
}
}

View file

@ -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;
}
}

View file

@ -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());

View file

@ -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;
}

View file

@ -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());
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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
}
}
}

View file

@ -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;
}
}

View file

@ -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));

View file

@ -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);
}
}
}

View file

@ -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();
}
}

View file

@ -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,

View file

@ -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));
}
}

View file

@ -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 {

View file

@ -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();
}
}

View file

@ -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);
}
}
}

View file

@ -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"));

View file

@ -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());
}
}

View file

@ -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) + ")";
}
}

View file

@ -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

View file

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

View file

@ -1,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;
}
}

View file

@ -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;
}
}

View file

@ -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));
}
}
}
}
}

View file

@ -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;
}
}

View file

@ -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());
}
}

View file

@ -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());
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
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>

View file

@ -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" />

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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"

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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">&lt;span style=\"background-color: #%1$s; color: #%2$s;\"&gt;&nbsp;%3$s&nbsp;&lt;/span&gt;%4$s</string>
<string name="menu_restore">Import wallet</string>

View file

@ -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>

View file

@ -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>";
}
}

View file

@ -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" +
"}";
}
}

View file

@ -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\":[]}";
}
}

View file

@ -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" +
"}";
}
}

View file

@ -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