diff --git a/build.gradle b/build.gradle
index 932fa067..f4fc9714 100644
--- a/build.gradle
+++ b/build.gradle
@@ -20,7 +20,7 @@ if(System.getProperty("os.arch") == "aarch64") {
def headless = "true".equals(System.getProperty("java.awt.headless"))
group 'com.sparrowwallet'
-version '2.2.2'
+version '2.2.4'
repositories {
mavenCentral()
@@ -78,7 +78,7 @@ dependencies {
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
implementation('com.sparrowwallet:hummingbird:1.7.4')
implementation('co.nstant.in:cbor:0.9')
- implementation('org.openpnp:openpnp-capture-java:0.0.28-5')
+ implementation('org.openpnp:openpnp-capture-java:0.0.28-6')
implementation("io.matthewnelson.kmp-tor:runtime:2.2.1")
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.3")
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
diff --git a/docs/reproducible.md b/docs/reproducible.md
index 5df236eb..e6e97ea0 100644
--- a/docs/reproducible.md
+++ b/docs/reproducible.md
@@ -83,7 +83,7 @@ sudo apt install -y rpm fakeroot binutils
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
```shell
-GIT_TAG="2.2.1"
+GIT_TAG="2.2.3"
```
The project can then be initially cloned as follows:
diff --git a/drongo b/drongo
index abb598d3..13e1fafb 160000
--- a/drongo
+++ b/drongo
@@ -1 +1 @@
-Subproject commit abb598d3b041a9d0b3d0ba41b5fb9785e2100193
+Subproject commit 13e1fafbe8892d7005a043ae561e09ed66f7cea6
diff --git a/src/main/deploy/package/osx/Info.plist b/src/main/deploy/package/osx/Info.plist
index dfb179de..54cb165d 100644
--- a/src/main/deploy/package/osx/Info.plist
+++ b/src/main/deploy/package/osx/Info.plist
@@ -21,7 +21,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 2.2.2
+ 2.2.4
CFBundleSignature
????
diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java
index df2286ab..38b18758 100644
--- a/src/main/java/com/sparrowwallet/sparrow/AppController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java
@@ -1462,6 +1462,10 @@ public class AppController implements Initializable {
}
public void sendToMany(ActionEvent event) {
+ sendToMany(Collections.emptyList());
+ }
+
+ private void sendToMany(List initialPayments) {
if(sendToManyDialog != null) {
Stage stage = (Stage)sendToManyDialog.getDialogPane().getScene().getWindow();
stage.setAlwaysOnTop(true);
@@ -1477,7 +1481,7 @@ public class AppController implements Initializable {
bitcoinUnit = wallet.getAutoUnit();
}
- sendToManyDialog = new SendToManyDialog(bitcoinUnit);
+ sendToManyDialog = new SendToManyDialog(bitcoinUnit, initialPayments);
sendToManyDialog.initModality(Modality.NONE);
Optional> optPayments = sendToManyDialog.showAndWait();
sendToManyDialog = null;
@@ -3147,6 +3151,11 @@ public class AppController implements Initializable {
}
}
+ @Subscribe
+ public void requestSendToMany(RequestSendToManyEvent event) {
+ sendToMany(event.getPayments());
+ }
+
@Subscribe
public void functionAction(FunctionActionEvent event) {
selectTab(event.getWallet());
diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java
index e5adc238..53df02ac 100644
--- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java
+++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java
@@ -136,6 +136,8 @@ public class AppServices {
private static Map targetBlockFeeRates;
+ private static Double nextBlockMedianFeeRate;
+
private static final TreeMap> mempoolHistogram = new TreeMap<>();
private static Double minimumRelayFeeRate;
@@ -748,6 +750,10 @@ public class AppServices {
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
}
+ public static Double getNextBlockMedianFeeRate() {
+ return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate;
+ }
+
public static double getFallbackFeeRate() {
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
}
@@ -1249,11 +1255,13 @@ public class AppServices {
if(AppServices.currentBlockHeight != null) {
blockSummaries.keySet().removeIf(height -> AppServices.currentBlockHeight - height > 5);
}
+ nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
}
@Subscribe
public void feesUpdated(FeeRatesUpdatedEvent event) {
targetBlockFeeRates = event.getTargetBlockFeeRates();
+ nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
}
@Subscribe
diff --git a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java
index 776ddc53..946906f8 100644
--- a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java
+++ b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java
@@ -18,7 +18,7 @@ import java.util.*;
public class SparrowWallet {
public static final String APP_ID = "sparrow";
public static final String APP_NAME = "Sparrow";
- public static final String APP_VERSION = "2.2.2";
+ public static final String APP_VERSION = "2.2.4";
public static final String APP_VERSION_SUFFIX = "";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java b/src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java
index d02e449c..b8722270 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java
@@ -52,7 +52,10 @@ public class BlockCube extends Group {
public BlockCube(Integer weight, Double medianFee, Integer height, Integer txCount, Long timestamp, boolean confirmed) {
getStyleClass().addAll("block-" + Network.getCanonical().getName(), "block-cube");
this.confirmedProperty.set(confirmed);
- this.feeRatesSource.set(Config.get().getFeeRatesSource());
+
+ FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
+ feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
+ this.feeRatesSource.set(feeRatesSource);
this.weightProperty.addListener((_, _, _) -> {
if(front != null) {
@@ -198,6 +201,8 @@ public class BlockCube extends Group {
} else {
feeRateIcon.getChildren().clear();
}
+ } else {
+ feeRateIcon.getChildren().clear();
}
}
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/ConfirmationAlert.java b/src/main/java/com/sparrowwallet/sparrow/control/ConfirmationAlert.java
new file mode 100644
index 00000000..3fa613b0
--- /dev/null
+++ b/src/main/java/com/sparrowwallet/sparrow/control/ConfirmationAlert.java
@@ -0,0 +1,39 @@
+package com.sparrowwallet.sparrow.control;
+
+import com.sparrowwallet.sparrow.AppServices;
+import javafx.geometry.Insets;
+import javafx.scene.control.Alert;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.Label;
+import javafx.scene.layout.VBox;
+
+import static com.sparrowwallet.sparrow.AppServices.getActiveWindow;
+import static com.sparrowwallet.sparrow.AppServices.setStageIcon;
+
+public class ConfirmationAlert extends Alert {
+ private final CheckBox dontAskAgain;
+
+ public ConfirmationAlert(String title, String contentText, ButtonType... buttons) {
+ super(AlertType.CONFIRMATION, contentText, buttons);
+
+ initOwner(getActiveWindow());
+ setStageIcon(getDialogPane().getScene().getWindow());
+ getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
+ setTitle(title);
+ setHeaderText(title);
+
+ VBox contentBox = new VBox(20);
+ contentBox.setPadding(new Insets(10, 20, 10, 20));
+ Label contentLabel = new Label(contentText);
+ contentLabel.setWrapText(true);
+ dontAskAgain = new CheckBox("Don't ask again");
+ contentBox.getChildren().addAll(contentLabel, dontAskAgain);
+
+ getDialogPane().setContent(contentBox);
+ }
+
+ public boolean isDontAskAgain() {
+ return dontAskAgain.isSelected();
+ }
+}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java
index 867e702d..fac7e84a 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java
@@ -240,6 +240,9 @@ public class MessageSignDialog extends Dialog {
setFormatFromScriptType(address.getScriptType());
if(wallet != null) {
setWalletNodeFromAddress(wallet, address);
+ if(walletNode != null) {
+ setFormatFromScriptType(getSigningScriptType(walletNode));
+ }
}
} catch(InvalidAddressException e) {
//can't happen
@@ -273,7 +276,7 @@ public class MessageSignDialog extends Dialog {
}
if(wallet != null && walletNode != null) {
- setFormatFromScriptType(wallet.getScriptType());
+ setFormatFromScriptType(getSigningScriptType(walletNode));
} else {
formatGroup.selectToggle(formatElectrum);
}
@@ -287,9 +290,13 @@ public class MessageSignDialog extends Dialog {
}
private boolean canSign(Wallet wallet) {
- return wallet.getKeystores().get(0).hasPrivateKey()
- || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB
- || wallet.getKeystores().get(0).getWalletModel().isCard();
+ return wallet.getKeystores().getFirst().hasPrivateKey()
+ || wallet.getKeystores().getFirst().getSource() == KeystoreSource.HW_USB
+ || wallet.getKeystores().getFirst().getWalletModel().isCard();
+ }
+
+ private boolean canSignBip322(Wallet wallet) {
+ return wallet.getKeystores().getFirst().hasPrivateKey();
}
private Address getAddress()throws InvalidAddressException {
@@ -313,6 +320,11 @@ public class MessageSignDialog extends Dialog {
walletNode = wallet.getWalletAddresses().get(address);
}
+ private ScriptType getSigningScriptType(WalletNode walletNode) {
+ ScriptType scriptType = walletNode.getWallet().getScriptType();
+ return canSign(walletNode.getWallet()) && !canSignBip322(walletNode.getWallet()) ? ScriptType.P2PKH : scriptType;
+ }
+
private void setFormatFromScriptType(ScriptType scriptType) {
formatElectrum.setDisable(scriptType == ScriptType.P2TR);
formatTrezor.setDisable(scriptType == ScriptType.P2TR || scriptType == ScriptType.P2PKH);
@@ -345,7 +357,7 @@ public class MessageSignDialog extends Dialog {
//Note we can expect a single keystore due to the check in the constructor
Wallet signingWallet = walletNode.getWallet();
- if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
+ if(signingWallet.getKeystores().getFirst().hasPrivateKey()) {
if(signingWallet.isEncrypted()) {
EventManager.get().post(new RequestOpenWalletsEvent());
} else {
@@ -358,7 +370,7 @@ public class MessageSignDialog extends Dialog {
private void signUnencryptedKeystore(Wallet decryptedWallet) {
try {
- Keystore keystore = decryptedWallet.getKeystores().get(0);
+ Keystore keystore = decryptedWallet.getKeystores().getFirst();
ECKey privKey = keystore.getKey(walletNode);
String signatureText;
if(isBip322()) {
@@ -378,8 +390,8 @@ public class MessageSignDialog extends Dialog {
}
private void signDeviceKeystore(Wallet deviceWallet) {
- List fingerprints = List.of(deviceWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
- KeyDerivation fullDerivation = deviceWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
+ List fingerprints = List.of(deviceWallet.getKeystores().getFirst().getKeyDerivation().getMasterFingerprint());
+ KeyDerivation fullDerivation = deviceWallet.getKeystores().getFirst().getKeyDerivation().extend(walletNode.getDerivation());
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
deviceSignMessageDialog.initOwner(getDialogPane().getScene().getWindow());
Optional optSignature = deviceSignMessageDialog.showAndWait();
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java
index d6c39809..608468c7 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java
@@ -122,19 +122,21 @@ public class QRScanDialog extends Dialog {
if(percentComplete.get() <= 0.0) {
Platform.runLater(() -> percentComplete.set(opening ? 0.0 : -1.0));
}
+ });
- if(opening) {
+ webcamService.openedProperty().addListener((_, _, opened) -> {
+ if(opened) {
Platform.runLater(() -> {
try {
postOpenUpdate = true;
- List newDevices = new ArrayList<>(webcamService.getDevices());
+ List newDevices = new ArrayList<>(webcamService.getAvailableDevices());
newDevices.removeAll(foundDevices);
foundDevices.addAll(newDevices);
foundDevices.removeIf(device -> !webcamService.getDevices().contains(device));
- if(Config.get().getWebcamDevice() != null && webcamDeviceProperty.get() == null) {
+ if(webcamService.getDevice() != null) {
for(CaptureDevice device : foundDevices) {
- if(device.getName().equals(Config.get().getWebcamDevice())) {
+ if(device.equals(webcamService.getDevice())) {
webcamDeviceProperty.set(device);
}
}
@@ -146,10 +148,7 @@ public class QRScanDialog extends Dialog {
postOpenUpdate = false;
}
});
- }
- });
- webcamService.closedProperty().addListener((_, _, closed) -> {
- if(closed && webcamResolutionProperty.get() != null) {
+ } else if(webcamResolutionProperty.get() != null) {
webcamService.setResolution(webcamResolutionProperty.get());
webcamService.setDevice(webcamDeviceProperty.get());
Platform.runLater(() -> {
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java b/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java
index db47bd84..53f76084 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java
@@ -50,7 +50,9 @@ public class RecentBlocksView extends Pane {
}
}));
- updateFeeRatesSource(Config.get().getFeeRatesSource());
+ FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
+ feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
+ updateFeeRatesSource(feeRatesSource);
Tooltip.install(this, tooltip);
}
@@ -106,7 +108,7 @@ public class RecentBlocksView extends Pane {
}
}
- public void addNewBlock(List latestBlocks, Double currentFeeRate) {
+ private void addNewBlock(List latestBlocks, Double currentFeeRate) {
if(getCubes().isEmpty()) {
return;
}
@@ -140,8 +142,10 @@ public class RecentBlocksView extends Pane {
public void updateFeeRate(Map targetBlockFeeRates) {
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
- Double defaultRate = targetBlockFeeRates.get(defaultTarget);
- updateFeeRate(defaultRate);
+ if(targetBlockFeeRates.get(defaultTarget) != null) {
+ Double defaultRate = targetBlockFeeRates.get(defaultTarget);
+ updateFeeRate(defaultRate);
+ }
}
public void updateFeeRate(Double currentFeeRate) {
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java
index 0899baf8..bad10169 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java
@@ -13,8 +13,6 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.control.*;
-import javafx.scene.image.Image;
-import javafx.scene.image.ImageView;
import javafx.scene.input.Clipboard;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
@@ -34,7 +32,7 @@ public class SendToManyDialog extends Dialog> {
private final SpreadsheetView spreadsheetView;
public static final AddressCellType ADDRESS = new AddressCellType();
- public SendToManyDialog(BitcoinUnit bitcoinUnit) {
+ public SendToManyDialog(BitcoinUnit bitcoinUnit, List payments) {
this.bitcoinUnit = bitcoinUnit;
final DialogPane dialogPane = new SendToManyDialogPane();
@@ -44,7 +42,8 @@ public class SendToManyDialog extends Dialog> {
dialogPane.setHeaderText("Send to many recipients by specifying addresses and amounts.\nOnly the first row's label is necessary.");
dialogPane.setGraphic(new DialogImage(DialogImage.Type.SPARROW));
- List initialPayments = IntStream.range(0, 100).mapToObj(i -> new Payment(null, null, -1, false)).collect(Collectors.toList());
+ List initialPayments = IntStream.range(0, 100)
+ .mapToObj(i -> i < payments.size() ? payments.get(i) : new Payment(null, null, -1, false)).collect(Collectors.toList());
Grid grid = getGrid(initialPayments);
spreadsheetView = new SpreadsheetView(grid) {
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WebcamPixelFormat.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamPixelFormat.java
index fe72cf65..9845fe79 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamPixelFormat.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamPixelFormat.java
@@ -6,8 +6,8 @@ public enum WebcamPixelFormat {
//Only V4L2 formats defined in linux/videodev2.h are required here, declared in order of priority for supported formats
PIX_FMT_RGB24("RGB3", true),
PIX_FMT_YUYV("YUYV", true),
- PIX_FMT_MJPG("MJPG", true),
- PIX_FMT_NV12("NV12", false);
+ PIX_FMT_NV12("NV12", true),
+ PIX_FMT_MJPG("MJPG", true);
private final String name;
private final boolean supported;
@@ -25,6 +25,14 @@ public enum WebcamPixelFormat {
return supported;
}
+ public int getFourCC() {
+ char a = name.charAt(0);
+ char b = name.charAt(1);
+ char c = name.charAt(2);
+ char d = name.charAt(3);
+ return ((int) a) | ((int) b << 8) | ((int) c << 16) | ((int) d << 24);
+ }
+
public String toString() {
return name;
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java
index e7f97973..ab3f4b68 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java
@@ -27,6 +27,9 @@ import java.awt.image.BufferedImage;
import java.awt.image.WritableRaster;
import java.util.*;
import java.util.List;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -34,13 +37,18 @@ import java.util.stream.Stream;
public class WebcamService extends ScheduledService {
private static final Logger log = LoggerFactory.getLogger(WebcamService.class);
+ private final Semaphore taskSemaphore = new Semaphore(1);
+ private final AtomicBoolean cancelRequested = new AtomicBoolean(false);
+ private final AtomicBoolean captureClosed = new AtomicBoolean(false);
+
private List devices;
+ private List availableDevices;
private Set resolutions;
private WebcamResolution resolution;
private CaptureDevice device;
private final BooleanProperty opening = new SimpleBooleanProperty(false);
- private final BooleanProperty closed = new SimpleBooleanProperty(false);
+ private final BooleanProperty opened = new SimpleBooleanProperty(false);
private final ObjectProperty resultProperty = new SimpleObjectProperty<>(null);
@@ -105,24 +113,36 @@ public class WebcamService extends ScheduledService {
return new Task<>() {
@Override
protected Image call() throws Exception {
+ if(cancelRequested.get() || isCancelled() || captureClosed.get()) {
+ return null;
+ }
+
+ if(!taskSemaphore.tryAcquire()) {
+ log.warn("Skipped execution of webcam capture task, another task is running");
+ return null;
+ }
+
try {
- if(stream == null) {
+ if(devices == null) {
devices = capture.getDevices();
+ availableDevices = new ArrayList<>(devices);
if(devices.isEmpty()) {
throw new UnsupportedOperationException("No cameras available");
}
+ }
- CaptureDevice selectedDevice = devices.stream().filter(d -> !d.getFormats().isEmpty()).findFirst().orElse(devices.getFirst());
+ while(stream == null && !availableDevices.isEmpty()) {
+ CaptureDevice selectedDevice = availableDevices.stream().filter(d -> !d.getFormats().isEmpty()).findFirst().orElse(availableDevices.getFirst());
if(device != null) {
- for(CaptureDevice webcam : devices) {
+ for(CaptureDevice webcam : availableDevices) {
if(webcam.getName().equals(device.getName())) {
selectedDevice = webcam;
}
}
} else if(Config.get().getWebcamDevice() != null) {
- for(CaptureDevice webcam : devices) {
+ for(CaptureDevice webcam : availableDevices) {
if(webcam.getName().equals(Config.get().getWebcamDevice())) {
selectedDevice = webcam;
}
@@ -163,22 +183,35 @@ public class WebcamService extends ScheduledService {
}
}
+ //On Linux, formats not defined in WebcamPixelFormat are unsupported
+ if(OsType.getCurrent() == OsType.UNIX && WebcamPixelFormat.fromFourCC(format.getFormatInfo().fourcc) == null) {
+ log.warn("Unsupported camera pixel format " + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc));
+ }
+
if(log.isDebugEnabled()) {
- log.debug("Opening capture stream on " + device + " with format " + format.getFormatInfo().width + "x" + format.getFormatInfo().height + " (" + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc) + ")");
+ log.debug("Opening capture stream on " + device + " with format " + format.getFormatInfo().width + "x" + format.getFormatInfo().height + " (" + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc) + ")");
}
opening.set(true);
stream = device.openStream(format);
opening.set(false);
- closed.set(false);
try {
zoomLimits = stream.getPropertyLimits(CaptureProperty.Zoom);
} catch(Throwable e) {
log.debug("Error getting zoom limits on " + device + ", assuming no zoom function");
}
+
+ if(stream == null) {
+ availableDevices.remove(device);
+ }
}
+ if(stream == null) {
+ throw new UnsupportedOperationException("No usable cameras available, tried " + devices);
+ }
+
+ opened.set(true);
BufferedImage originalImage = stream.capture();
CroppedDimension cropped = getCroppedDimension(originalImage);
BufferedImage croppedImage = originalImage.getSubimage(cropped.x, cropped.y, cropped.length, cropped.length);
@@ -195,6 +228,7 @@ public class WebcamService extends ScheduledService {
return image;
} finally {
opening.set(false);
+ taskSemaphore.release();
}
}
};
@@ -204,21 +238,38 @@ public class WebcamService extends ScheduledService {
public void reset() {
stream = null;
zoomLimits = null;
+ cancelRequested.set(false);
super.reset();
}
@Override
public boolean cancel() {
- if(stream != null) {
- stream.close();
- closed.set(true);
+ cancelRequested.set(true);
+ boolean cancelled = super.cancel();
+
+ try {
+ if(taskSemaphore.tryAcquire(1, TimeUnit.SECONDS)) {
+ taskSemaphore.release();
+ } else {
+ log.error("Timed out waiting for task semaphore to be available to cancel, cancelling anyway");
+ }
+ } catch(InterruptedException e) {
+ log.error("Interrupted while waiting for task semaphore to be available to cancel, cancelling anyway");
}
- return super.cancel();
+ if(stream != null) {
+ stream.close();
+ opened.set(false);
+ }
+
+ return cancelled;
}
- public void close() {
- capture.close();
+ public synchronized void close() {
+ if(!captureClosed.get()) {
+ captureClosed.set(true);
+ capture.close();
+ }
}
public PropertyLimits getZoomLimits() {
@@ -336,6 +387,10 @@ public class WebcamService extends ScheduledService {
return devices;
}
+ public List getAvailableDevices() {
+ return availableDevices;
+ }
+
public Set getResolutions() {
return resolutions;
}
@@ -376,8 +431,12 @@ public class WebcamService extends ScheduledService {
return opening;
}
- public BooleanProperty closedProperty() {
- return closed;
+ public BooleanProperty openedProperty() {
+ return opened;
+ }
+
+ public boolean getCancelRequested() {
+ return cancelRequested.get();
}
public static > T getNearestEnum(T target) {
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WebcamView.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamView.java
index a34f50fb..5e9b87f5 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamView.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamView.java
@@ -61,7 +61,7 @@ public class WebcamView {
});
service.valueProperty().addListener((observable, oldValue, newValue) -> {
- if(newValue != null) {
+ if(newValue != null && !service.getCancelRequested()) {
imageProperty.set(newValue);
}
});
diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BlockSummaryEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/BlockSummaryEvent.java
index 4cd74e14..973560b1 100644
--- a/src/main/java/com/sparrowwallet/sparrow/event/BlockSummaryEvent.java
+++ b/src/main/java/com/sparrowwallet/sparrow/event/BlockSummaryEvent.java
@@ -6,12 +6,18 @@ import java.util.Map;
public class BlockSummaryEvent {
private final Map blockSummaryMap;
+ private final Double nextBlockMedianFeeRate;
- public BlockSummaryEvent(Map blockSummaryMap) {
+ public BlockSummaryEvent(Map blockSummaryMap, Double nextBlockMedianFeeRate) {
this.blockSummaryMap = blockSummaryMap;
+ this.nextBlockMedianFeeRate = nextBlockMedianFeeRate;
}
public Map getBlockSummaryMap() {
return blockSummaryMap;
}
+
+ public Double getNextBlockMedianFeeRate() {
+ return nextBlockMedianFeeRate;
+ }
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java
index 82a99958..660d882c 100644
--- a/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java
+++ b/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java
@@ -7,13 +7,23 @@ import java.util.Set;
public class FeeRatesUpdatedEvent extends MempoolRateSizesUpdatedEvent {
private final Map targetBlockFeeRates;
+ private final Double nextBlockMedianFeeRate;
public FeeRatesUpdatedEvent(Map targetBlockFeeRates, Set mempoolRateSizes) {
+ this(targetBlockFeeRates, mempoolRateSizes, null);
+ }
+
+ public FeeRatesUpdatedEvent(Map targetBlockFeeRates, Set mempoolRateSizes, Double nextBlockMedianFeeRate) {
super(mempoolRateSizes);
this.targetBlockFeeRates = targetBlockFeeRates;
+ this.nextBlockMedianFeeRate = nextBlockMedianFeeRate;
}
public Map getTargetBlockFeeRates() {
return targetBlockFeeRates;
}
+
+ public Double getNextBlockMedianFeeRate() {
+ return nextBlockMedianFeeRate;
+ }
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/event/RequestSendToManyEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/RequestSendToManyEvent.java
new file mode 100644
index 00000000..2e88d634
--- /dev/null
+++ b/src/main/java/com/sparrowwallet/sparrow/event/RequestSendToManyEvent.java
@@ -0,0 +1,17 @@
+package com.sparrowwallet.sparrow.event;
+
+import com.sparrowwallet.drongo.wallet.Payment;
+
+import java.util.List;
+
+public class RequestSendToManyEvent {
+ private final List payments;
+
+ public RequestSendToManyEvent(List payments) {
+ this.payments = payments;
+ }
+
+ public List getPayments() {
+ return payments;
+ }
+}
diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletNodeHistoryChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletNodeHistoryChangedEvent.java
index 483a9d14..5697c57f 100644
--- a/src/main/java/com/sparrowwallet/sparrow/event/WalletNodeHistoryChangedEvent.java
+++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletNodeHistoryChangedEvent.java
@@ -14,9 +14,16 @@ import java.util.List;
*/
public class WalletNodeHistoryChangedEvent {
private final String scriptHash;
+ private final String status;
public WalletNodeHistoryChangedEvent(String scriptHash) {
this.scriptHash = scriptHash;
+ this.status = null;
+ }
+
+ public WalletNodeHistoryChangedEvent(String scriptHash, String status) {
+ this.scriptHash = scriptHash;
+ this.status = status;
}
public WalletNode getWalletNode(Wallet wallet) {
@@ -70,4 +77,8 @@ public class WalletNodeHistoryChangedEvent {
public String getScriptHash() {
return scriptHash;
}
+
+ public String getStatus() {
+ return status;
+ }
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java
index 5c226f6c..4def51a2 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java
@@ -52,6 +52,8 @@ public class Config {
private boolean showDeprecatedImportExport = false;
private boolean signBsmsExports = false;
private boolean preventSleep = false;
+ private Boolean connectToBroadcast;
+ private Boolean suggestSendToMany;
private List recentWalletFiles;
private Integer keyDerivationPeriod;
private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS;
@@ -69,6 +71,7 @@ public class Config {
private File coreDataDir;
private String coreAuth;
private boolean useLegacyCoreWallet;
+ private boolean legacyServer;
private Server electrumServer;
private List recentElectrumServers;
private File electrumServerCert;
@@ -355,6 +358,25 @@ public class Config {
public void setPreventSleep(boolean preventSleep) {
this.preventSleep = preventSleep;
+ flush();
+ }
+
+ public Boolean getConnectToBroadcast() {
+ return connectToBroadcast;
+ }
+
+ public void setConnectToBroadcast(Boolean connectToBroadcast) {
+ this.connectToBroadcast = connectToBroadcast;
+ flush();
+ }
+
+ public Boolean getSuggestSendToMany() {
+ return suggestSendToMany;
+ }
+
+ public void setSuggestSendToMany(Boolean suggestSendToMany) {
+ this.suggestSendToMany = suggestSendToMany;
+ flush();
}
public List getRecentWalletFiles() {
@@ -557,6 +579,15 @@ public class Config {
flush();
}
+ public boolean isLegacyServer() {
+ return legacyServer;
+ }
+
+ public void setLegacyServer(boolean legacyServer) {
+ this.legacyServer = legacyServer;
+ flush();
+ }
+
public Server getElectrumServer() {
return electrumServer;
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java b/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java
index e1cfde7b..e836ab22 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java
@@ -126,7 +126,7 @@ public class Descriptor implements WalletImport, WalletExport {
} else if(line.startsWith("#")) {
continue;
} else {
- paragraph.append(line);
+ paragraph.append(line.replaceFirst("^.+:", "").trim());
}
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ElectrumPersonalServer.java b/src/main/java/com/sparrowwallet/sparrow/io/ElectrumPersonalServer.java
index 6f1e4d42..ddcb4cf2 100644
--- a/src/main/java/com/sparrowwallet/sparrow/io/ElectrumPersonalServer.java
+++ b/src/main/java/com/sparrowwallet/sparrow/io/ElectrumPersonalServer.java
@@ -37,7 +37,8 @@ public class ElectrumPersonalServer implements WalletExport {
try {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
writer.write("# Electrum Personal Server configuration file fragments\n");
- writer.write("# Copy the lines below into the relevant sections in your EPS config.ini file\n\n");
+ writer.write("# First close Sparrow and edit your config file in Sparrow home to set \"legacyServer\": true\n");
+ writer.write("# Then copy the lines below into the relevant sections in your EPS config.ini file\n\n");
writer.write("# Copy into [master-public-keys] section\n");
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
writeWalletXpub(masterWallet, writer);
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java
index 83441f97..c262e3ba 100644
--- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java
+++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java
@@ -76,6 +76,10 @@ public class ElectrumServer {
private static final Set sameHeightTxioScriptHashes = ConcurrentHashMap.newKeySet();
+ private final static Map subscribedRecent = new ConcurrentHashMap<>();
+
+ private final static Map broadcastRecent = new ConcurrentHashMap<>();
+
private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc();
private static Cormorant cormorant;
@@ -936,6 +940,20 @@ public class ElectrumServer {
return targetBlocksFeeRatesSats;
}
+ public Double getNextBlockMedianFeeRate() {
+ FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
+ feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
+ if(feeRatesSource.supportsNetwork(Network.get())) {
+ try {
+ return feeRatesSource.getNextBlockMedianFeeRate();
+ } catch(Exception e) {
+ return null;
+ }
+ }
+
+ return null;
+ }
+
public Map getDefaultFeeEstimates(List targetBlocks) throws ServerException {
try {
Map targetBlocksFeeRatesBtcKb = electrumServerRpc.getFeeEstimates(getTransport(), targetBlocks);
@@ -1048,6 +1066,11 @@ public class ElectrumServer {
List recentTransactions = feeRatesSource.getRecentMempoolTransactions();
Map setReferences = new HashMap<>();
setReferences.put(recentTransactions.getFirst(), null);
+ if(recentTransactions.size() > 1) {
+ Random random = new Random();
+ int halfSize = recentTransactions.size() / 2;
+ setReferences.put(recentTransactions.get(halfSize == 1 ? 1 : random.nextInt(halfSize) + 1), null);
+ }
Map transactions = getTransactions(null, setReferences, Collections.emptyMap());
return transactions.values().stream().filter(blxTx -> blxTx.getTransaction() != null).toList();
} catch(Exception e) {
@@ -1232,11 +1255,11 @@ public class ElectrumServer {
if(!serverVersion.isEmpty()) {
String server = serverVersion.getFirst().toLowerCase(Locale.ROOT);
if(server.contains("electrumx")) {
- return new ServerCapability(true);
+ return new ServerCapability(true, true);
}
if(server.startsWith("cormorant")) {
- return new ServerCapability(true, false, true);
+ return new ServerCapability(true, false, true, false);
}
if(server.startsWith("electrs/")) {
@@ -1248,7 +1271,7 @@ public class ElectrumServer {
try {
Version version = new Version(electrsVersion);
if(version.compareTo(ELECTRS_MIN_BATCHING_VERSION) >= 0) {
- return new ServerCapability(true);
+ return new ServerCapability(true, true);
}
} catch(Exception e) {
//ignore
@@ -1264,7 +1287,7 @@ public class ElectrumServer {
try {
Version version = new Version(fulcrumVersion);
if(version.compareTo(FULCRUM_MIN_BATCHING_VERSION) >= 0) {
- return new ServerCapability(true);
+ return new ServerCapability(true, true);
}
} catch(Exception e) {
//ignore
@@ -1283,15 +1306,19 @@ public class ElectrumServer {
Version version = new Version(mempoolElectrsVersion);
if(version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) > 0 ||
(version.compareTo(MEMPOOL_ELECTRS_MIN_BATCHING_VERSION) == 0 && (!mempoolElectrsSuffix.contains("dev") || mempoolElectrsSuffix.contains("dev-249848d")))) {
- return new ServerCapability(true, 25);
+ return new ServerCapability(true, 25, false);
}
} catch(Exception e) {
//ignore
}
}
+
+ if(server.startsWith("electrumpersonalserver")) {
+ return new ServerCapability(false, false);
+ }
}
- return new ServerCapability(false);
+ return new ServerCapability(false, true);
}
public static class ServerVersionService extends Service> {
@@ -1456,8 +1483,9 @@ public class ElectrumServer {
if(elapsed > FEE_RATES_PERIOD) {
Map blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
Set mempoolRateSizes = electrumServer.getMempoolRateSizes();
+ Double nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate();
feeRatesRetrievedAt = System.currentTimeMillis();
- return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
+ return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes, nextBlockMedianFeeRate);
}
} else {
closeConnection();
@@ -1583,6 +1611,31 @@ public class ElectrumServer {
Set mempoolRateSizes = electrumServer.getMempoolRateSizes();
EventManager.get().post(new MempoolRateSizesUpdatedEvent(mempoolRateSizes));
}
+
+ @Subscribe
+ public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
+ String status = broadcastRecent.remove(event.getScriptHash());
+ if(status != null && status.equals(event.getStatus())) {
+ Map subscribeScriptHashes = new HashMap<>();
+ Random random = new Random();
+ int subscriptions = random.nextInt(2) + 1;
+ for(int i = 0; i < subscriptions; i++) {
+ byte[] randomScriptHashBytes = new byte[32];
+ random.nextBytes(randomScriptHashBytes);
+ String randomScriptHash = Utils.bytesToHex(randomScriptHashBytes);
+ if(!subscribedScriptHashes.containsKey(randomScriptHash)) {
+ subscribeScriptHashes.put("m/" + subscribeScriptHashes.size(), randomScriptHash);
+ }
+ }
+
+ try {
+ electrumServerRpc.subscribeScriptHashes(transport, null, subscribeScriptHashes);
+ subscribeScriptHashes.values().forEach(scriptHash -> subscribedRecent.put(scriptHash, AppServices.getCurrentBlockHeight()));
+ } catch(ElectrumServerRpcException e) {
+ log.debug("Error subscribing to recent mempool transaction outputs", e);
+ }
+ }
+ }
}
public static class ReadRunnable implements Runnable {
@@ -1935,7 +1988,8 @@ public class ElectrumServer {
protected FeeRatesUpdatedEvent call() throws ServerException {
ElectrumServer electrumServer = new ElectrumServer();
Map blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
- return new FeeRatesUpdatedEvent(blockTargetFeeRates, null);
+ Double nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate();
+ return new FeeRatesUpdatedEvent(blockTargetFeeRates, null, nextBlockMedianFeeRate);
}
};
}
@@ -1982,10 +2036,14 @@ public class ElectrumServer {
Config config = Config.get();
if(!isBlockstorm(totalBlocks) && !AppServices.isUsingProxy() && config.getServer().getProtocol().equals(Protocol.SSL)
&& (config.getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER || config.getServerType() == ServerType.ELECTRUM_SERVER)) {
- subscribeRecent(electrumServer);
+ subscribeRecent(electrumServer, AppServices.getCurrentBlockHeight() == null ? endHeight : AppServices.getCurrentBlockHeight());
}
- return new BlockSummaryEvent(blockSummaryMap);
+ Double nextBlockMedianFeeRate = null;
+ if(!isBlockstorm(totalBlocks)) {
+ nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate();
+ }
+ return new BlockSummaryEvent(blockSummaryMap, nextBlockMedianFeeRate);
}
};
}
@@ -1994,43 +2052,72 @@ public class ElectrumServer {
return Network.get() != Network.MAINNET && totalBlocks > 2;
}
- private final static Set subscribedRecent = Collections.newSetFromMap(new ConcurrentHashMap<>());
-
- private void subscribeRecent(ElectrumServer electrumServer) {
- Set unsubscribeScriptHashes = new HashSet<>(subscribedRecent);
+ private void subscribeRecent(ElectrumServer electrumServer, int currentHeight) {
+ Set unsubscribeScriptHashes = subscribedRecent.entrySet().stream().filter(entry -> entry.getValue() == null || entry.getValue() <= currentHeight - 3)
+ .map(Map.Entry::getKey).collect(Collectors.toSet());
unsubscribeScriptHashes.removeIf(subscribedScriptHashes::containsKey);
- electrumServerRpc.unsubscribeScriptHashes(transport, unsubscribeScriptHashes);
- subscribedRecent.removeAll(unsubscribeScriptHashes);
+ if(!unsubscribeScriptHashes.isEmpty() && serverCapability.supportsUnsubscribe()) {
+ electrumServerRpc.unsubscribeScriptHashes(transport, unsubscribeScriptHashes);
+ }
+ subscribedRecent.keySet().removeAll(unsubscribeScriptHashes);
+ broadcastRecent.keySet().removeAll(unsubscribeScriptHashes);
Map subscribeScriptHashes = new HashMap<>();
List recentTransactions = electrumServer.getRecentMempoolTransactions();
for(BlockTransaction blkTx : recentTransactions) {
- for(int i = 0; i < blkTx.getTransaction().getOutputs().size() && subscribeScriptHashes.size() < 10; i++) {
+ for(int i = 0; i < blkTx.getTransaction().getOutputs().size(); i++) {
TransactionOutput txOutput = blkTx.getTransaction().getOutputs().get(i);
String scriptHash = getScriptHash(txOutput);
if(!subscribedScriptHashes.containsKey(scriptHash)) {
- subscribeScriptHashes.put("m/" + i, getScriptHash(txOutput));
+ subscribeScriptHashes.put("m/" + subscribeScriptHashes.size(), scriptHash);
+ }
+ if(Math.random() < 0.1d) {
+ break;
}
}
}
if(!subscribeScriptHashes.isEmpty()) {
+ Random random = new Random();
+ int additionalRandomScriptHashes = random.nextInt(8);
+ for(int i = 0; i < additionalRandomScriptHashes; i++) {
+ byte[] randomScriptHashBytes = new byte[32];
+ random.nextBytes(randomScriptHashBytes);
+ String randomScriptHash = Utils.bytesToHex(randomScriptHashBytes);
+ if(!subscribedScriptHashes.containsKey(randomScriptHash)) {
+ subscribeScriptHashes.put("m/" + subscribeScriptHashes.size(), randomScriptHash);
+ }
+ }
+
try {
electrumServerRpc.subscribeScriptHashes(transport, null, subscribeScriptHashes);
- subscribedRecent.addAll(subscribeScriptHashes.values());
+ subscribeScriptHashes.values().forEach(scriptHash -> subscribedRecent.put(scriptHash, currentHeight));
} catch(ElectrumServerRpcException e) {
log.debug("Error subscribing to recent mempool transactions", e);
}
}
+ if(!recentTransactions.isEmpty()) {
+ broadcastRecent(electrumServer, recentTransactions);
+ }
+ }
+
+ private void broadcastRecent(ElectrumServer electrumServer, List recentTransactions) {
ScheduledService broadcastService = new ScheduledService<>() {
@Override
protected Task createTask() {
return new Task<>() {
@Override
protected Void call() throws Exception {
- for(BlockTransaction blkTx : recentTransactions) {
- electrumServer.broadcastTransaction(blkTx.getTransaction());
+ if(!recentTransactions.isEmpty()) {
+ Random random = new Random();
+ if(random.nextBoolean()) {
+ BlockTransaction blkTx = recentTransactions.get(random.nextInt(recentTransactions.size()));
+ String scriptHash = getScriptHash(blkTx.getTransaction().getOutputs().getFirst());
+ String status = getScriptHashStatus(List.of(new ScriptHashTx(0, blkTx.getHashAsString(), blkTx.getFee())));
+ broadcastRecent.put(scriptHash, status);
+ electrumServer.broadcastTransaction(blkTx.getTransaction());
+ }
}
return null;
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java
index 5b99f782..4eebee6e 100644
--- a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java
+++ b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java
@@ -34,6 +34,12 @@ public enum FeeRatesSource {
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
}
+ @Override
+ public Double getNextBlockMedianFeeRate() throws Exception {
+ String url = getApiUrl() + "v1/fees/mempool-blocks";
+ return requestNextBlockMedianFeeRate(this, url);
+ }
+
@Override
public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception {
String url = getApiUrl() + "v1/block/" + Utils.bytesToHex(blockId.getReversedBytes());
@@ -130,6 +136,10 @@ public enum FeeRatesSource {
public abstract Map getBlockTargetFeeRates(Map defaultblockTargetFeeRates);
+ public Double getNextBlockMedianFeeRate() throws Exception {
+ throw new UnsupportedOperationException(name + " does not support retrieving the next block median fee rate");
+ }
+
public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception {
throw new UnsupportedOperationException(name + " does not support block summaries");
}
@@ -199,6 +209,30 @@ public enum FeeRatesSource {
return httpClientService.requestJson(url, ThreeTierRates.class, null);
}
+ protected static Double requestNextBlockMedianFeeRate(FeeRatesSource feeRatesSource, String url) throws Exception {
+ if(log.isInfoEnabled()) {
+ log.info("Requesting next block median fee rate from " + url);
+ }
+
+ HttpClientService httpClientService = AppServices.getHttpClientService();
+ try {
+ MempoolBlock[] mempoolBlocks = feeRatesSource.requestMempoolBlocks(url, httpClientService);
+ return mempoolBlocks.length > 0 ? mempoolBlocks[0].medianFee : null;
+ } catch (Exception e) {
+ if(log.isDebugEnabled()) {
+ log.warn("Error retrieving next block median fee rate from " + url, e);
+ } else {
+ log.warn("Error retrieving next block median fee rate from " + url + " (" + e.getMessage() + ")");
+ }
+
+ throw e;
+ }
+ }
+
+ protected MempoolBlock[] requestMempoolBlocks(String url, HttpClientService httpClientService) throws Exception {
+ return httpClientService.requestJson(url, MempoolBlock[].class, null);
+ }
+
protected static BlockSummary requestBlockSummary(FeeRatesSource feeRatesSource, String url) throws Exception {
if(log.isInfoEnabled()) {
log.info("Requesting block summary from " + url);
@@ -309,6 +343,8 @@ public enum FeeRatesSource {
}
}
+ protected record MempoolBlock(Integer nTx, Double medianFee) {}
+
protected record MempoolBlockSummary(String id, Integer height, Long timestamp, Integer tx_count, Integer weight, MempoolBlockSummaryExtras extras) {
public Double getMedianFee() {
return extras == null ? null : extras.medianFee();
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java b/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java
index 98c7099b..fb15e240 100644
--- a/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java
+++ b/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java
@@ -7,27 +7,30 @@ public class ServerCapability {
private final int maxTargetBlocks;
private final boolean supportsRecentMempool;
private final boolean supportsBlockStats;
+ private final boolean supportsUnsubscribe;
- public ServerCapability(boolean supportsBatching) {
- this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast());
+ public ServerCapability(boolean supportsBatching, boolean supportsUnsubscribe) {
+ this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsUnsubscribe);
}
- public ServerCapability(boolean supportsBatching, int maxTargetBlocks) {
+ public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsUnsubscribe) {
this.supportsBatching = supportsBatching;
this.maxTargetBlocks = maxTargetBlocks;
this.supportsRecentMempool = false;
this.supportsBlockStats = false;
+ this.supportsUnsubscribe = supportsUnsubscribe;
}
- public ServerCapability(boolean supportsBatching, boolean supportsRecentMempool, boolean supportsBlockStats) {
- this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsRecentMempool, supportsBlockStats);
+ public ServerCapability(boolean supportsBatching, boolean supportsRecentMempool, boolean supportsBlockStats, boolean supportsUnsubscribe) {
+ this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsRecentMempool, supportsBlockStats, supportsUnsubscribe);
}
- public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsRecentMempool, boolean supportsBlockStats) {
+ public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsRecentMempool, boolean supportsBlockStats, boolean supportsUnsubscribe) {
this.supportsBatching = supportsBatching;
this.maxTargetBlocks = maxTargetBlocks;
this.supportsRecentMempool = supportsRecentMempool;
this.supportsBlockStats = supportsBlockStats;
+ this.supportsUnsubscribe = supportsUnsubscribe;
}
public boolean supportsBatching() {
@@ -45,4 +48,8 @@ public class ServerCapability {
public boolean supportsBlockStats() {
return supportsBlockStats;
}
+
+ public boolean supportsUnsubscribe() {
+ return supportsUnsubscribe;
+ }
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java
index 7a16e03b..91179334 100644
--- a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java
+++ b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java
@@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
+import com.sparrowwallet.sparrow.io.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -38,16 +39,32 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
@Override
public List getServerVersion(Transport transport, String clientName, String[] supportedVersions) {
+ if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER && Config.get().isLegacyServer()) {
+ return getLegacyServerVersion(transport, clientName);
+ }
+
try {
JsonRpcClient client = new JsonRpcClient(transport);
- //Using 1.4 as the version number as EPS tries to parse this number to a float :(
return new RetryLogic>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
- client.createRequest().returnAsList(String.class).method("server.version").id(idCounter.incrementAndGet()).params(clientName, "1.4").execute());
+ client.createRequest().returnAsList(String.class).method("server.version").id(idCounter.incrementAndGet()).params(clientName, supportedVersions).execute());
+ } catch(JsonRpcException e) {
+ return getLegacyServerVersion(transport, clientName);
} catch(Exception e) {
throw new ElectrumServerRpcException("Error getting server version", e);
}
}
+ private List getLegacyServerVersion(Transport transport, String clientName) {
+ try {
+ //Fallback to using 1.4 as the version number as EPS tries to parse this number to a float :(
+ JsonRpcClient client = new JsonRpcClient(transport);
+ return new RetryLogic>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
+ client.createRequest().returnAsList(String.class).method("server.version").id(idCounter.incrementAndGet()).params(clientName, "1.4").execute());
+ } catch(Exception ex) {
+ throw new ElectrumServerRpcException("Error getting legacy server version", ex);
+ }
+ }
+
@Override
public String getServerBanner(Transport transport) {
try {
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java b/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java
index 1410a0a0..b7b16a33 100644
--- a/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java
+++ b/src/main/java/com/sparrowwallet/sparrow/net/SubscriptionService.java
@@ -38,6 +38,6 @@ public class SubscriptionService {
existingStatuses.add(status);
}
- Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHash)));
+ Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHash, status)));
}
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerService.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerService.java
index 9a3e71cf..65e3529d 100644
--- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerService.java
+++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerService.java
@@ -35,10 +35,11 @@ public class ElectrumServerService {
}
@JsonRpcMethod("server.version")
- public List getServerVersion(@JsonRpcParam("client_name") String clientName, @JsonRpcParam("protocol_version") String protocolVersion) throws UnsupportedVersionException {
- Version clientVersion = new Version(protocolVersion);
+ public List getServerVersion(@JsonRpcParam("client_name") String clientName, @JsonRpcParam("protocol_version") String[] protocolVersion) throws UnsupportedVersionException {
+ String version = protocolVersion.length > 1 ? protocolVersion[1] : protocolVersion[0];
+ Version clientVersion = new Version(version);
if(clientVersion.compareTo(VERSION) < 0) {
- throw new UnsupportedVersionException(protocolVersion);
+ throw new UnsupportedVersionException(version);
}
return List.of(Cormorant.SERVER_NAME + " " + SparrowWallet.APP_VERSION, VERSION.get());
diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java
index 967d2386..4fd6b76e 100644
--- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java
@@ -1559,6 +1559,23 @@ public class HeadersController extends TransactionFormController implements Init
signButtonBox.setVisible(false);
broadcastButtonBox.setVisible(true);
+
+ if(Config.get().hasServer() && !AppServices.isConnected() && !AppServices.isConnecting()) {
+ if(Config.get().getConnectToBroadcast() == null) {
+ Platform.runLater(() -> {
+ ConfirmationAlert confirmationAlert = new ConfirmationAlert("Connect to broadcast?", "Connect to the configured server to broadcast the transaction?", ButtonType.NO, ButtonType.YES);
+ Optional optType = confirmationAlert.showAndWait();
+ if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
+ Config.get().setConnectToBroadcast(optType.get() == ButtonType.YES);
+ }
+ if(optType.isPresent() && optType.get() == ButtonType.YES) {
+ EventManager.get().post(new RequestConnectEvent());
+ }
+ });
+ } else if(Config.get().getConnectToBroadcast()) {
+ Platform.runLater(() -> EventManager.get().post(new RequestConnectEvent()));
+ }
+ }
}
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java
index 720c6b04..57f4d970 100644
--- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java
@@ -326,7 +326,7 @@ public class SendController extends WalletFormController implements Initializabl
recentBlocksView.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.RECENT_BLOCKS));
List blockSummaries = AppServices.getBlockSummaries().values().stream().sorted().toList();
if(!blockSummaries.isEmpty()) {
- recentBlocksView.update(blockSummaries, AppServices.getDefaultFeeRate());
+ recentBlocksView.update(blockSummaries, AppServices.getNextBlockMedianFeeRate());
}
feeRatesSelectionProperty.addListener((_, oldValue, newValue) -> {
@@ -491,11 +491,35 @@ public class SendController extends WalletFormController implements Initializabl
validationSupport.setErrorDecorationEnabled(false);
}
- public Tab addPaymentTab() {
+ public void addPaymentTab() {
+ if(Config.get().getSuggestSendToMany() == null && openSendToMany()) {
+ return;
+ }
+
Tab tab = getPaymentTab();
paymentTabs.getTabs().add(tab);
paymentTabs.getSelectionModel().select(tab);
- return tab;
+ }
+
+ private boolean openSendToMany() {
+ try {
+ List payments = getPayments();
+ if(payments.size() == 3) {
+ ConfirmationAlert confirmationAlert = new ConfirmationAlert("Open Send To Many?", "Open the Tools > Send To Many dialog to add multiple payments?", ButtonType.NO, ButtonType.YES);
+ Optional optType = confirmationAlert.showAndWait();
+ if(confirmationAlert.isDontAskAgain() && optType.isPresent()) {
+ Config.get().setSuggestSendToMany(optType.get() == ButtonType.YES);
+ }
+ if(optType.isPresent() && optType.get() == ButtonType.YES) {
+ Platform.runLater(() -> EventManager.get().post(new RequestSendToManyEvent(payments)));
+ return true;
+ }
+ }
+ } catch(Exception e) {
+ //ignore
+ }
+
+ return false;
}
public Tab getPaymentTab() {
@@ -1205,7 +1229,7 @@ public class SendController extends WalletFormController implements Initializabl
public void broadcastNotification(Wallet decryptedWallet) {
try {
- PaymentCode paymentCode = decryptedWallet.getPaymentCode();
+ PaymentCode paymentCode = decryptedWallet.isMasterWallet() ? decryptedWallet.getPaymentCode() : decryptedWallet.getMasterWallet().getPaymentCode();
PaymentCode externalPaymentCode = paymentCodeProperty.get();
WalletTransaction walletTransaction = walletTransactionProperty.get();
WalletNode input0Node = walletTransaction.getSelectedUtxos().entrySet().iterator().next().getValue();
@@ -1411,7 +1435,12 @@ public class SendController extends WalletFormController implements Initializabl
setFeeRatePriority(getFeeRangeRate());
}
feeRange.updateTrackHighlight();
- recentBlocksView.updateFeeRate(event.getTargetBlockFeeRates());
+
+ if(event.getNextBlockMedianFeeRate() != null) {
+ recentBlocksView.updateFeeRate(event.getNextBlockMedianFeeRate());
+ } else {
+ recentBlocksView.updateFeeRate(event.getTargetBlockFeeRates());
+ }
if(updateDefaultFeeRate) {
if(getFeeRate() != null && Long.valueOf((long)getFallbackFeeRate()).equals(getFeeRate().longValue())) {
@@ -1435,7 +1464,7 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe
public void blockSummary(BlockSummaryEvent event) {
- Platform.runLater(() -> recentBlocksView.update(AppServices.getBlockSummaries().values().stream().sorted().toList(), AppServices.getDefaultFeeRate()));
+ Platform.runLater(() -> recentBlocksView.update(AppServices.getBlockSummaries().values().stream().sorted().toList(), AppServices.getNextBlockMedianFeeRate()));
}
@Subscribe
diff --git a/src/main/resources/image/walletmodel/specter-icon-invert.svg b/src/main/resources/image/walletmodel/specter-icon-invert.svg
index 73eca817..3bf323da 100644
--- a/src/main/resources/image/walletmodel/specter-icon-invert.svg
+++ b/src/main/resources/image/walletmodel/specter-icon-invert.svg
@@ -5,11 +5,11 @@
-
-
-
-
-
+
+
+
+
+
diff --git a/src/main/resources/image/walletmodel/specter-icon.svg b/src/main/resources/image/walletmodel/specter-icon.svg
index 27a5d7d2..53c43b51 100644
--- a/src/main/resources/image/walletmodel/specter-icon.svg
+++ b/src/main/resources/image/walletmodel/specter-icon.svg
@@ -5,11 +5,11 @@
-
-
-
-
-
+
+
+
+
+