add software update check

This commit is contained in:
Craig Raw 2020-09-18 15:48:05 +02:00
parent b9db4421df
commit f22312e04f
7 changed files with 256 additions and 3 deletions

View file

@ -19,6 +19,7 @@ import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.*; import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.net.ElectrumServer; import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.VersionCheckService;
import com.sparrowwallet.sparrow.preferences.PreferencesDialog; import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
import com.sparrowwallet.sparrow.transaction.TransactionController; import com.sparrowwallet.sparrow.transaction.TransactionController;
import com.sparrowwallet.sparrow.transaction.TransactionData; 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 SERVER_PING_PERIOD = 10 * 1000;
private static final int ENUMERATE_HW_PERIOD = 30 * 1000; private static final int ENUMERATE_HW_PERIOD = 30 * 1000;
private static final int RATES_PERIOD = 5 * 60 * 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 ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO;
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD"); private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
@ -125,6 +126,8 @@ public class AppController implements Initializable {
private Hwi.ScheduledEnumerateService deviceEnumerateService; private Hwi.ScheduledEnumerateService deviceEnumerateService;
private VersionCheckService versionCheckService;
private static Integer currentBlockHeight; private static Integer currentBlockHeight;
public static boolean showTxHexProperty; public static boolean showTxHexProperty;
@ -247,9 +250,18 @@ public class AppController implements Initializable {
if(!ratesService.isRunning() && ratesService.getExchangeSource() != ExchangeSource.NONE) { if(!ratesService.isRunning() && ratesService.getExchangeSource() != ExchangeSource.NONE) {
ratesService.start(); ratesService.start();
} }
if(versionCheckService.getState() == Worker.State.CANCELLED) {
versionCheckService.reset();
}
if(!versionCheckService.isRunning() && Config.get().isCheckNewVersions()) {
versionCheckService.start();
}
} else { } else {
connectionService.cancel(); connectionService.cancel();
ratesService.cancel(); ratesService.cancel();
versionCheckService.cancel();
} }
} }
}); });
@ -272,6 +284,11 @@ public class AppController implements Initializable {
ratesService.start(); ratesService.start();
} }
versionCheckService = createVersionCheckService();
if(config.getMode() == Mode.ONLINE && config.isCheckNewVersions()) {
versionCheckService.start();
}
openTransactionIdItem.disableProperty().bind(onlineProperty.not()); openTransactionIdItem.disableProperty().bind(onlineProperty.not());
} }
@ -323,6 +340,22 @@ public class AppController implements Initializable {
return ratesService; 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() { private Hwi.ScheduledEnumerateService createDeviceEnumerateService() {
Hwi.ScheduledEnumerateService enumerateService = new Hwi.ScheduledEnumerateService(null); Hwi.ScheduledEnumerateService enumerateService = new Hwi.ScheduledEnumerateService(null);
enumerateService.setPeriod(new Duration(ENUMERATE_HW_PERIOD)); enumerateService.setPeriod(new Duration(ENUMERATE_HW_PERIOD));
@ -1171,6 +1204,20 @@ public class AppController implements Initializable {
wait.play(); 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 @Subscribe
public void timedWorker(TimedEvent event) { public void timedWorker(TimedEvent event) {
if(event.getTimeMills() == 0) { if(event.getTimeMills() == 0) {
@ -1214,7 +1261,7 @@ public class AppController implements Initializable {
} else { } else {
if(usbStatus == null) { if(usbStatus == null) {
usbStatus = new UsbStatusButton(); usbStatus = new UsbStatusButton();
statusBar.getRightItems().add(0, usbStatus); statusBar.getRightItems().add(Math.max(statusBar.getRightItems().size() - 1, 0), usbStatus);
} else { } else {
usbStatus.getItems().remove(0, usbStatus.getItems().size()); usbStatus.getItems().remove(0, usbStatus.getItems().size());
} }
@ -1285,6 +1332,16 @@ public class AppController implements Initializable {
fiatCurrencyExchangeRate = event.getCurrencyRate(); fiatCurrencyExchangeRate = event.getCurrencyRate();
} }
@Subscribe
public void versionCheckStatus(VersionCheckStatusEvent event) {
versionCheckService.cancel();
if(Config.get().getMode() != Mode.OFFLINE && event.isEnabled()) {
versionCheckService = createVersionCheckService();
versionCheckService.start();
}
}
@Subscribe @Subscribe
public void openWallets(OpenWalletsEvent event) { public void openWallets(OpenWalletsEvent event) {
List<File> walletFiles = event.getWalletsMap().values().stream().map(storage -> storage.getWalletFile()).collect(Collectors.toList()); List<File> walletFiles = event.getWalletsMap().values().stream().map(storage -> storage.getWalletFile()).collect(Collectors.toList());

View file

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

View file

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

View file

@ -23,6 +23,7 @@ public class Config {
private boolean groupByAddress = true; private boolean groupByAddress = true;
private boolean includeMempoolChange = true; private boolean includeMempoolChange = true;
private boolean notifyNewTransactions = true; private boolean notifyNewTransactions = true;
private boolean checkNewVersions = true;
private List<File> recentWalletFiles; private List<File> recentWalletFiles;
private Integer keyDerivationPeriod; private Integer keyDerivationPeriod;
private File hwi; private File hwi;
@ -135,6 +136,15 @@ public class Config {
flush(); flush();
} }
public boolean isCheckNewVersions() {
return checkNewVersions;
}
public void setCheckNewVersions(boolean checkNewVersions) {
this.checkNewVersions = checkNewVersions;
flush();
}
public List<File> getRecentWalletFiles() { public List<File> getRecentWalletFiles() {
return recentWalletFiles; return recentWalletFiles;
} }

View file

@ -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<VersionUpdatedEvent> {
private static final Logger log = LoggerFactory.getLogger(VersionCheckService.class);
private static final String VERSION_CHECK_URL = "https://www.sparrowwallet.com/version";
@Override
protected Task<VersionUpdatedEvent> 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<String, String> signatures;
}
public static class Version implements Comparable<Version> {
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;
}
}
}

View file

@ -5,6 +5,7 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch; import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent; import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent; import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent;
import com.sparrowwallet.sparrow.event.VersionCheckStatusEvent;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.ExchangeSource; import com.sparrowwallet.sparrow.io.ExchangeSource;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
@ -38,6 +39,9 @@ public class GeneralPreferencesController extends PreferencesDetailController {
@FXML @FXML
private UnlabeledToggleSwitch notifyNewTransactions; private UnlabeledToggleSwitch notifyNewTransactions;
@FXML
private UnlabeledToggleSwitch checkNewVersions;
private final ChangeListener<Currency> fiatCurrencyListener = new ChangeListener<Currency>() { private final ChangeListener<Currency> fiatCurrencyListener = new ChangeListener<Currency>() {
@Override @Override
public void changed(ObservableValue<? extends Currency> observable, Currency oldValue, Currency newValue) { public void changed(ObservableValue<? extends Currency> observable, Currency oldValue, Currency newValue) {
@ -86,6 +90,12 @@ public class GeneralPreferencesController extends PreferencesDetailController {
notifyNewTransactions.selectedProperty().addListener((observableValue, oldValue, newValue) -> { notifyNewTransactions.selectedProperty().addListener((observableValue, oldValue, newValue) -> {
config.setNotifyNewTransactions(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) { private void updateCurrencies(ExchangeSource exchangeSource) {

View file

@ -73,6 +73,10 @@
<UnlabeledToggleSwitch fx:id="notifyNewTransactions" /> <UnlabeledToggleSwitch fx:id="notifyNewTransactions" />
<HelpLabel helpText="Show system notifications on new wallet transactions"/> <HelpLabel helpText="Show system notifications on new wallet transactions"/>
</Field> </Field>
<Field text="Software updates:">
<UnlabeledToggleSwitch fx:id="checkNewVersions" />
<HelpLabel helpText="Check for updates to Sparrow"/>
</Field>
</Fieldset> </Fieldset>
</Form> </Form>
</GridPane> </GridPane>