diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index edfe311d..358b81dd 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -19,6 +19,7 @@ import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.*; import com.sparrowwallet.sparrow.net.ElectrumServer; +import com.sparrowwallet.sparrow.net.VersionCheckService; import com.sparrowwallet.sparrow.preferences.PreferencesDialog; import com.sparrowwallet.sparrow.transaction.TransactionController; import com.sparrowwallet.sparrow.transaction.TransactionData; @@ -70,8 +71,8 @@ public class AppController implements Initializable { private static final int SERVER_PING_PERIOD = 10 * 1000; private static final int ENUMERATE_HW_PERIOD = 30 * 1000; - private static final int RATES_PERIOD = 5 * 60 * 1000; + private static final int VERSION_CHECK_PERIOD_HOURS = 24; private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO; private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD"); @@ -125,6 +126,8 @@ public class AppController implements Initializable { private Hwi.ScheduledEnumerateService deviceEnumerateService; + private VersionCheckService versionCheckService; + private static Integer currentBlockHeight; public static boolean showTxHexProperty; @@ -247,9 +250,18 @@ public class AppController implements Initializable { if(!ratesService.isRunning() && ratesService.getExchangeSource() != ExchangeSource.NONE) { ratesService.start(); } + + if(versionCheckService.getState() == Worker.State.CANCELLED) { + versionCheckService.reset(); + } + + if(!versionCheckService.isRunning() && Config.get().isCheckNewVersions()) { + versionCheckService.start(); + } } else { connectionService.cancel(); ratesService.cancel(); + versionCheckService.cancel(); } } }); @@ -268,10 +280,15 @@ public class AppController implements Initializable { ExchangeSource source = config.getExchangeSource() != null ? config.getExchangeSource() : DEFAULT_EXCHANGE_SOURCE; Currency currency = config.getFiatCurrency() != null ? config.getFiatCurrency() : DEFAULT_FIAT_CURRENCY; ratesService = createRatesService(source, currency); - if (config.getMode() == Mode.ONLINE && source != ExchangeSource.NONE) { + if(config.getMode() == Mode.ONLINE && source != ExchangeSource.NONE) { ratesService.start(); } + versionCheckService = createVersionCheckService(); + if(config.getMode() == Mode.ONLINE && config.isCheckNewVersions()) { + versionCheckService.start(); + } + openTransactionIdItem.disableProperty().bind(onlineProperty.not()); } @@ -323,6 +340,22 @@ public class AppController implements Initializable { return ratesService; } + private VersionCheckService createVersionCheckService() { + VersionCheckService versionCheckService = new VersionCheckService(); + versionCheckService.setDelay(Duration.seconds(10)); + versionCheckService.setPeriod(Duration.hours(VERSION_CHECK_PERIOD_HOURS)); + versionCheckService.setRestartOnFailure(true); + + versionCheckService.setOnSucceeded(successEvent -> { + VersionUpdatedEvent event = versionCheckService.getValue(); + if(event != null) { + EventManager.get().post(event); + } + }); + + return versionCheckService; + } + private Hwi.ScheduledEnumerateService createDeviceEnumerateService() { Hwi.ScheduledEnumerateService enumerateService = new Hwi.ScheduledEnumerateService(null); enumerateService.setPeriod(new Duration(ENUMERATE_HW_PERIOD)); @@ -1171,6 +1204,20 @@ public class AppController implements Initializable { wait.play(); } + @Subscribe + public void versionUpdated(VersionUpdatedEvent event) { + Hyperlink versionUpdateLabel = new Hyperlink("Sparrow " + event.getVersion() + " available"); + versionUpdateLabel.setOnAction(event1 -> { + application.getHostServices().showDocument("https://www.sparrowwallet.com/download"); + }); + + if(statusBar.getRightItems().size() > 0 && statusBar.getRightItems().get(0) instanceof Hyperlink) { + statusBar.getRightItems().remove(0); + } + + statusBar.getRightItems().add(0, versionUpdateLabel); + } + @Subscribe public void timedWorker(TimedEvent event) { if(event.getTimeMills() == 0) { @@ -1214,7 +1261,7 @@ public class AppController implements Initializable { } else { if(usbStatus == null) { usbStatus = new UsbStatusButton(); - statusBar.getRightItems().add(0, usbStatus); + statusBar.getRightItems().add(Math.max(statusBar.getRightItems().size() - 1, 0), usbStatus); } else { usbStatus.getItems().remove(0, usbStatus.getItems().size()); } @@ -1285,6 +1332,16 @@ public class AppController implements Initializable { fiatCurrencyExchangeRate = event.getCurrencyRate(); } + @Subscribe + public void versionCheckStatus(VersionCheckStatusEvent event) { + versionCheckService.cancel(); + + if(Config.get().getMode() != Mode.OFFLINE && event.isEnabled()) { + versionCheckService = createVersionCheckService(); + versionCheckService.start(); + } + } + @Subscribe public void openWallets(OpenWalletsEvent event) { List walletFiles = event.getWalletsMap().values().stream().map(storage -> storage.getWalletFile()).collect(Collectors.toList()); diff --git a/src/main/java/com/sparrowwallet/sparrow/event/VersionCheckStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/VersionCheckStatusEvent.java new file mode 100644 index 00000000..3a1216fe --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/VersionCheckStatusEvent.java @@ -0,0 +1,13 @@ +package com.sparrowwallet.sparrow.event; + +public class VersionCheckStatusEvent { + private final boolean enabled; + + public VersionCheckStatusEvent(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/VersionUpdatedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/VersionUpdatedEvent.java new file mode 100644 index 00000000..55078cf0 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/VersionUpdatedEvent.java @@ -0,0 +1,13 @@ +package com.sparrowwallet.sparrow.event; + +public class VersionUpdatedEvent { + private final String version; + + public VersionUpdatedEvent(String version) { + this.version = version; + } + + public String getVersion() { + return version; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index b316726b..e2c2163d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -23,6 +23,7 @@ public class Config { private boolean groupByAddress = true; private boolean includeMempoolChange = true; private boolean notifyNewTransactions = true; + private boolean checkNewVersions = true; private List recentWalletFiles; private Integer keyDerivationPeriod; private File hwi; @@ -135,6 +136,15 @@ public class Config { flush(); } + public boolean isCheckNewVersions() { + return checkNewVersions; + } + + public void setCheckNewVersions(boolean checkNewVersions) { + this.checkNewVersions = checkNewVersions; + flush(); + } + public List getRecentWalletFiles() { return recentWalletFiles; } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/VersionCheckService.java b/src/main/java/com/sparrowwallet/sparrow/net/VersionCheckService.java new file mode 100644 index 00000000..e3b30ff6 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/VersionCheckService.java @@ -0,0 +1,146 @@ +package com.sparrowwallet.sparrow.net; + +import com.google.gson.Gson; +import com.sparrowwallet.drongo.address.Address; +import com.sparrowwallet.drongo.address.InvalidAddressException; +import com.sparrowwallet.drongo.crypto.ECKey; +import com.sparrowwallet.drongo.protocol.ScriptType; +import com.sparrowwallet.sparrow.MainApp; +import com.sparrowwallet.sparrow.event.VersionUpdatedEvent; +import javafx.concurrent.ScheduledService; +import javafx.concurrent.Task; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.net.ssl.HttpsURLConnection; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.security.SignatureException; +import java.util.Map; + +public class VersionCheckService extends ScheduledService { + private static final Logger log = LoggerFactory.getLogger(VersionCheckService.class); + private static final String VERSION_CHECK_URL = "https://www.sparrowwallet.com/version"; + + @Override + protected Task createTask() { + return new Task<>() { + protected VersionUpdatedEvent call() { + try { + VersionCheck versionCheck = getVersionCheck(); + if(isNewer(versionCheck) && verifySignature(versionCheck)) { + return new VersionUpdatedEvent(versionCheck.version); + } + } catch(IOException e) { + log.error("Error retrieving version check file", e); + } + + return null; + } + }; + } + + private VersionCheck getVersionCheck() throws IOException { + URL url = new URL(VERSION_CHECK_URL); + HttpsURLConnection conn = (HttpsURLConnection)url.openConnection(); + + try(InputStreamReader reader = new InputStreamReader(conn.getInputStream())) { + Gson gson = new Gson(); + return gson.fromJson(reader, VersionCheck.class); + } + } + + private boolean verifySignature(VersionCheck versionCheck) { + try { + for(String addressString : versionCheck.signatures.keySet()) { + String signature = versionCheck.signatures.get(addressString); + ECKey signedMessageKey = ECKey.signedMessageToKey(versionCheck.version, signature, false); + Address providedAddress = Address.fromString(addressString); + Address signedMessageAddress = ScriptType.P2PKH.getAddress(signedMessageKey); + + if(providedAddress.equals(signedMessageAddress)) { + return true; + } else { + log.warn("Invalid signature for version check " + signature + " from address " + addressString); + } + } + } catch(SignatureException e) { + log.error("Error in version check signature", e); + } catch(InvalidAddressException e) { + log.error("Error in version check address", e); + } + + return false; + } + + private boolean isNewer(VersionCheck versionCheck) { + try { + Version versionCheckVersion = new Version(versionCheck.version); + Version currentVersion = new Version(MainApp.APP_VERSION); + return versionCheckVersion.compareTo(currentVersion) > 0; + } catch(IllegalArgumentException e) { + log.error("Invalid versions to compare: " + versionCheck.version + " to " + MainApp.APP_VERSION, e); + } + + return false; + } + + private static class VersionCheck { + public String version; + public Map signatures; + } + + public static class Version implements Comparable { + private final String version; + + public final String get() { + return this.version; + } + + public Version(String version) { + if(version == null) { + throw new IllegalArgumentException("Version can not be null"); + } + if(!version.matches("[0-9]+(\\.[0-9]+)*")) { + throw new IllegalArgumentException("Invalid version format"); + } + this.version = version; + } + + @Override + public int compareTo(Version that) { + if(that == null) { + return 1; + } + String[] thisParts = this.get().split("\\."); + String[] thatParts = that.get().split("\\."); + int length = Math.max(thisParts.length, thatParts.length); + for(int i = 0; i < length; i++) { + int thisPart = i < thisParts.length ? Integer.parseInt(thisParts[i]) : 0; + int thatPart = i < thatParts.length ? Integer.parseInt(thatParts[i]) : 0; + if(thisPart < thatPart) { + return -1; + } + if(thisPart > thatPart) { + return 1; + } + } + return 0; + } + + @Override + public boolean equals(Object that) { + if(this == that) { + return true; + } + if(that == null) { + return false; + } + if(this.getClass() != that.getClass()) { + return false; + } + return this.compareTo((Version)that) == 0; + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java b/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java index 34269875..795fd8e6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java +++ b/src/main/java/com/sparrowwallet/sparrow/preferences/GeneralPreferencesController.java @@ -5,6 +5,7 @@ import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch; import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent; import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent; +import com.sparrowwallet.sparrow.event.VersionCheckStatusEvent; import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.ExchangeSource; import javafx.beans.value.ChangeListener; @@ -38,6 +39,9 @@ public class GeneralPreferencesController extends PreferencesDetailController { @FXML private UnlabeledToggleSwitch notifyNewTransactions; + @FXML + private UnlabeledToggleSwitch checkNewVersions; + private final ChangeListener fiatCurrencyListener = new ChangeListener() { @Override public void changed(ObservableValue observable, Currency oldValue, Currency newValue) { @@ -86,6 +90,12 @@ public class GeneralPreferencesController extends PreferencesDetailController { notifyNewTransactions.selectedProperty().addListener((observableValue, oldValue, newValue) -> { config.setNotifyNewTransactions(newValue); }); + + checkNewVersions.setSelected(config.isCheckNewVersions()); + checkNewVersions.selectedProperty().addListener((observableValue, oldValue, newValue) -> { + config.setCheckNewVersions(newValue); + EventManager.get().post(new VersionCheckStatusEvent(newValue)); + }); } private void updateCurrencies(ExchangeSource exchangeSource) { diff --git a/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml b/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml index 7c4f569b..ff96af44 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/preferences/general.fxml @@ -73,6 +73,10 @@ + + + +