diff --git a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java index 677e7fc..1f1310c 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/GenerateFragment.java @@ -32,12 +32,16 @@ import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.TextView; +import com.m2049r.xmrwallet.util.RestoreHeight; import com.m2049r.xmrwallet.widget.Toolbar; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.model.WalletManager; import com.m2049r.xmrwallet.util.Helper; import java.io.File; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; import timber.log.Timber; @@ -231,9 +235,7 @@ public class GenerateFragment extends Fragment { } if (!type.equals(TYPE_NEW)) { etWalletRestoreHeight.setVisibility(View.VISIBLE); - etWalletRestoreHeight.getEditText().setOnEditorActionListener(new TextView.OnEditorActionListener() - - { + etWalletRestoreHeight.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)) { Helper.hideKeyboard(getActivity()); @@ -281,6 +283,42 @@ public class GenerateFragment extends Fragment { return ok; } + private boolean checkHeight() { + long height = !type.equals(TYPE_NEW) ? getHeight() : 0; + boolean ok = true; + if (height < 0) { + etWalletRestoreHeight.setError(getString(R.string.generate_restoreheight_error)); + ok = false; + } + if (ok) { + etWalletRestoreHeight.setError(null); + } + return ok; + } + + private long getHeight() { + long height = 0; + + String restoreHeight = etWalletRestoreHeight.getEditText().getText().toString().trim(); + if (restoreHeight.isEmpty()) return -1; + try { + // 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) { + try { + // or is it a height? + height = Long.parseLong(restoreHeight); + } catch (NumberFormatException exNFE) { + return -1; + } + } + Timber.d("Using Restore Height = %d", height); + return height; + } + private boolean checkMnemonic() { String seed = etWalletMnemonic.getEditText().getText().toString(); boolean ok = (seed.split("\\s").length == 25); // 25 words @@ -327,15 +365,13 @@ public class GenerateFragment extends Fragment { private void generateWallet() { if (!checkName()) return; + if (!checkHeight()) return; + String name = etWalletName.getEditText().getText().toString(); String password = etWalletPassword.getEditText().getText().toString(); - long height; - try { - height = Long.parseLong(etWalletRestoreHeight.getEditText().getText().toString()); - } catch (NumberFormatException ex) { - height = 0; // Keep calm and carry on! - } + long height = getHeight(); + if (height < 0) height = 0; if (type.equals(TYPE_NEW)) { bGenerate.setEnabled(false); diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/RestoreHeight.java b/app/src/main/java/com/m2049r/xmrwallet/util/RestoreHeight.java new file mode 100644 index 0000000..29b1c53 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/RestoreHeight.java @@ -0,0 +1,152 @@ +/* + * 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.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 blockheight = new HashMap<>(); + + RestoreHeight() { + blockheight.put("2014-05-01", 18844L); + blockheight.put("2014-06-01", 65406L); + blockheight.put("2014-07-01", 108882L); + blockheight.put("2014-08-01", 153594L); + blockheight.put("2014-09-01", 198072L); + blockheight.put("2014-10-01", 241088L); + blockheight.put("2014-11-01", 285305L); + blockheight.put("2014-12-01", 328069L); + blockheight.put("2015-01-01", 372369L); + blockheight.put("2015-02-01", 416505L); + blockheight.put("2015-03-01", 456631L); + blockheight.put("2015-04-01", 501084L); + blockheight.put("2015-05-01", 543973L); + blockheight.put("2015-06-01", 588326L); + blockheight.put("2015-07-01", 631187L); + blockheight.put("2015-08-01", 675484L); + blockheight.put("2015-09-01", 719725L); + blockheight.put("2015-10-01", 762463L); + blockheight.put("2015-11-01", 806528L); + blockheight.put("2015-12-01", 849041L); + blockheight.put("2016-01-01", 892866L); + blockheight.put("2016-02-01", 936736L); + blockheight.put("2016-03-01", 977691L); + blockheight.put("2016-04-01", 1015848L); + blockheight.put("2016-05-01", 1037417L); + blockheight.put("2016-06-01", 1059651L); + blockheight.put("2016-07-01", 1081269L); + blockheight.put("2016-08-01", 1103630L); + blockheight.put("2016-09-01", 1125983L); + blockheight.put("2016-10-01", 1147617L); + blockheight.put("2016-11-01", 1169779L); + blockheight.put("2016-12-01", 1191402L); + blockheight.put("2017-01-01", 1213861L); + blockheight.put("2017-02-01", 1236197L); + blockheight.put("2017-03-01", 1256358L); + blockheight.put("2017-04-01", 1278622L); + blockheight.put("2017-05-01", 1300239L); + blockheight.put("2017-06-01", 1322564L); + blockheight.put("2017-07-01", 1344225L); + blockheight.put("2017-08-01", 1366664L); + blockheight.put("2017-09-01", 1389113L); + blockheight.put("2017-10-01", 1410738L); + blockheight.put("2017-11-01", 1433039L); + blockheight.put("2017-12-01", 1454639L); + blockheight.put("2018-01-01", 1477201L); + blockheight.put("2018-02-01", 1499599L); + } + + 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); + } + } +} diff --git a/app/src/main/res/layout/fragment_generate.xml b/app/src/main/res/layout/fragment_generate.xml index 27b0a0d..517ba60 100644 --- a/app/src/main/res/layout/fragment_generate.xml +++ b/app/src/main/res/layout/fragment_generate.xml @@ -144,7 +144,7 @@ android:layout_height="wrap_content" android:hint="@string/generate_restoreheight_hint" android:imeOptions="actionDone" - android:inputType="number" + android:inputType="date" android:textAlignment="textStart" /> diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index cd42622..ce52333 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -142,6 +142,8 @@ Monedero creada Creación de monedero fallida + Introduce un número o una fecha (AAAA-MM-DD) + Claves Nuevo Semilla @@ -151,7 +153,7 @@ Clave de Vista Clave de Gasto Semilla Mnemotécnica de 25 Palabras - Altura de Restauración + Altura o Fecha (YYYY-MM-DD) de Restauración Monedero Contraseña diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c9514d5..fd4e819 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -213,6 +213,8 @@ Wallet created Wallet create failed + Enter Number or Date (YYYY-MM-DD) + Keys New Seed @@ -222,7 +224,7 @@ View Key Spend Key 25-Word Mnemonic Seed - Restore Height + Restore Height or Date (YYYY-MM-DD) Wallet Password diff --git a/app/src/test/java/com/m2049r/xmrwallet/util/RestoreHeightTest.java b/app/src/test/java/com/m2049r/xmrwallet/util/RestoreHeightTest.java new file mode 100644 index 0000000..620b06e --- /dev/null +++ b/app/src/test/java/com/m2049r/xmrwallet/util/RestoreHeightTest.java @@ -0,0 +1,134 @@ +/* + * 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.util; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.junit.Assert.assertTrue; + +// all ranges go back 5 days + +public class RestoreHeightTest { + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void pre2014() { + assertTrue(getHeight("2013-12-01") == 1); + assertTrue(getHeight("1958-12-01") == 1); + } + + @Test + public void zero() { + assertTrue(getHeight("2014-04-27") == 1); + } + + @Test + public void notZero() { + assertTrue(getHeight("2014-05-07") > 1); + } + + @Test(expected = IllegalArgumentException.class) + public void notDateA() { + getHeight("2013-13-04"); + } + + @Test(expected = IllegalArgumentException.class) + public void notDateB() { + getHeight("2013-13-01-"); + } + + @Test(expected = IllegalArgumentException.class) + public void notDateC() { + getHeight("x013-13-01"); + } + + @Test(expected = IllegalArgumentException.class) + public void notDateD() { + getHeight("2013-12-41"); + } + + @Test + public void test201709() { + // getHeight() returns blockheight of < two days ago + assertTrue(isInRange(getHeight("2017-09-01"), 1383957, 1387716)); + assertTrue(isInRange(getHeight("2017-09-05"), 1386967, 1390583)); + assertTrue(isInRange(getHeight("2017-09-21"), 1398492, 1402068)); + } + + @Test + public void test20160324() { // blocktime changed from 1 minute to 2 minutes on this day + assertTrue(isInRange(getHeight("2016-03-23"), 998955, 1006105)); + assertTrue(isInRange(getHeight("2016-03-24"), 1000414, 1007486)); + assertTrue(isInRange(getHeight("2016-03-25"), 1001800, 1008900)); + assertTrue(isInRange(getHeight("2016-03-26"), 1003243, 1009985)); + assertTrue(isInRange(getHeight("2016-03-27"), 1004694, 1010746)); + } + + @Test + public void test2014() { + assertTrue(isInRange(getHeight("2014-04-26"), 1, 8501)); + assertTrue(isInRange(getHeight("2014-05-09"), 20289, 28311)); + assertTrue(isInRange(getHeight("2014-05-17"), 32608, 40075)); + assertTrue(isInRange(getHeight("2014-05-30"), 52139, 59548)); + } + + @Test + public void test2015() { + assertTrue(isInRange(getHeight("2015-01-26"), 397914, 405055)); + assertTrue(isInRange(getHeight("2015-08-13"), 682595, 689748)); + } + + @Test + public void test2016() { + assertTrue(isInRange(getHeight("2016-01-26"), 918313, 925424)); + assertTrue(isInRange(getHeight("2016-08-13"), 1107244, 1110793)); + } + + @Test + public void test2017() { + assertTrue(isInRange(getHeight("2017-01-26"), 1226806, 1230402)); + assertTrue(isInRange(getHeight("2017-08-13"), 1370264, 1373854)); + assertTrue(isInRange(getHeight("2017-08-31"), 1383254, 1386967)); + assertTrue(isInRange(getHeight("2017-06-09"), 1323288, 1326884)); + } + + @Test + public void post201802() { + assertTrue(isInRange(getHeight("2018-02-19"), 1507579, 1511127)); + } + + @Test + public void postFuture() { + long b_20180208 = 1504715; + long b_20180808 = b_20180208 + 720 * (28 + 31 + 30 + 31 + 30 + 31); + assertTrue(isInRange(getHeight("2018-08-08"), b_20180808 - 720 * 5, b_20180808)); + } + + + private boolean isInRange(long n, long min, long max) { + if (n > max) return false; + if (n < min) return false; + return true; + } + + private long getHeight(String date) { + return RestoreHeight.getInstance().getHeight(date); + } +}