1
mirror of https://github.com/m2049r/xmrwallet synced 2025-09-04 00:53:36 +02:00

Compare commits

..

66 Commits

Author SHA1 Message Date
m2049r
7fc2dc3ba1 bump version code 2018-08-08 22:10:39 +02:00
m2049r
43204d64ef Interim merge of Ledger translations (#383)
* Ledger french translation (#359)

* zh-TW translation for Ledger (#368, #373)

* Hungarian Ledgers strings added (#372)

* German translation

* update help about entering height
2018-08-08 22:10:28 +02:00
m2049r
1433143a39 Use NFC for receive & send (#380)
* Use NFC Tag for receive & send

* prep translations
2018-08-06 22:18:57 +02:00
m2049r
a9a78393a9 clear amounts when typing (#379) 2018-08-05 23:26:01 +02:00
m2049r
fe7ab31050 getRestoreHeight (#378) 2018-08-05 23:25:41 +02:00
m2049r
bf5ed793b3 Monerujo for Ledger Nano S (#377) 2018-08-04 01:03:26 +02:00
m2049r
679bae5f42 update de strings.xml (#376) 2018-08-02 11:11:57 +02:00
m2049r
e3ccda910e update hu translation (#375) 2018-08-02 11:05:09 +02:00
0140454
ae75a34977 Improve method of enumerating available locales (#374)
* Change method of enumerating translated locales
2018-08-02 10:51:10 +02:00
0140454
03efedf35c Implement language switcher (#344)
* Implement language switcher

* Use unique string resource to enumerate translated locale
2018-08-01 14:18:54 +02:00
m2049r
403dbdf14f update restoreheightdate handling (#370)
* restore date without dashes

* heights upated to august 2018
2018-08-01 08:25:25 +02:00
Attila
0bf3c6f099 Hungarian translation (#371) 2018-07-31 22:39:17 +02:00
0140454
1b0ac1c481 Show message for recoverable error encountered during authentication (#343) 2018-07-30 10:25:42 +02:00
uiharu
dc95539fc1 Update zh-rTW "strings.xml" (#336)
* Update zh-rTW "strings.xml

- translated new strings
- correction and editing

* Update strings.xml
2018-07-29 23:41:29 +02:00
0140454
023fb9e215 Wrap change password layout with ScrollView (Fix #346) (#349) 2018-07-28 17:06:56 +02:00
m2049r
92728026c7 greek is el (#365) 2018-07-28 16:55:53 +02:00
m2049r
8fd9598c6c increase Xmx for gradle (#364) 2018-07-27 16:20:00 +02:00
m2049r
6633261ba2 cleaned ru & sv translations (#363)
* removed unused strings
* added new strings
2018-07-27 13:06:03 +02:00
uiharu
dc8c8634cb Update strings.xml (#337)
change one instance of "xmr.to" to "XMR.TO" for consistency
2018-07-27 12:10:55 +02:00
v1docq47
dcf9b6db15 translate to russian language #244 (#278)
* Update strings.xml

* Update about.xml

* Update help.xml
2018-07-27 12:10:13 +02:00
Leza89
654d63c32e Added missing translations, corrected some grammar, tried to make it more newbie friendly and shorter (#332)
(i.e. No german speaker will know TX as "transaction" - "Empfangsadresse" instead of "Öffentliche Adresse" should make it more clear that this is where your money goes to)

strings.xml Translation[German]
2018-07-27 12:06:39 +02:00
noxxi
caf91fccfd Swedish translation #236 (#265)
* Translated to SE

All paragraphs and headers have been translated, with the exception of paragraphs and headers included in a string with a translatable="false" attribute.

* Swedish - Task #236: 1/3 complete

About.xml is fully translated, until further notice

*  Swedish - Task #236: 2/3 complete 

Help.xml is fully translated, until further notice

*  Swedish - Task #236: 3/3 complete 

Strings.xml is fully translated, until further notice

* Swedish - Task #236: 1/3 complete

about.xml is fully translated, until further notice
2018-07-27 12:03:06 +02:00
Ordtrogen
41f1f3dec0 Swedish translation of strings.xml (#261)
* Swedish translation of strings.xml

* removed untranslatable strings

* added some missing strings
2018-07-27 11:57:03 +02:00
m2049r
bb66d1f68d cleaned gr & ro translations (#360)
- removed unused strings
- added new strings
2018-07-27 11:49:21 +02:00
AlexUnderHood
310548b031 Romanian translation - strings.xml (#241) 2018-07-27 10:46:11 +02:00
AlexUnderHood
ad7737475f Romanian translation - help.xml (#240) 2018-07-27 10:45:51 +02:00
AlexUnderHood
b2d07a65b7 Romanian translation - about.xml (#239) 2018-07-27 10:45:35 +02:00
gerasimos7777
5dfcaae5b9 Greek translation (#256)
* Create strings.xml

* Create help.xml

* Update strings.xml

* Update help.xml
2018-07-27 10:44:29 +02:00
el00ruobuob
3c91fc060c Fix "restore from seeds" too long on certain devices. (#345)
+ change "restore from key" for consistency.
2018-07-27 10:43:38 +02:00
uiharu
f8f113faab Update zh-rCN "strings.xml" (#338)
- basically mirrors changes for zh-rTW
- fill in new strings
- correct and edit
2018-07-27 10:36:38 +02:00
m2049r
268a00cb3e new version code 2018-07-26 08:42:48 +02:00
m2049r
878500ae71 hamburger on sync only in wallet screen (#357) 2018-07-26 08:42:26 +02:00
m2049r
a0debb0f7e Upgrade Monero Core to v0.12.3.0 (#356)
* upgrade v0.12.3.0 monero api

* new version code
2018-07-24 18:20:27 +02:00
m2049r
3a71e8d352 fix path of build script 2018-06-22 22:43:31 +02:00
m2049r
af68c5e51f new version 1.5.10 2018-06-22 22:11:49 +02:00
m2049r
5a2b48a087 fix displayed subaddress # 2018-06-22 22:09:44 +02:00
m2049r
0776b7b6a3 avoid NPE (#330) 2018-06-21 22:15:14 +02:00
m2049r
c2eed85a83 v1.5.9 2018-06-21 21:51:39 +02:00
m2049r
d7c2b4a727 restore deleted it/string 2018-06-21 20:22:54 +02:00
erciccione
085e41f5da update italian strings (#323) 2018-06-21 20:20:43 +02:00
Re-Diculous
6c6b3061a8 Update strings.xml (#325) 2018-06-21 20:19:32 +02:00
m2049r
5fc15779b7 Update Translations (#329)
* Portuguese

* update italian

* update german
2018-06-21 20:14:30 +02:00
m2049r
520d151f3c new subaddress bump effect (#327) 2018-06-20 18:33:28 +02:00
m2049r
7aad941dab new version v1.5.8 2018-06-18 09:43:11 +02:00
m2049r
01e7693425 subaddresses (#322) 2018-06-18 09:43:32 +02:00
m2049r
091538752b add our stagenet server to defaults (#321) 2018-06-17 13:53:01 +02:00
m2049r
d5b95dd976 new version v1.5.7 2018-06-17 13:31:33 +02:00
m2049r
008f06959c remove mixin settings (#320) 2018-06-17 13:22:29 +02:00
m2049r
817816cd34 Layout tweaks (#319)
* sweep layout tweak

* reduce label to fit

* change sweep message
2018-06-17 13:21:59 +02:00
m2049r
9d41d5da52 new version v1.5.6 2018-06-16 15:27:28 +02:00
m2049r
ad76a7ffc1 Fixes to v1.5.5 (#318)
* fix disappearing hamburger

* german fixes to fit space
2018-06-16 15:20:13 +02:00
el00ruobuob
475542c4f3 French Strings updated to #297 (#316)
+ Missing update to #312
+ enhancement on "wrapping things up"
2018-06-16 15:19:40 +02:00
m2049r
b5d0659ca9 sweep all (#317) 2018-06-16 15:17:48 +02:00
m2049r
781bfbc78b Update FAQ.md 2018-06-14 23:40:41 +02:00
m2049r
8985511209 fix length test (#314) 2018-06-14 22:28:33 +02:00
m2049r
3c8a4ce967 Merge branch 'master' of https://github.com/m2049r/xmrwallet 2018-06-14 21:59:48 +02:00
m2049r
fcfedbcfae Fix balance (#313)
* new version

* fix balance
2018-06-14 22:00:15 +02:00
m2049r
74279b135a new version 2018-06-14 21:37:19 +02:00
0140454
d6d2de8312 Inform user the progress or result of opening wallet (#297) 2018-06-14 21:33:23 +02:00
m2049r
af0ecb2894 accounts (#312) 2018-06-14 21:32:52 +02:00
m2049r
975cc4f43c show correct amount for pending tx (#309) 2018-06-11 10:21:22 +02:00
0140454
74ba36de26 Use FingerprintManager instead of FingerprintManagerCompat (Fix #300) (#302) 2018-06-10 10:59:20 +02:00
m2049r
7627e15a48 Fix keystore null (#308)
* avoid crash if input to large

* avoid NPE if wallet key not found
2018-06-10 10:57:24 +02:00
m2049r
37244cb9e0 coinmarketcap for exchange rates (#304) 2018-06-10 10:56:46 +02:00
m2049r
843566b820 spinner tweaks (#306) 2018-06-09 12:42:09 +02:00
m2049r
0bcf156929 update gradle version (#305) 2018-06-09 10:21:44 +02:00
229 changed files with 14035 additions and 1654 deletions

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
workspace.xml
markdown-*
misc.xml

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
xmrwallet

22
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<resourceExtensions />
<wildcardResourcePatterns>
<entry name="!?*.java" />
<entry name="!?*.form" />
<entry name="!?*.class" />
<entry name="!?*.groovy" />
<entry name="!?*.scala" />
<entry name="!?*.flex" />
<entry name="!?*.kt" />
<entry name="!?*.clj" />
<entry name="!?*.aj" />
</wildcardResourcePatterns>
<annotationProcessing>
<profile default="true" name="Default" enabled="false">
<processorPath useClasspath="true" />
</profile>
</annotationProcessing>
</component>
</project>

3
.idea/copyright/profiles_settings.xml generated Normal file
View File

@@ -0,0 +1,3 @@
<component name="CopyrightManager">
<settings default="" />
</component>

2
.idea/modules.xml generated
View File

@@ -2,8 +2,8 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/aeonwallet.iml" filepath="$PROJECT_DIR$/aeonwallet.iml" />
<module fileurl="file://$PROJECT_DIR$/app/app.iml" filepath="$PROJECT_DIR$/app/app.iml" />
<module fileurl="file://$PROJECT_DIR$/xmrwallet.iml" filepath="$PROJECT_DIR$/xmrwallet.iml" />
</modules>
</component>
</project>

View File

@@ -9,7 +9,7 @@ Another Android Monero Wallet for Monero
Monero V)**
### QUICKSTART
- Download the APK for the most current release [here](https://github.com/monerujo-io/aeonwallet/releases) and install it
- Download the APK for the most current release [here](https://github.com/m2049r/xmrwallet/releases) and install it
- Alternatively add our F-Droid repo https://f-droid.monerujo.io/fdroid/repo with fingerpint ```A8 2C 68 E1 4A F0 AA 6A 2E C2 0E 6B 27 2E FF 25 E5 A0 38 F3 F6 58 84 31 6E 0F 5E 0D 91 E7 B7 13``` to your F-Droid client
- Run the App and select "Generate Wallet" to create a new wallet or recover a wallet
- Advanced users can copy over synced wallet files (all files) onto sdcard in directory Monerujo (created first time App is started)

View File

@@ -60,84 +60,84 @@ set_target_properties(boost_wserialization PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/boost/lib/${ANDROID_ABI}/libboost_wserialization.a)
#############
# Aeon
# Monero
#############
add_library(wallet_api STATIC IMPORTED)
set_target_properties(wallet_api PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libwallet_api.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libwallet_api.a)
add_library(wallet STATIC IMPORTED)
set_target_properties(wallet PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libwallet.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libwallet.a)
add_library(cryptonote_core STATIC IMPORTED)
set_target_properties(cryptonote_core PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libcryptonote_core.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libcryptonote_core.a)
add_library(cryptonote_basic STATIC IMPORTED)
set_target_properties(cryptonote_basic PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libcryptonote_basic.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libcryptonote_basic.a)
add_library(mnemonics STATIC IMPORTED)
set_target_properties(mnemonics PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libmnemonics.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libmnemonics.a)
add_library(common STATIC IMPORTED)
set_target_properties(common PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libcommon.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libcommon.a)
add_library(cncrypto STATIC IMPORTED)
set_target_properties(cncrypto PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libcncrypto.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libcncrypto.a)
add_library(ringct STATIC IMPORTED)
set_target_properties(ringct PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libringct.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libringct.a)
add_library(ringct_basic STATIC IMPORTED)
set_target_properties(ringct_basic PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libringct_basic.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libringct_basic.a)
add_library(blockchain_db STATIC IMPORTED)
set_target_properties(blockchain_db PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libblockchain_db.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libblockchain_db.a)
add_library(lmdb STATIC IMPORTED)
set_target_properties(lmdb PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/liblmdb.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/liblmdb.a)
add_library(easylogging STATIC IMPORTED)
set_target_properties(easylogging PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libeasylogging.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libeasylogging.a)
add_library(unbound STATIC IMPORTED)
set_target_properties(unbound PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libunbound.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libunbound.a)
add_library(epee STATIC IMPORTED)
set_target_properties(epee PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libepee.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libepee.a)
add_library(blocks STATIC IMPORTED)
set_target_properties(blocks PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libblocks.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libblocks.a)
add_library(checkpoints STATIC IMPORTED)
set_target_properties(checkpoints PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libcheckpoints.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libcheckpoints.a)
add_library(device STATIC IMPORTED)
set_target_properties(device PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libdevice.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libdevice.a)
add_library(multisig STATIC IMPORTED)
set_target_properties(multisig PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libmultisig.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libmultisig.a)
add_library(version STATIC IMPORTED)
set_target_properties(version PROPERTIES IMPORTED_LOCATION
${EXTERNAL_LIBS_DIR}/aeon/lib/${ANDROID_ABI}/libversion.a)
${EXTERNAL_LIBS_DIR}/monero/lib/${ANDROID_ABI}/libversion.a)
#############
# System
@@ -145,7 +145,7 @@ set_target_properties(version PROPERTIES IMPORTED_LOCATION
find_library( log-lib log )
include_directories( ${EXTERNAL_LIBS_DIR}/aeon/include )
include_directories( ${EXTERNAL_LIBS_DIR}/monero/include )
message(STATUS EXTERNAL_LIBS_DIR : ${EXTERNAL_LIBS_DIR})

View File

@@ -4,11 +4,11 @@ android {
compileSdkVersion 25
buildToolsVersion '27.0.3'
defaultConfig {
applicationId "com.m2049r.aeonwallet"
applicationId "com.m2049r.xmrwallet"
minSdkVersion 21
targetSdkVersion 25
versionCode 10
versionName "0.1.0 'Rebase ALPHA'"
versionCode 113
versionName "1.6.3 'Nano S'"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
@@ -45,8 +45,17 @@ android {
// Map for the version code that gives each ABI a value.
def abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, 'x86': 3, 'x86_64': 4]
// Enumerate translated locales
def availableLocales = ["en"]
new File("app/src/main/res/").eachFileMatch(~/^values-.*/) { file ->
def languageTag = file.name.substring(7).replace("-r", "-")
availableLocales.add(languageTag)
}
// APKs for the same app that all have the same version information.
android.applicationVariants.all { variant ->
// Update string resource: available_locales
variant.resValue("string", "available_locales", availableLocales.join(","))
// Assigns a different version code for each output APK.
variant.outputs.all {
output ->

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">monerujoAE</string>
<string name="app_name" translatable="false">monerujo - Debug</string>
</resources>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.m2049r.aeonwallet">
package="com.m2049r.xmrwallet">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
@@ -8,13 +8,14 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.NFC" />
<application
android:allowBackup="true"
android:name=".XmrWalletApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:name=".XmrWalletApplication"
android:theme="@style/MyMaterialTheme">
<activity
@@ -28,19 +29,25 @@
android:name=".LoginActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/app_name"
android:launchMode="singleTop"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/usb_device_filter" />
</activity>
<service
android:name=".service.WalletService"
android:description="@string/service_description"
android:exported="false"
android:label="Wallet Service" />
android:label="Monero Wallet Service" />
</application>
</manifest>

File diff suppressed because it is too large Load Diff

View File

@@ -60,9 +60,14 @@ enum {
HASH_DATA_AREA = 136
};
void cn_slow_hash(const void *data, size_t length, char *hash, int light, int variant, int prehashed);
void cn_slow_hash(const void *data, size_t length, char *hash, int variant, int prehashed);
inline void slow_hash(const void *data, const size_t length, char *hash) {
cn_slow_hash(data, length, hash, 0 /*light*/, 0 /* variant */, 0/*prehashed*/);
cn_slow_hash(data, length, hash, 0 /* variant */, 0/*prehashed*/);
}
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*/);
}
#ifdef __cplusplus

View File

@@ -0,0 +1,46 @@
/**
* Copyright (c) 2018 m2049r
* <p>
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.
*/
#ifndef XMRWALLET_LEDGER_H
#define XMRWALLET_LEDGER_H
#ifdef __cplusplus
extern "C"
{
#endif
#define SCARD_S_SUCCESS ((LONG)0x00000000) /**< No error was encountered. */
#define SCARD_E_INSUFFICIENT_BUFFER ((LONG)0x80100008) /**< The data buffer to receive returned data is too small for the returned data. */
#define SCARD_E_NO_READERS_AVAILABLE ((LONG)0x8010002E) /**< Cannot find a smart card reader. */
typedef long LONG;
typedef unsigned long DWORD;
typedef DWORD *LPDWORD;
typedef unsigned char BYTE;
typedef BYTE *LPBYTE;
typedef const BYTE *LPCBYTE;
typedef char CHAR;
typedef CHAR *LPSTR;
int LedgerFind(char *buffer, size_t len);
LONG LedgerExchange(LPCBYTE pbSendBuffer, DWORD cbSendLength, LPBYTE pbRecvBuffer, LPDWORD pcbRecvLength);
#ifdef __cplusplus
}
#endif
#endif //XMRWALLET_LEDGER_H

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 245 KiB

View File

@@ -0,0 +1,53 @@
/*
*******************************************************************************
* 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

@@ -0,0 +1,31 @@
/*
*******************************************************************************
* 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 {
public byte[] exchange(byte[] command);
public void close();
public void setDebug(boolean debugFlag);
}

View File

@@ -0,0 +1,126 @@
/*
*******************************************************************************
* 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

@@ -0,0 +1,145 @@
/*
*******************************************************************************
* 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) && (device.getProductId() == PID_HID)) {
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_HID = 0x0001;
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

@@ -0,0 +1,62 @@
/*
*******************************************************************************
* 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,36 +0,0 @@
package com.m2049r.aeonwallet.util;
import android.app.KeyguardManager;
import android.content.Context;
import android.support.v4.hardware.fingerprint.FingerprintManagerCompat;
import android.support.v4.os.CancellationSignal;
import timber.log.Timber;
public class FingerprintHelper {
public static boolean isDeviceSupported(Context context) {
FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.from(context);
KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE);
return keyguardManager != null &&
keyguardManager.isKeyguardSecure() &&
fingerprintManager.isHardwareDetected() &&
fingerprintManager.hasEnrolledFingerprints();
}
public static boolean isFingerPassValid(Context context, String wallet) {
try {
KeyStoreHelper.loadWalletUserPass(context, wallet);
return true;
} catch (KeyStoreHelper.BrokenPasswordStoreException ex) {
return false;
}
}
public static void authenticate(Context context, CancellationSignal cancelSignal,
FingerprintManagerCompat.AuthenticationCallback callback) {
FingerprintManagerCompat manager = FingerprintManagerCompat.from(context);
manager.authenticate(null, 0, cancelSignal, callback, null);
}
}

View File

@@ -1,154 +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.aeonwallet.util;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
public class RestoreHeight {
static private RestoreHeight Singleton = null;
static public RestoreHeight getInstance() {
if (Singleton == null) {
synchronized (RestoreHeight.class) {
if (Singleton == null) {
Singleton = new RestoreHeight();
}
}
}
return Singleton;
}
private Map<String, Long> blockheight = new HashMap<>();
RestoreHeight() {
blockheight.put("2014-07-01", 33199L);
blockheight.put("2014-08-01", 76438L);
blockheight.put("2014-09-01", 119160L);
blockheight.put("2014-10-01", 159775L);
blockheight.put("2014-11-01", 202939L);
blockheight.put("2014-12-01", 245080L);
blockheight.put("2015-01-01", 287638L);
blockheight.put("2015-02-01", 330551L);
blockheight.put("2015-03-01", 368463L);
blockheight.put("2015-04-01", 412335L);
blockheight.put("2015-05-01", 454056L);
blockheight.put("2015-06-01", 498582L);
blockheight.put("2015-07-01", 541584L);
blockheight.put("2015-08-01", 586745L);
blockheight.put("2015-09-01", 601813L);
blockheight.put("2015-10-01", 612518L);
blockheight.put("2015-11-01", 623877L);
blockheight.put("2015-12-01", 634764L);
blockheight.put("2016-01-01", 645860L);
blockheight.put("2016-02-01", 656788L);
blockheight.put("2016-03-01", 666979L);
blockheight.put("2016-04-01", 678157L);
blockheight.put("2016-05-01", 688749L);
blockheight.put("2016-06-01", 699775L);
blockheight.put("2016-07-01", 710467L);
blockheight.put("2016-08-01", 721825L);
blockheight.put("2016-09-01", 733175L);
blockheight.put("2016-10-01", 743838L);
blockheight.put("2016-11-01", 755118L);
blockheight.put("2016-12-01", 765378L);
blockheight.put("2017-01-01", 776857L);
blockheight.put("2017-02-01", 787829L);
blockheight.put("2017-03-01", 797953L);
blockheight.put("2017-04-01", 808374L);
blockheight.put("2017-05-01", 819274L);
blockheight.put("2017-06-01", 830700L);
blockheight.put("2017-07-01", 841624L);
blockheight.put("2017-08-01", 852228L);
blockheight.put("2017-09-01", 863601L);
blockheight.put("2017-10-01", 874583L);
blockheight.put("2017-11-01", 885830L);
blockheight.put("2017-12-01", 896685L);
blockheight.put("2018-01-01", 907962L);
blockheight.put("2018-02-01", 919119L);
blockheight.put("2018-03-01", 929093L);
blockheight.put("2018-04-01", 940228L);
blockheight.put("2018-05-01", 951203L);
blockheight.put("2018-06-01", 962592L);
}
public long getHeight(String date) {
SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd");
parser.setTimeZone(TimeZone.getTimeZone("UTC"));
parser.setLenient(false);
try {
Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
cal.set(Calendar.DST_OFFSET, 0);
cal.setTime(parser.parse(date));
cal.add(Calendar.DAY_OF_MONTH, -4); // give it some leeway
if (cal.get(Calendar.YEAR) < 2014)
return 1;
if ((cal.get(Calendar.YEAR) == 2014) && (cal.get(Calendar.MONTH) <= 3))
// before May 2014
return 1;
Calendar query = (Calendar) cal.clone();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
cal.set(Calendar.DAY_OF_MONTH, 1);
long prevTime = cal.getTimeInMillis();
String prevDate = formatter.format(prevTime);
// lookup blockheight at first of the month
Long prevBc = blockheight.get(prevDate);
if (prevBc == null) {
// if too recent, go back in time and find latest one we have
while (prevBc == null) {
cal.add(Calendar.MONTH, -1);
if (cal.get(Calendar.YEAR) < 2014) {
throw new IllegalStateException("endless loop looking for blockheight");
}
prevTime = cal.getTimeInMillis();
prevDate = formatter.format(prevTime);
prevBc = blockheight.get(prevDate);
}
}
long height = prevBc;
// now we have a blockheight & a date ON or BEFORE the restore date requested
if (date.equals(prevDate)) return height;
// see if we have a blockheight after this date
cal.add(Calendar.MONTH, 1);
long nextTime = cal.getTimeInMillis();
String nextDate = formatter.format(nextTime);
Long nextBc = blockheight.get(nextDate);
if (nextBc != null) { // we have a range - interpolate the blockheight we are looking for
long diff = nextBc - prevBc;
long diffDays = TimeUnit.DAYS.convert(nextTime - prevTime, TimeUnit.MILLISECONDS);
long days = TimeUnit.DAYS.convert(query.getTimeInMillis() - prevTime,
TimeUnit.MILLISECONDS);
height = Math.round(prevBc + diff * (1.0 * days / diffDays));
} else {
long days = TimeUnit.DAYS.convert(query.getTimeInMillis() - prevTime,
TimeUnit.MILLISECONDS);
height = Math.round(prevBc + 1.0 * days * (24 * 60 / 2));
}
return height;
} catch (ParseException ex) {
throw new IllegalArgumentException(ex);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.m2049r.aeonwallet;
package com.m2049r.xmrwallet;
import android.app.AlertDialog;
import android.content.Context;
@@ -39,13 +39,13 @@ import android.widget.LinearLayout;
import android.widget.Switch;
import android.widget.TextView;
import com.m2049r.aeonwallet.model.Wallet;
import com.m2049r.aeonwallet.model.WalletManager;
import com.m2049r.aeonwallet.util.FingerprintHelper;
import com.m2049r.aeonwallet.util.Helper;
import com.m2049r.aeonwallet.util.KeyStoreHelper;
import com.m2049r.aeonwallet.util.RestoreHeight;
import com.m2049r.aeonwallet.widget.Toolbar;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.FingerprintHelper;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.KeyStoreHelper;
import com.m2049r.xmrwallet.util.RestoreHeight;
import com.m2049r.xmrwallet.widget.Toolbar;
import com.nulabinc.zxcvbn.Strength;
import com.nulabinc.zxcvbn.Zxcvbn;
@@ -61,6 +61,7 @@ public class GenerateFragment extends Fragment {
static final String TYPE_NEW = "new";
static final String TYPE_KEY = "key";
static final String TYPE_SEED = "seed";
static final String TYPE_LEDGER = "ledger";
static final String TYPE_VIEWONLY = "view";
private TextInputLayout etWalletName;
@@ -190,6 +191,17 @@ public class GenerateFragment extends Fragment {
return false;
}
});
} else if (type.equals(TYPE_LEDGER)) {
etWalletPassword.getEditText().setImeOptions(EditorInfo.IME_ACTION_DONE);
etWalletPassword.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() {
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) || (actionId == EditorInfo.IME_ACTION_DONE)) {
etWalletRestoreHeight.requestFocus();
return true;
}
return false;
}
});
} else if (type.equals(TYPE_SEED)) {
etWalletPassword.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() {
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
@@ -391,24 +403,31 @@ public class GenerateFragment extends Fragment {
// is it a date?
SimpleDateFormat parser = new SimpleDateFormat("yyyy-MM-dd");
parser.setLenient(false);
parser.parse(restoreHeight);
height = RestoreHeight.getInstance().getHeight(restoreHeight);
} catch (ParseException exPE) {
height = RestoreHeight.getInstance().getHeight(parser.parse(restoreHeight));
} catch (ParseException ex) {
}
if (height <= 0)
try {
// is it a date without dashes?
SimpleDateFormat parser = new SimpleDateFormat("yyyyMMdd");
parser.setLenient(false);
height = RestoreHeight.getInstance().getHeight(parser.parse(restoreHeight));
} catch (ParseException ex) {
}
if (height <= 0)
try {
// or is it a height?
height = Long.parseLong(restoreHeight);
} catch (NumberFormatException exNFE) {
} catch (NumberFormatException ex) {
return -1;
}
}
Timber.d("Using Restore Height = %d", height);
return height;
}
private boolean checkMnemonic() {
String seed = etWalletMnemonic.getEditText().getText().toString();
int seedWords = seed.split("\\s").length;
boolean ok = ((seedWords == 25) || (seedWords == 24)); // 24 are old style
boolean ok = (seed.split("\\s").length == 25); // 25 words
if (!ok) {
etWalletMnemonic.setError(getString(R.string.generate_check_mnemonic));
} else {
@@ -478,6 +497,12 @@ public class GenerateFragment extends Fragment {
KeyStoreHelper.saveWalletUserPass(getActivity(), name, password);
}
activityCallback.onGenerate(name, crazyPass, seed, height);
} else if (type.equals(TYPE_LEDGER)) {
bGenerate.setEnabled(false);
if (fingerprintAuthAllowed) {
KeyStoreHelper.saveWalletUserPass(getActivity(), name, password);
}
activityCallback.onGenerateLedger(name, crazyPass, height);
} else if (type.equals(TYPE_KEY) || type.equals(TYPE_VIEWONLY)) {
if (checkAddress() && checkViewKey() && checkSpendKey()) {
bGenerate.setEnabled(false);
@@ -516,6 +541,8 @@ public class GenerateFragment extends Fragment {
return getString(R.string.generate_wallet_type_new);
case TYPE_SEED:
return getString(R.string.generate_wallet_type_seed);
case TYPE_LEDGER:
return getString(R.string.generate_wallet_type_ledger);
case TYPE_VIEWONLY:
return getString(R.string.generate_wallet_type_view);
default:
@@ -533,6 +560,8 @@ public class GenerateFragment extends Fragment {
void onGenerate(String name, String password, String address, String viewKey, String spendKey, long height);
void onGenerateLedger(String name, String password, long height);
void setTitle(String title);
void setToolbarButton(int type);
@@ -568,6 +597,9 @@ public class GenerateFragment extends Fragment {
case TYPE_SEED:
inflater.inflate(R.menu.create_wallet_seed, menu);
break;
case TYPE_LEDGER:
inflater.inflate(R.menu.create_wallet_ledger, menu);
break;
case TYPE_VIEWONLY:
inflater.inflate(R.menu.create_wallet_view, menu);
break;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.m2049r.aeonwallet;
package com.m2049r.xmrwallet;
import android.app.AlertDialog;
import android.content.Context;
@@ -43,16 +43,16 @@ import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import com.m2049r.aeonwallet.model.NetworkType;
import com.m2049r.aeonwallet.model.Wallet;
import com.m2049r.aeonwallet.model.WalletManager;
import com.m2049r.aeonwallet.util.FingerprintHelper;
import com.m2049r.aeonwallet.util.Helper;
import com.m2049r.aeonwallet.util.KeyStoreHelper;
import com.m2049r.aeonwallet.util.MoneroThreadPoolExecutor;
import com.m2049r.aeonwallet.widget.Toolbar;
import java.io.File;
import com.m2049r.xmrwallet.ledger.Ledger;
import com.m2049r.xmrwallet.ledger.LedgerProgressDialog;
import com.m2049r.xmrwallet.model.NetworkType;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.FingerprintHelper;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.KeyStoreHelper;
import com.m2049r.xmrwallet.util.MoneroThreadPoolExecutor;
import com.m2049r.xmrwallet.widget.Toolbar;
import timber.log.Timber;
@@ -76,6 +76,9 @@ public class GenerateReviewFragment extends Fragment {
private ImageButton bCopyAddress;
private LinearLayout llAdvancedInfo;
private LinearLayout llPassword;
private LinearLayout llMnemonic;
private LinearLayout llSpendKey;
private LinearLayout llViewKey;
private Button bAdvancedInfo;
private Button bAccept;
@@ -99,6 +102,9 @@ public class GenerateReviewFragment extends Fragment {
bAdvancedInfo = (Button) view.findViewById(R.id.bAdvancedInfo);
llAdvancedInfo = (LinearLayout) view.findViewById(R.id.llAdvancedInfo);
llPassword = (LinearLayout) view.findViewById(R.id.llPassword);
llMnemonic = (LinearLayout) view.findViewById(R.id.llMnemonic);
llSpendKey = (LinearLayout) view.findViewById(R.id.llSpendKey);
llViewKey = (LinearLayout) view.findViewById(R.id.llViewKey);
bAccept = (Button) view.findViewById(R.id.bAccept);
@@ -142,7 +148,6 @@ public class GenerateReviewFragment extends Fragment {
}
void showDetails() {
showProgress();
tvWalletPassword.setText(null);
new AsyncShow().executeOnExecutor(MoneroThreadPoolExecutor.MONERO_THREAD_POOL_EXECUTOR, walletPath);
}
@@ -188,6 +193,20 @@ public class GenerateReviewFragment extends Fragment {
boolean isWatchOnly;
Wallet.Status status;
boolean dialogOpened = false;
@Override
protected void onPreExecute() {
super.onPreExecute();
showProgress();
if ((walletPath != null)
&& (WalletManager.getInstance().queryWalletHardware(walletPath + ".keys", getPassword()) == 1)
&& (progressCallback != null)) {
progressCallback.showLedgerProgressDialog(LedgerProgressDialog.TYPE_RESTORE);
dialogOpened = true;
}
}
@Override
protected Boolean doInBackground(String... params) {
if (params.length != 1) return false;
@@ -212,7 +231,11 @@ public class GenerateReviewFragment extends Fragment {
address = wallet.getAddress();
seed = wallet.getSeed();
viewKey = wallet.getSecretViewKey();
if (wallet.isKeyOnDevice()) {
viewKey = Ledger.Key();
} else {
viewKey = wallet.getSecretViewKey();
}
spendKey = isWatchOnly ? getActivity().getString(R.string.label_watchonly) : wallet.getSecretSpendKey();
isWatchOnly = wallet.isWatchOnly();
if (closeWallet) wallet.close();
@@ -222,6 +245,8 @@ public class GenerateReviewFragment extends Fragment {
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
if (dialogOpened)
progressCallback.dismissProgressDialog();
if (!isAdded()) return; // never mind
walletName = name;
if (result) {
@@ -232,10 +257,22 @@ public class GenerateReviewFragment extends Fragment {
llPassword.setVisibility(View.VISIBLE);
tvWalletPassword.setText(getPassword());
tvWalletAddress.setText(address);
tvWalletMnemonic.setText(seed);
tvWalletViewKey.setText(viewKey);
tvWalletSpendKey.setText(spendKey);
bAdvancedInfo.setVisibility(View.VISIBLE);
if (!seed.isEmpty()) {
llMnemonic.setVisibility(View.VISIBLE);
tvWalletMnemonic.setText(seed);
}
boolean showAdvanced = false;
if (isKeyValid(viewKey)) {
llViewKey.setVisibility(View.VISIBLE);
tvWalletViewKey.setText(viewKey);
showAdvanced = true;
}
if (isKeyValid(spendKey)) {
llSpendKey.setVisibility(View.VISIBLE);
tvWalletSpendKey.setText(spendKey);
showAdvanced = true;
}
if (showAdvanced) bAdvancedInfo.setVisibility(View.VISIBLE);
bCopyAddress.setClickable(true);
bCopyAddress.setImageResource(R.drawable.ic_content_copy_black_24dp);
activityCallback.setTitle(name, getString(R.string.details_title));
@@ -267,6 +304,8 @@ public class GenerateReviewFragment extends Fragment {
public interface ProgressListener {
void showProgressDialog(int msgId);
void showLedgerProgressDialog(int mode);
void dismissProgressDialog();
}
@@ -577,4 +616,10 @@ public class GenerateReviewFragment extends Fragment {
return openDialog;
}
private boolean isKeyValid(String key) {
return (key != null) && (key.length() == 64)
&& !key.equals("0000000000000000000000000000000000000000000000000000000000000000")
&& !key.toLowerCase().equals("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
// ledger implmenetation returns the spend key as f's
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.m2049r.aeonwallet;
package com.m2049r.xmrwallet;
import android.content.Context;
import android.content.SharedPreferences;
@@ -43,15 +43,15 @@ import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.m2049r.aeonwallet.layout.WalletInfoAdapter;
import com.m2049r.aeonwallet.model.NetworkType;
import com.m2049r.aeonwallet.model.WalletManager;
import com.m2049r.aeonwallet.util.Helper;
import com.m2049r.aeonwallet.util.KeyStoreHelper;
import com.m2049r.aeonwallet.util.NodeList;
import com.m2049r.aeonwallet.util.Notice;
import com.m2049r.aeonwallet.widget.DropDownEditText;
import com.m2049r.aeonwallet.widget.Toolbar;
import com.m2049r.xmrwallet.layout.WalletInfoAdapter;
import com.m2049r.xmrwallet.model.NetworkType;
import com.m2049r.xmrwallet.model.WalletManager;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.KeyStoreHelper;
import com.m2049r.xmrwallet.util.NodeList;
import com.m2049r.xmrwallet.util.Notice;
import com.m2049r.xmrwallet.widget.DropDownEditText;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.io.File;
import java.util.ArrayList;
@@ -103,6 +103,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
void setNetworkType(NetworkType networkType);
boolean hasLedger();
}
@Override
@@ -145,11 +146,13 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
fabView = (FloatingActionButton) view.findViewById(R.id.fabView);
fabKey = (FloatingActionButton) view.findViewById(R.id.fabKey);
fabSeed = (FloatingActionButton) view.findViewById(R.id.fabSeed);
fabLedger = (FloatingActionButton) view.findViewById(R.id.fabLedger);
fabNewL = (RelativeLayout) view.findViewById(R.id.fabNewL);
fabViewL = (RelativeLayout) view.findViewById(R.id.fabViewL);
fabKeyL = (RelativeLayout) view.findViewById(R.id.fabKeyL);
fabSeedL = (RelativeLayout) view.findViewById(R.id.fabSeedL);
fabLedgerL = (RelativeLayout) view.findViewById(R.id.fabLedgerL);
fab_pulse = AnimationUtils.loadAnimation(getContext(), R.anim.fab_pulse);
fab_open_screen = AnimationUtils.loadAnimation(getContext(), R.anim.fab_open_screen);
@@ -163,6 +166,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
fabView.setOnClickListener(this);
fabKey.setOnClickListener(this);
fabSeed.setOnClickListener(this);
fabLedger.setOnClickListener(this);
fabScreen.setOnClickListener(this);
RecyclerView recyclerView = (RecyclerView) view.findViewById(R.id.list);
@@ -173,7 +177,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
etDummy = (EditText) view.findViewById(R.id.etDummy);
ViewGroup llNotice = (ViewGroup) view.findViewById(R.id.llNotice);
Notice.showAll(llNotice,".*_login");
Notice.showAll(llNotice, ".*_login");
etDaemonAddress = (DropDownEditText) view.findViewById(R.id.etDaemonAddress);
nodeAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_dropdown_item_1line);
@@ -266,11 +270,11 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
private String addressPrefix() {
switch (WalletManager.getInstance().getNetworkType()) {
case NetworkType_Testnet:
return "-"; // no idea
return "9A-";
case NetworkType_Mainnet:
return "W-"; // guess
return "4-";
case NetworkType_Stagenet:
return "S-"; // guess
return "5-";
default:
throw new IllegalStateException("Unsupported Network: " + WalletManager.getInstance().getNetworkType());
}
@@ -368,9 +372,11 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
private static final String PREF_DAEMON_STAGENET = "daemon_stagenet";
private static final String PREF_DAEMON_MAINNET = "daemon_mainnet";
private static final String PREF_DAEMONLIST_MAINNET = "aeon.monerujo.io";
private static final String PREF_DAEMONLIST_MAINNET =
"node.moneroworld.com:18089;node.xmrbackb.one;node.xmr.be";
private static final String PREF_DAEMONLIST_STAGENET = "aeon.monerujo.io";
private static final String PREF_DAEMONLIST_STAGENET =
"stagenet.monerujo.io;stagenet.xmr-tw.org";
private NodeList daemonStageNet;
private NodeList daemonMainNet;
@@ -424,9 +430,9 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
}
private boolean isFabOpen = false;
private FloatingActionButton fab, fabNew, fabView, fabKey, fabSeed;
private FloatingActionButton fab, fabNew, fabView, fabKey, fabSeed, fabLedger;
private FrameLayout fabScreen;
private RelativeLayout fabNewL, fabViewL, fabKeyL, fabSeedL;
private RelativeLayout fabNewL, fabViewL, fabKeyL, fabSeedL, fabLedgerL;
private Animation fab_open, fab_close, rotate_forward, rotate_backward, fab_open_screen, fab_close_screen;
private Animation fab_pulse;
@@ -435,32 +441,53 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
}
public void animateFAB() {
if (isFabOpen) {
fabScreen.setVisibility(View.INVISIBLE);
if (isFabOpen) { // close the fab
fabScreen.setClickable(false);
fabScreen.startAnimation(fab_close_screen);
fab.startAnimation(rotate_backward);
fabNewL.startAnimation(fab_close);
fabNew.setClickable(false);
fabViewL.startAnimation(fab_close);
fabView.setClickable(false);
fabKeyL.startAnimation(fab_close);
fabKey.setClickable(false);
fabSeedL.startAnimation(fab_close);
fabSeed.setClickable(false);
if (fabLedgerL.getVisibility() == View.VISIBLE) {
fabLedgerL.startAnimation(fab_close);
fabLedger.setClickable(false);
} else {
fabNewL.startAnimation(fab_close);
fabNew.setClickable(false);
fabViewL.startAnimation(fab_close);
fabView.setClickable(false);
fabKeyL.startAnimation(fab_close);
fabKey.setClickable(false);
fabSeedL.startAnimation(fab_close);
fabSeed.setClickable(false);
}
isFabOpen = false;
} else {
} else { // open the fab
fabScreen.setClickable(true);
fabScreen.startAnimation(fab_open_screen);
fab.startAnimation(rotate_forward);
fabNewL.startAnimation(fab_open);
fabNew.setClickable(true);
fabViewL.startAnimation(fab_open);
fabView.setClickable(true);
fabKeyL.startAnimation(fab_open);
fabKey.setClickable(true);
fabSeedL.startAnimation(fab_open);
fabSeed.setClickable(true);
if (activityCallback.hasLedger()) {
fabLedgerL.setVisibility(View.VISIBLE);
fabNewL.setVisibility(View.GONE);
fabViewL.setVisibility(View.GONE);
fabKeyL.setVisibility(View.GONE);
fabSeedL.setVisibility(View.GONE);
fabLedgerL.startAnimation(fab_open);
fabLedger.setClickable(true);
} else {
fabLedgerL.setVisibility(View.GONE);
fabNewL.setVisibility(View.VISIBLE);
fabViewL.setVisibility(View.VISIBLE);
fabKeyL.setVisibility(View.VISIBLE);
fabSeedL.setVisibility(View.VISIBLE);
fabNewL.startAnimation(fab_open);
fabNew.setClickable(true);
fabViewL.startAnimation(fab_open);
fabView.setClickable(true);
fabKeyL.startAnimation(fab_open);
fabKey.setClickable(true);
fabSeedL.startAnimation(fab_open);
fabSeed.setClickable(true);
}
isFabOpen = true;
}
}
@@ -468,6 +495,7 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
@Override
public void onClick(View v) {
int id = v.getId();
Timber.d("onClick %d/%d", id, R.id.fabLedger);
switch (id) {
case R.id.fab:
animateFAB();
@@ -489,6 +517,11 @@ public class LoginFragment extends Fragment implements WalletInfoAdapter.OnInter
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_SEED);
break;
case R.id.fabLedger:
Timber.d("FAB_LEDGER");
animateFAB();
activityCallback.onAddWallet(GenerateFragment.TYPE_LEDGER);
break;
case R.id.fabScreen:
animateFAB();
break;

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.m2049r.aeonwallet;
package com.m2049r.xmrwallet;
public interface OnBackPressedListener {
boolean onBackPressed();

View File

@@ -0,0 +1,23 @@
/*
* 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;
import com.m2049r.xmrwallet.data.BarcodeData;
public interface OnUriScannedListener {
boolean onUriScanned(BarcodeData barcodeData);
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.m2049r.aeonwallet;
package com.m2049r.xmrwallet;
import android.content.Context;
import android.os.Bundle;

View File

@@ -14,15 +14,14 @@
* limitations under the License.
*/
package com.m2049r.aeonwallet;
package com.m2049r.xmrwallet;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import com.m2049r.aeonwallet.util.Helper;
import java.io.File;
import com.m2049r.xmrwallet.util.LocaleHelper;
import static android.view.WindowManager.LayoutParams;
@@ -36,4 +35,9 @@ public abstract class SecureActivity extends AppCompatActivity {
getWindow().setFlags(LayoutParams.FLAG_SECURE, LayoutParams.FLAG_SECURE);
}
}
@Override
protected void attachBaseContext(Context context) {
super.attachBaseContext(LocaleHelper.setLocale(context, LocaleHelper.getLocale(context)));
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.m2049r.aeonwallet;
package com.m2049r.xmrwallet;
import android.content.Context;
import android.content.Intent;
@@ -30,12 +30,14 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import com.m2049r.aeonwallet.model.TransactionInfo;
import com.m2049r.aeonwallet.model.Transfer;
import com.m2049r.aeonwallet.model.Wallet;
import com.m2049r.aeonwallet.util.UserNotes;
import com.m2049r.aeonwallet.widget.Toolbar;
import com.m2049r.xmrwallet.model.TransactionInfo;
import com.m2049r.xmrwallet.model.Transfer;
import com.m2049r.xmrwallet.model.Wallet;
import com.m2049r.xmrwallet.util.Helper;
import com.m2049r.xmrwallet.util.UserNotes;
import com.m2049r.xmrwallet.widget.Toolbar;
import java.text.SimpleDateFormat;
import java.util.Calendar;
@@ -57,6 +59,7 @@ public class TxFragment extends Fragment {
TS_FORMATTER.setTimeZone(tz);
}
private TextView tvAccount;
private TextView tvTxTimestamp;
private TextView tvTxId;
private TextView tvTxKey;
@@ -69,12 +72,24 @@ public class TxFragment extends Fragment {
private TextView etTxNotes;
private Button bTxNotes;
// XMRTO stuff
private View cvXmrTo;
private TextView tvTxXmrToKey;
private TextView tvDestinationBtc;
private TextView tvTxAmountBtc;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_tx_info, container, false);
cvXmrTo = view.findViewById(R.id.cvXmrTo);
tvTxXmrToKey = (TextView) view.findViewById(R.id.tvTxXmrToKey);
tvDestinationBtc = (TextView) view.findViewById(R.id.tvDestinationBtc);
tvTxAmountBtc = (TextView) view.findViewById(R.id.tvTxAmountBtc);
tvAccount = (TextView) view.findViewById(R.id.tvAccount);
tvTxTimestamp = (TextView) view.findViewById(R.id.tvTxTimestamp);
tvTxId = (TextView) view.findViewById(R.id.tvTxId);
tvTxKey = (TextView) view.findViewById(R.id.tvTxKey);
@@ -100,6 +115,14 @@ public class TxFragment extends Fragment {
}
});
tvTxXmrToKey.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Helper.clipBoardCopy(getActivity(), getString(R.string.label_copy_xmrtokey), tvTxXmrToKey.getText().toString());
Toast.makeText(getActivity(), getString(R.string.message_copy_xmrtokey), Toast.LENGTH_SHORT).show();
}
});
Bundle args = getArguments();
TransactionInfo info = args.getParcelable(ARG_INFO);
show(info);
@@ -201,6 +224,8 @@ public class TxFragment extends Fragment {
activityCallback.setSubtitle(getString(R.string.tx_title));
activityCallback.setToolbarButton(Toolbar.BUTTON_BACK);
tvAccount.setText(getString(R.string.tx_account_formatted, info.account, info.subaddress));
tvTxTimestamp.setText(TS_FORMATTER.format(new Date(info.timestamp * 1000)));
tvTxId.setText(info.hash);
tvTxKey.setText(info.txKey.isEmpty() ? "-" : info.txKey);
@@ -215,9 +240,6 @@ public class TxFragment extends Fragment {
String sign = (info.direction == TransactionInfo.Direction.Direction_In ? "+" : "-");
long realAmount = info.amount;
if (info.isPending) {
realAmount = realAmount - info.fee;
}
tvTxAmount.setText(sign + Wallet.getDisplayAmount(realAmount));
if ((info.fee > 0)) {
@@ -265,13 +287,29 @@ public class TxFragment extends Fragment {
}
} else {
sb.append("-");
dstSb.append(info.direction == TransactionInfo.Direction.Direction_In ? activityCallback.getWalletAddress() : "-");
dstSb.append(info.direction ==
TransactionInfo.Direction.Direction_In ?
activityCallback.getWalletSubaddress(info.account, info.subaddress) :
"-");
}
tvTxTransfers.setText(sb.toString());
tvDestination.setText(dstSb.toString());
this.info = info;
showBtcInfo();
}
void showBtcInfo() {
if (userNotes.xmrtoKey != null) {
cvXmrTo.setVisibility(View.VISIBLE);
tvTxXmrToKey.setText(userNotes.xmrtoKey);
tvDestinationBtc.setText(userNotes.xmrtoDestination);
tvTxAmountBtc.setText(userNotes.xmrtoAmount + " BTC");
} else {
cvXmrTo.setVisibility(View.GONE);
}
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -287,7 +325,7 @@ public class TxFragment extends Fragment {
Listener activityCallback;
public interface Listener {
String getWalletAddress();
String getWalletSubaddress(int accountIndex, int subaddressIndex);
String getTxKey(String hash);

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