From 853949675eea5a0274a4d883d3cd3fb7e3e60ee8 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 22 May 2025 08:44:39 +0200 Subject: [PATCH 01/24] fix npe configuring recent blocks view on new installs --- .../com/sparrowwallet/sparrow/control/BlockCube.java | 7 ++++++- .../sparrow/control/RecentBlocksView.java | 10 +++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) 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/RecentBlocksView.java b/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java index db47bd84..653c2e46 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); } @@ -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) { From 52470ee6d8ca18918b220b7eccce7e69d996a392 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 22 May 2025 11:59:25 +0200 Subject: [PATCH 02/24] further electrum server optimization tweaks --- .../sparrow/net/ElectrumServer.java | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 83441f97..88972ada 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -1048,6 +1048,10 @@ public class ElectrumServer { List recentTransactions = feeRatesSource.getRecentMempoolTransactions(); Map setReferences = new HashMap<>(); setReferences.put(recentTransactions.getFirst(), null); + Random random = new Random(); + if(random.nextBoolean()) { + setReferences.put(recentTransactions.get(random.nextInt(recentTransactions.size())), null); + } Map transactions = getTransactions(null, setReferences, Collections.emptyMap()); return transactions.values().stream().filter(blxTx -> blxTx.getTransaction() != null).toList(); } catch(Exception e) { @@ -2005,12 +2009,15 @@ public class ElectrumServer { 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)); } + if(Math.random() < 0.1d) { + break; + } } } @@ -2023,21 +2030,31 @@ public class ElectrumServer { } } + 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())); + electrumServer.broadcastTransaction(blkTx.getTransaction()); + } } return null; } }; } }; - broadcastService.setDelay(Duration.seconds(Math.random() * 60 * 10)); + broadcastService.setDelay(Duration.seconds(Math.random() * 60 )); broadcastService.setPeriod(Duration.hours(1)); broadcastService.setOnSucceeded(_ -> broadcastService.cancel()); broadcastService.setOnFailed(_ -> broadcastService.cancel()); From 231eb13ceeb1599f182102368f7899bf6172d55c Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 22 May 2025 13:35:59 +0200 Subject: [PATCH 03/24] retrieve and show next block median fee rate in recent blocks view where available --- .../sparrowwallet/sparrow/AppServices.java | 8 +++++ .../sparrow/control/RecentBlocksView.java | 4 +-- .../sparrow/event/BlockSummaryEvent.java | 8 ++++- .../sparrow/event/FeeRatesUpdatedEvent.java | 10 ++++++ .../sparrow/net/ElectrumServer.java | 28 ++++++++++++--- .../sparrow/net/FeeRatesSource.java | 36 +++++++++++++++++++ .../sparrow/wallet/SendController.java | 11 ++++-- 7 files changed, 95 insertions(+), 10 deletions(-) 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/control/RecentBlocksView.java b/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java index 653c2e46..bb0df858 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java @@ -57,7 +57,7 @@ public class RecentBlocksView extends Pane { } public void updateFeeRatesSource(FeeRatesSource feeRatesSource) { - tooltip.setText("Fee rate estimate from " + feeRatesSource.getDescription()); + tooltip.setText("Fee rates from " + feeRatesSource.getDescription()); if(getCubes() != null && !getCubes().isEmpty()) { getCubes().getFirst().setFeeRatesSource(feeRatesSource); } @@ -108,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; } 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/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 88972ada..0c1c3f5f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -936,6 +936,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); @@ -1460,8 +1474,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(); @@ -1939,7 +1954,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); } }; } @@ -1989,7 +2005,11 @@ public class ElectrumServer { subscribeRecent(electrumServer); } - return new BlockSummaryEvent(blockSummaryMap); + Double nextBlockMedianFeeRate = null; + if(!isBlockstorm(totalBlocks)) { + nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate(); + } + return new BlockSummaryEvent(blockSummaryMap, nextBlockMedianFeeRate); } }; } @@ -2054,7 +2074,7 @@ public class ElectrumServer { }; } }; - broadcastService.setDelay(Duration.seconds(Math.random() * 60 )); + broadcastService.setDelay(Duration.seconds(Math.random() * 60 * 10)); broadcastService.setPeriod(Duration.hours(1)); broadcastService.setOnSucceeded(_ -> broadcastService.cancel()); broadcastService.setOnFailed(_ -> broadcastService.cancel()); 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/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 720c6b04..97271657 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) -> { @@ -1411,7 +1411,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 +1440,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 From 4298bfb053de44808ebf4d323a095e6e59d72dad Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 22 May 2025 14:58:09 +0200 Subject: [PATCH 04/24] bump to v2.2.3 --- build.gradle | 2 +- docs/reproducible.md | 2 +- src/main/deploy/package/osx/Info.plist | 2 +- src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 9e482db0..c49a5f79 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.3' repositories { mavenCentral() diff --git a/docs/reproducible.md b/docs/reproducible.md index 5df236eb..c27347fa 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.2" ``` The project can then be initially cloned as follows: diff --git a/src/main/deploy/package/osx/Info.plist b/src/main/deploy/package/osx/Info.plist index dfb179de..04032a37 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.3 CFBundleSignature ???? diff --git a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java index 776ddc53..fd897ff6 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.3"; 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"; From 74c298fd933a296d5251fa96d9bf9d1ed45e3c57 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 29 May 2025 13:58:46 +0200 Subject: [PATCH 05/24] iterate and remove faulty capture devices on opening qr scan dialog --- .../sparrow/control/QRScanDialog.java | 15 ++++---- .../sparrow/control/WebcamService.java | 34 ++++++++++++++----- 2 files changed, 32 insertions(+), 17 deletions(-) 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/WebcamService.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java index e7f97973..bffa0532 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java @@ -35,12 +35,13 @@ public class WebcamService extends ScheduledService { private static final Logger log = LoggerFactory.getLogger(WebcamService.class); 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); @@ -106,23 +107,26 @@ public class WebcamService extends ScheduledService { @Override protected Image call() throws Exception { 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; } @@ -170,15 +174,23 @@ public class WebcamService extends ScheduledService { 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); @@ -211,7 +223,7 @@ public class WebcamService extends ScheduledService { public boolean cancel() { if(stream != null) { stream.close(); - closed.set(true); + opened.set(false); } return super.cancel(); @@ -336,6 +348,10 @@ public class WebcamService extends ScheduledService { return devices; } + public List getAvailableDevices() { + return availableDevices; + } + public Set getResolutions() { return resolutions; } @@ -376,8 +392,8 @@ public class WebcamService extends ScheduledService { return opening; } - public BooleanProperty closedProperty() { - return closed; + public BooleanProperty openedProperty() { + return opened; } public static > T getNearestEnum(T target) { From 3fdf093a26f191f9ad48a6f60b1dc67e9bf57aa2 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 29 May 2025 14:17:58 +0200 Subject: [PATCH 06/24] use semaphore to ensure last webcam service task has completed before closing stream --- .../sparrow/control/WebcamService.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java index bffa0532..c143b414 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java @@ -27,6 +27,8 @@ 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.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -34,6 +36,8 @@ 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, true); + private List devices; private List availableDevices; private Set resolutions; @@ -106,6 +110,7 @@ public class WebcamService extends ScheduledService { return new Task<>() { @Override protected Image call() throws Exception { + taskSemaphore.acquire(); try { if(devices == null) { devices = capture.getDevices(); @@ -207,6 +212,7 @@ public class WebcamService extends ScheduledService { return image; } finally { opening.set(false); + taskSemaphore.release(); } } }; @@ -221,12 +227,24 @@ public class WebcamService extends ScheduledService { @Override public boolean cancel() { + 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"); + } + if(stream != null) { stream.close(); opened.set(false); } - return super.cancel(); + return cancelled; } public void close() { From d7d23f9b58149f177d7f81abc9d8f1e5b9ae1932 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 2 Jun 2025 09:41:46 +0200 Subject: [PATCH 07/24] always use the master wallet payment code when creating the notification transaction payload on the send tab --- .../java/com/sparrowwallet/sparrow/wallet/SendController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 97271657..07cf7619 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -1205,7 +1205,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(); From b0d05146174047068b667b9db7f940cf75e4ba14 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 2 Jun 2025 11:36:06 +0200 Subject: [PATCH 08/24] remove possibility of task queueing in webcam service --- .../sparrow/control/WebcamService.java | 20 +++++++++++++++++-- .../sparrow/control/WebcamView.java | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java index c143b414..6e076a5f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java @@ -29,6 +29,7 @@ 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; @@ -36,7 +37,8 @@ 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, true); + private final Semaphore taskSemaphore = new Semaphore(1); + private final AtomicBoolean cancelRequested = new AtomicBoolean(false); private List devices; private List availableDevices; @@ -110,7 +112,15 @@ public class WebcamService extends ScheduledService { return new Task<>() { @Override protected Image call() throws Exception { - taskSemaphore.acquire(); + if(cancelRequested.get() || isCancelled()) { + return null; + } + + if(!taskSemaphore.tryAcquire()) { + log.warn("Skipped execution of webcam capture task, another task is running"); + return null; + } + try { if(devices == null) { devices = capture.getDevices(); @@ -222,11 +232,13 @@ public class WebcamService extends ScheduledService { public void reset() { stream = null; zoomLimits = null; + cancelRequested.set(false); super.reset(); } @Override public boolean cancel() { + cancelRequested.set(true); boolean cancelled = super.cancel(); try { @@ -414,6 +426,10 @@ public class WebcamService extends ScheduledService { return opened; } + public boolean getCancelRequested() { + return cancelRequested.get(); + } + public static > T getNearestEnum(T target) { return getNearestEnum(target, target.getDeclaringClass().getEnumConstants()); } 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); } }); From 31ce3ce68a9e097552325b32719112c634f251ec Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 2 Jun 2025 15:56:46 +0200 Subject: [PATCH 09/24] further electrum server optimisations --- .../event/WalletNodeHistoryChangedEvent.java | 11 ++++ .../sparrow/net/ElectrumServer.java | 55 +++++++++++++++++-- .../sparrow/net/SubscriptionService.java | 2 +- 3 files changed, 61 insertions(+), 7 deletions(-) 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/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 0c1c3f5f..033fece2 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 Set subscribedRecent = ConcurrentHashMap.newKeySet(); + + private final static Map broadcastRecent = new ConcurrentHashMap<>(); + private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc(); private static Cormorant cormorant; @@ -1062,9 +1066,10 @@ public class ElectrumServer { List recentTransactions = feeRatesSource.getRecentMempoolTransactions(); Map setReferences = new HashMap<>(); setReferences.put(recentTransactions.getFirst(), null); - Random random = new Random(); - if(random.nextBoolean()) { - setReferences.put(recentTransactions.get(random.nextInt(recentTransactions.size())), 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(); @@ -1602,6 +1607,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); + subscribedRecent.addAll(subscribeScriptHashes.values()); + } catch(ElectrumServerRpcException e) { + log.debug("Error subscribing to recent mempool transaction outputs", e); + } + } + } } public static class ReadRunnable implements Runnable { @@ -2018,13 +2048,12 @@ 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); unsubscribeScriptHashes.removeIf(subscribedScriptHashes::containsKey); electrumServerRpc.unsubscribeScriptHashes(transport, unsubscribeScriptHashes); subscribedRecent.removeAll(unsubscribeScriptHashes); + broadcastRecent.clear(); Map subscribeScriptHashes = new HashMap<>(); List recentTransactions = electrumServer.getRecentMempoolTransactions(); @@ -2033,7 +2062,7 @@ public class ElectrumServer { 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; @@ -2042,6 +2071,17 @@ public class ElectrumServer { } if(!subscribeScriptHashes.isEmpty()) { + Random random = new Random(); + int additionalRandomScriptHashes = random.nextInt(8) + 4; + 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()); @@ -2066,6 +2106,9 @@ public class ElectrumServer { 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()); } } 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))); } } From 8885e48ed94cf0606d0985b5bec604540f087e1a Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 2 Jun 2025 16:28:44 +0200 Subject: [PATCH 10/24] request rgb3 pixel format on linux where returned format is unsupported --- .../sparrow/control/WebcamPixelFormat.java | 11 +++++++++-- .../sparrowwallet/sparrow/control/WebcamService.java | 7 ++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WebcamPixelFormat.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamPixelFormat.java index fe72cf65..dd6bf68f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamPixelFormat.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamPixelFormat.java @@ -6,8 +6,7 @@ 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_MJPG("MJPG", true); private final String name; private final boolean supported; @@ -25,6 +24,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 6e076a5f..39760e08 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java @@ -182,8 +182,13 @@ public class WebcamService extends ScheduledService { } } + //On Linux, formats not defined in WebcamPixelFormat are unsupported so ask for RGB3 + if(OsType.getCurrent() == OsType.UNIX && WebcamPixelFormat.fromFourCC(format.getFormatInfo().fourcc) == null) { + format.getFormatInfo().fourcc = WebcamPixelFormat.PIX_FMT_RGB24.getFourCC(); + } + 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); From 38f0068411e37adc1cbdeae563d57064e01256ac Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 3 Jun 2025 12:38:03 +0200 Subject: [PATCH 11/24] detect if electrum server supports scripthash unsubscribe capability --- .../sparrow/net/ElectrumServer.java | 18 ++++++++++-------- .../sparrow/net/ServerCapability.java | 19 +++++++++++++------ 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 033fece2..75baf3a3 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -1255,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, false); } if(server.startsWith("cormorant")) { - return new ServerCapability(true, false, true); + return new ServerCapability(true, false, true, false); } if(server.startsWith("electrs/")) { @@ -1271,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 @@ -1287,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 @@ -1306,7 +1306,7 @@ 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 @@ -1314,7 +1314,7 @@ public class ElectrumServer { } } - return new ServerCapability(false); + return new ServerCapability(false, true); } public static class ServerVersionService extends Service> { @@ -2051,7 +2051,9 @@ public class ElectrumServer { private void subscribeRecent(ElectrumServer electrumServer) { Set unsubscribeScriptHashes = new HashSet<>(subscribedRecent); unsubscribeScriptHashes.removeIf(subscribedScriptHashes::containsKey); - electrumServerRpc.unsubscribeScriptHashes(transport, unsubscribeScriptHashes); + if(!unsubscribeScriptHashes.isEmpty() && serverCapability.supportsUnsubscribe()) { + electrumServerRpc.unsubscribeScriptHashes(transport, unsubscribeScriptHashes); + } subscribedRecent.removeAll(unsubscribeScriptHashes); broadcastRecent.clear(); @@ -2072,7 +2074,7 @@ public class ElectrumServer { if(!subscribeScriptHashes.isEmpty()) { Random random = new Random(); - int additionalRandomScriptHashes = random.nextInt(8) + 4; + int additionalRandomScriptHashes = random.nextInt(4) + 4; for(int i = 0; i < additionalRandomScriptHashes; i++) { byte[] randomScriptHashBytes = new byte[32]; random.nextBytes(randomScriptHashBytes); 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; + } } From 364909cfa3164d349799a11cc109b3d9e818dce6 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 3 Jun 2025 12:48:01 +0200 Subject: [PATCH 12/24] support nv12 capture pixel format on linux --- build.gradle | 2 +- .../com/sparrowwallet/sparrow/control/WebcamPixelFormat.java | 1 + .../java/com/sparrowwallet/sparrow/control/WebcamService.java | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index c49a5f79..99b1bd47 100644 --- a/build.gradle +++ b/build.gradle @@ -74,7 +74,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/src/main/java/com/sparrowwallet/sparrow/control/WebcamPixelFormat.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamPixelFormat.java index dd6bf68f..9845fe79 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamPixelFormat.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamPixelFormat.java @@ -6,6 +6,7 @@ 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_NV12("NV12", true), PIX_FMT_MJPG("MJPG", true); private final String name; diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java index 39760e08..d5aae62e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java @@ -182,9 +182,9 @@ public class WebcamService extends ScheduledService { } } - //On Linux, formats not defined in WebcamPixelFormat are unsupported so ask for RGB3 + //On Linux, formats not defined in WebcamPixelFormat are unsupported if(OsType.getCurrent() == OsType.UNIX && WebcamPixelFormat.fromFourCC(format.getFormatInfo().fourcc) == null) { - format.getFormatInfo().fourcc = WebcamPixelFormat.PIX_FMT_RGB24.getFourCC(); + log.warn("Unsupported camera pixel format " + WebcamPixelFormat.fourCCToString(format.getFormatInfo().fourcc)); } if(log.isDebugEnabled()) { From 4d93381124a2602cae2021b6fac5196395bbf171 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 4 Jun 2025 14:52:33 +0200 Subject: [PATCH 13/24] improve electrum server script hash unsubscribe support --- .../com/sparrowwallet/sparrow/io/Config.java | 10 ++++++++ .../sparrow/io/ElectrumPersonalServer.java | 3 ++- .../sparrow/net/ElectrumServer.java | 23 +++++++++++-------- .../sparrow/net/SimpleElectrumServerRpc.java | 21 +++++++++++++++-- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index f93ad0f4..0a04de2a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -69,6 +69,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; @@ -549,6 +550,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/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 75baf3a3..29114e26 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -76,7 +76,7 @@ public class ElectrumServer { private static final Set sameHeightTxioScriptHashes = ConcurrentHashMap.newKeySet(); - private final static Set subscribedRecent = ConcurrentHashMap.newKeySet(); + private final static Map subscribedRecent = new ConcurrentHashMap<>(); private final static Map broadcastRecent = new ConcurrentHashMap<>(); @@ -1255,7 +1255,7 @@ public class ElectrumServer { if(!serverVersion.isEmpty()) { String server = serverVersion.getFirst().toLowerCase(Locale.ROOT); if(server.contains("electrumx")) { - return new ServerCapability(true, false); + return new ServerCapability(true, true); } if(server.startsWith("cormorant")) { @@ -1312,6 +1312,10 @@ public class ElectrumServer { //ignore } } + + if(server.startsWith("electrumpersonalserver")) { + return new ServerCapability(false, false); + } } return new ServerCapability(false, true); @@ -1626,7 +1630,7 @@ public class ElectrumServer { try { electrumServerRpc.subscribeScriptHashes(transport, null, subscribeScriptHashes); - subscribedRecent.addAll(subscribeScriptHashes.values()); + subscribeScriptHashes.values().forEach(scriptHash -> subscribedRecent.put(scriptHash, AppServices.getCurrentBlockHeight())); } catch(ElectrumServerRpcException e) { log.debug("Error subscribing to recent mempool transaction outputs", e); } @@ -2032,7 +2036,7 @@ 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()); } Double nextBlockMedianFeeRate = null; @@ -2048,13 +2052,14 @@ public class ElectrumServer { return Network.get() != Network.MAINNET && totalBlocks > 2; } - 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); if(!unsubscribeScriptHashes.isEmpty() && serverCapability.supportsUnsubscribe()) { electrumServerRpc.unsubscribeScriptHashes(transport, unsubscribeScriptHashes); } - subscribedRecent.removeAll(unsubscribeScriptHashes); + subscribedRecent.keySet().removeAll(unsubscribeScriptHashes); broadcastRecent.clear(); Map subscribeScriptHashes = new HashMap<>(); @@ -2074,7 +2079,7 @@ public class ElectrumServer { if(!subscribeScriptHashes.isEmpty()) { Random random = new Random(); - int additionalRandomScriptHashes = random.nextInt(4) + 4; + int additionalRandomScriptHashes = random.nextInt(8); for(int i = 0; i < additionalRandomScriptHashes; i++) { byte[] randomScriptHashBytes = new byte[32]; random.nextBytes(randomScriptHashBytes); @@ -2086,7 +2091,7 @@ public class ElectrumServer { 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); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java index 7a16e03b..d660009c 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().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 { From 890f0476b144af041d7641322c10bfa1e0a58931 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 4 Jun 2025 15:23:48 +0200 Subject: [PATCH 14/24] introduce delay before closing capture library --- .../java/com/sparrowwallet/sparrow/control/QRScanDialog.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java index 608468c7..921a1e12 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java @@ -31,6 +31,7 @@ import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder; import com.sparrowwallet.sparrow.io.bbqr.BBQRException; import com.sparrowwallet.sparrow.wallet.KeystoreController; +import io.reactivex.Observable; import javafx.application.Platform; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; @@ -57,6 +58,7 @@ import java.nio.charset.CharsetDecoder; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -201,7 +203,8 @@ public class QRScanDialog extends Dialog { Platform.runLater(() -> { webcamResolutionProperty.set(null); - webcamService.close(); + Observable.just(this).delay(500, TimeUnit.MILLISECONDS) + .subscribe(_ -> webcamService.close(), throwable -> log.error("Error closing webcam", throwable)); }); }); From c265fd1969421cf6f96d0ac000853f8ff4d59191 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 4 Jun 2025 17:18:31 +0200 Subject: [PATCH 15/24] fix cormorant server.version rpc issue --- .../sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java | 2 +- .../net/cormorant/electrum/ElectrumServerService.java | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java index d660009c..91179334 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java @@ -39,7 +39,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc { @Override public List getServerVersion(Transport transport, String clientName, String[] supportedVersions) { - if(Config.get().isLegacyServer()) { + if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER && Config.get().isLegacyServer()) { return getLegacyServerVersion(transport, clientName); } 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()); From 799cac7b1f75049ddf834a3752138ba1f1da4e64 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 5 Jun 2025 08:28:21 +0200 Subject: [PATCH 16/24] handle bitkey descriptor export format --- src/main/java/com/sparrowwallet/sparrow/io/Descriptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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()); } } From 25770c24262f8bd80b8f0090884178b02b676de9 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 5 Jun 2025 09:40:17 +0200 Subject: [PATCH 17/24] suggest connecting to broadcast a finalized transaction if offlineand a server is configured --- .../sparrow/control/ConfirmationAlert.java | 39 +++++++++++++++++++ .../com/sparrowwallet/sparrow/io/Config.java | 11 ++++++ .../transaction/HeadersController.java | 17 ++++++++ 3 files changed, 67 insertions(+) create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/ConfirmationAlert.java 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/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index 0a04de2a..fa375aab 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -52,6 +52,7 @@ public class Config { private boolean showDeprecatedImportExport = false; private boolean signBsmsExports = false; private boolean preventSleep = false; + private Boolean connectToBroadcast; private List recentWalletFiles; private Integer keyDerivationPeriod; private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS; @@ -348,6 +349,16 @@ 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 List getRecentWalletFiles() { 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())); + } + } } } From f28e00b97e1d60cd4abe526b5e9cd56518ceb955 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 5 Jun 2025 10:31:37 +0200 Subject: [PATCH 18/24] suggest opening the send to many dialog when adding multiple payments on the send tab --- .../sparrowwallet/sparrow/AppController.java | 11 +++++++- .../sparrow/control/SendToManyDialog.java | 7 ++--- .../sparrow/event/RequestSendToManyEvent.java | 17 +++++++++++ .../com/sparrowwallet/sparrow/io/Config.java | 10 +++++++ .../sparrow/wallet/SendController.java | 28 +++++++++++++++++-- 5 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/RequestSendToManyEvent.java diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 5bfaf9fb..55022f5b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1422,6 +1422,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); @@ -1437,7 +1441,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; @@ -3107,6 +3111,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/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/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/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index fa375aab..0e587b26 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -53,6 +53,7 @@ public class Config { 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; @@ -361,6 +362,15 @@ public class Config { flush(); } + public Boolean getSuggestSendToMany() { + return suggestSendToMany; + } + + public void setSuggestSendToMany(Boolean suggestSendToMany) { + this.suggestSendToMany = suggestSendToMany; + flush(); + } + public List getRecentWalletFiles() { return recentWalletFiles; } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 07cf7619..57f4d970 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -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() { From ebce34f3d1e32811ef16e1cfdc73df78f010fb62 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 5 Jun 2025 14:23:02 +0200 Subject: [PATCH 19/24] minor tweaks --- .../com/sparrowwallet/sparrow/control/RecentBlocksView.java | 2 +- src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java b/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java index bb0df858..53f76084 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java @@ -57,7 +57,7 @@ public class RecentBlocksView extends Pane { } public void updateFeeRatesSource(FeeRatesSource feeRatesSource) { - tooltip.setText("Fee rates from " + feeRatesSource.getDescription()); + tooltip.setText("Fee rate estimate from " + feeRatesSource.getDescription()); if(getCubes() != null && !getCubes().isEmpty()) { getCubes().getFirst().setFeeRatesSource(feeRatesSource); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 29114e26..c262e3ba 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -2060,7 +2060,7 @@ public class ElectrumServer { electrumServerRpc.unsubscribeScriptHashes(transport, unsubscribeScriptHashes); } subscribedRecent.keySet().removeAll(unsubscribeScriptHashes); - broadcastRecent.clear(); + broadcastRecent.keySet().removeAll(unsubscribeScriptHashes); Map subscribeScriptHashes = new HashMap<>(); List recentTransactions = electrumServer.getRecentMempoolTransactions(); From 26ce1b34697e2c23297634a63dd7f37394bee0c0 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 6 Jun 2025 11:45:46 +0200 Subject: [PATCH 20/24] derive to maximum bip32 account level where child path in output descriptor contains more than two elements --- drongo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drongo b/drongo index abb598d3..ad02b8a3 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit abb598d3b041a9d0b3d0ba41b5fb9785e2100193 +Subproject commit ad02b8a33c1d8412909528e7a407ccaef515d153 From e4dd4950bf7ca12ec79f0d2b14e1fb9e209484e5 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 6 Jun 2025 13:07:36 +0200 Subject: [PATCH 21/24] prevent selection of unsupported bip322 format when signing a message with a connected device --- .../sparrow/control/MessageSignDialog.java | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) 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(); From a94380e882e3afff5faa4a4a4a5879bc8254835f Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Sat, 7 Jun 2025 11:23:06 +0200 Subject: [PATCH 22/24] minor specter diy ui tweaks --- drongo | 2 +- .../image/walletmodel/specter-icon-invert.svg | 10 +++++----- src/main/resources/image/walletmodel/specter-icon.svg | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/drongo b/drongo index ad02b8a3..13e1fafb 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit ad02b8a33c1d8412909528e7a407ccaef515d153 +Subproject commit 13e1fafbe8892d7005a043ae561e09ed66f7cea6 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 @@ - - - - - + + + + + From 73d4fd5049ccff4767df83476c7471fc2f1ca413 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 9 Jun 2025 14:43:06 +0200 Subject: [PATCH 23/24] prevent double free when closing capture library --- .../sparrowwallet/sparrow/control/QRScanDialog.java | 5 +---- .../sparrowwallet/sparrow/control/WebcamService.java | 10 +++++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java index 921a1e12..608468c7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QRScanDialog.java @@ -31,7 +31,6 @@ import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder; import com.sparrowwallet.sparrow.io.bbqr.BBQRException; import com.sparrowwallet.sparrow.wallet.KeystoreController; -import io.reactivex.Observable; import javafx.application.Platform; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; @@ -58,7 +57,6 @@ import java.nio.charset.CharsetDecoder; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.List; -import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -203,8 +201,7 @@ public class QRScanDialog extends Dialog { Platform.runLater(() -> { webcamResolutionProperty.set(null); - Observable.just(this).delay(500, TimeUnit.MILLISECONDS) - .subscribe(_ -> webcamService.close(), throwable -> log.error("Error closing webcam", throwable)); + webcamService.close(); }); }); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java index d5aae62e..ab3f4b68 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WebcamService.java @@ -39,6 +39,7 @@ public class WebcamService extends ScheduledService { 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; @@ -112,7 +113,7 @@ public class WebcamService extends ScheduledService { return new Task<>() { @Override protected Image call() throws Exception { - if(cancelRequested.get() || isCancelled()) { + if(cancelRequested.get() || isCancelled() || captureClosed.get()) { return null; } @@ -264,8 +265,11 @@ public class WebcamService extends ScheduledService { return cancelled; } - public void close() { - capture.close(); + public synchronized void close() { + if(!captureClosed.get()) { + captureClosed.set(true); + capture.close(); + } } public PropertyLimits getZoomLimits() { From 3aae26b19682a2c318245d4bb4d6757b2556b90c Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 10 Jun 2025 09:01:37 +0200 Subject: [PATCH 24/24] bump to v2.2.4 --- build.gradle | 2 +- docs/reproducible.md | 2 +- src/main/deploy/package/osx/Info.plist | 2 +- src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index 99b1bd47..85297ae5 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.3' +version '2.2.4' repositories { mavenCentral() diff --git a/docs/reproducible.md b/docs/reproducible.md index c27347fa..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.2" +GIT_TAG="2.2.3" ``` The project can then be initially cloned as follows: diff --git a/src/main/deploy/package/osx/Info.plist b/src/main/deploy/package/osx/Info.plist index 04032a37..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.3 + 2.2.4 CFBundleSignature ???? diff --git a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java index fd897ff6..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.3"; + 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";