From 94b27ba7e8eb6b49c2902061009134592e0b1b8f Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 14 May 2025 08:19:21 +0200 Subject: [PATCH 01/13] add recent blocks view --- .../sparrowwallet/sparrow/AppServices.java | 25 +- .../sparrowwallet/sparrow/BlockSummary.java | 17 +- .../sparrow/event/BlockCube.java | 323 ++++++++++++++++++ .../sparrow/event/RecentBlocksView.java | 150 ++++++++ .../sparrow/net/ElectrumServer.java | 11 +- .../sparrow/net/FeeRatesSource.java | 4 +- .../sparrow/wallet/FeeRatesSelection.java | 2 +- .../sparrow/wallet/SendController.java | 64 ++-- .../com/sparrowwallet/sparrow/darktheme.css | 28 ++ .../com/sparrowwallet/sparrow/wallet/send.css | 102 ++++++ .../sparrowwallet/sparrow/wallet/send.fxml | 10 + 11 files changed, 696 insertions(+), 40 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/RecentBlocksView.java diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index dcfd8c13..e5adc238 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -305,12 +305,6 @@ public class AppServices { if(event != null) { EventManager.get().post(event); } - - FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource(); - feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource); - if(event instanceof ConnectionEvent && feeRatesSource.supportsNetwork(Network.get()) && feeRatesSource.isExternal()) { - EventManager.get().post(new FeeRatesSourceChangedEvent(feeRatesSource)); - } }); connectionService.setOnFailed(failEvent -> { //Close connection here to create a new transport next time we try @@ -494,6 +488,13 @@ public class AppServices { } } + private void fetchFeeRates() { + if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) { + feeRatesService = createFeeRatesService(); + feeRatesService.start(); + } + } + private void fetchBlockSummaries(List newBlockEvents) { if(isConnected()) { ElectrumServer.BlockSummaryService blockSummaryService = new ElectrumServer.BlockSummaryService(newBlockEvents); @@ -1216,6 +1217,12 @@ public class AppServices { latestBlockHeader = event.getBlockHeader(); Config.get().addRecentServer(); + FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource(); + feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource); + if(feeRatesSource.supportsNetwork(Network.get()) && feeRatesSource.isExternal()) { + fetchFeeRates(); + } + if(!blockSummaries.containsKey(currentBlockHeight)) { fetchBlockSummaries(Collections.emptyList()); } @@ -1259,10 +1266,8 @@ public class AppServices { @Subscribe public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) { //Perform once-off fee rates retrieval to immediately change displayed rates - if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) { - feeRatesService = createFeeRatesService(); - feeRatesService.start(); - } + fetchFeeRates(); + fetchBlockSummaries(Collections.emptyList()); } @Subscribe diff --git a/src/main/java/com/sparrowwallet/sparrow/BlockSummary.java b/src/main/java/com/sparrowwallet/sparrow/BlockSummary.java index 1c9c9a41..bc113eb1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/BlockSummary.java +++ b/src/main/java/com/sparrowwallet/sparrow/BlockSummary.java @@ -5,21 +5,23 @@ import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.Optional; -public class BlockSummary { +public class BlockSummary implements Comparable { private final Integer height; private final Date timestamp; private final Double medianFee; private final Integer transactionCount; + private final Integer weight; public BlockSummary(Integer height, Date timestamp) { - this(height, timestamp, null, null); + this(height, timestamp, null, null, null); } - public BlockSummary(Integer height, Date timestamp, Double medianFee, Integer transactionCount) { + public BlockSummary(Integer height, Date timestamp, Double medianFee, Integer transactionCount, Integer weight) { this.height = height; this.timestamp = timestamp; this.medianFee = medianFee; this.transactionCount = transactionCount; + this.weight = weight; } public Integer getHeight() { @@ -38,6 +40,10 @@ public class BlockSummary { return transactionCount == null ? Optional.empty() : Optional.of(transactionCount); } + public Optional getWeight() { + return weight == null ? Optional.empty() : Optional.of(weight); + } + private static long calculateElapsedSeconds(long timestampUtc) { Instant timestampInstant = Instant.ofEpochMilli(timestampUtc); Instant nowInstant = Instant.now(); @@ -62,4 +68,9 @@ public class BlockSummary { public String toString() { return getElapsed() + ":" + getMedianFee(); } + + @Override + public int compareTo(BlockSummary o) { + return o.height - height; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java b/src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java new file mode 100644 index 00000000..151202a0 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java @@ -0,0 +1,323 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.Network; +import com.sparrowwallet.drongo.protocol.Transaction; +import com.sparrowwallet.sparrow.BlockSummary; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.property.*; +import javafx.scene.Group; +import javafx.scene.shape.Polygon; +import javafx.scene.shape.Rectangle; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; +import javafx.util.Duration; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; + +public class BlockCube extends Group { + public static final List MEMPOOL_FEE_RATES_INTERVALS = List.of(1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000); + + public static final double CUBE_SIZE = 60; + + private final IntegerProperty weightProperty = new SimpleIntegerProperty(0); + private final DoubleProperty medianFeeProperty = new SimpleDoubleProperty(0); + private final IntegerProperty heightProperty = new SimpleIntegerProperty(0); + private final IntegerProperty txCountProperty = new SimpleIntegerProperty(0); + private final LongProperty timestampProperty = new SimpleLongProperty(System.currentTimeMillis()); + private final StringProperty elapsedProperty = new SimpleStringProperty(""); + private final BooleanProperty confirmedProperty = new SimpleBooleanProperty(false); + + private Polygon front; + private Rectangle unusedArea; + private Rectangle usedArea; + + private final Text heightText = new Text(); + private final Text medianFeeText = new Text(); + private final Text txCountText = new Text(); + private final Text elapsedText = new Text(); + + 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.weightProperty.addListener((_, _, _) -> { + if(front != null) { + updateFill(); + } + }); + this.medianFeeProperty.addListener((_, _, newValue) -> { + medianFeeText.setText("~" + Math.round(Math.max(newValue.doubleValue(), 1.0d)) + " s/vb"); + medianFeeText.setX((CUBE_SIZE - medianFeeText.getLayoutBounds().getWidth()) / 2); + }); + this.txCountProperty.addListener((_, _, newValue) -> { + txCountText.setText(newValue.intValue() == 0 ? "" : newValue + " txes"); + txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2); + }); + this.timestampProperty.addListener((_, _, newValue) -> { + elapsedProperty.set(getElapsed(newValue.longValue())); + }); + this.elapsedProperty.addListener((_, _, newValue) -> { + elapsedText.setText(isConfirmed() ? newValue : "In ~10m"); + elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2); + }); + this.heightProperty.addListener((_, _, newValue) -> { + heightText.setText(newValue.intValue() == 0 ? "" : String.valueOf(newValue)); + heightText.setX(((CUBE_SIZE * 0.7) - heightText.getLayoutBounds().getWidth()) / 2); + }); + this.confirmedProperty.addListener((_, _, _) -> { + if(front != null) { + updateFill(); + } + }); + this.medianFeeText.textProperty().addListener((_, _, _) -> { + pulse(); + }); + + if(weight != null) { + this.weightProperty.set(weight); + } + if(medianFee != null) { + this.medianFeeProperty.set(medianFee); + } + if(height != null) { + this.heightProperty.set(height); + } + if(txCount != null) { + this.txCountProperty.set(txCount); + } + if(timestamp != null) { + this.timestampProperty.set(timestamp); + } + + drawCube(); + } + + private void drawCube() { + double depth = CUBE_SIZE * 0.2; + double perspective = CUBE_SIZE * 0.04; + + front = new Polygon(0, 0, CUBE_SIZE, 0, CUBE_SIZE, CUBE_SIZE, 0, CUBE_SIZE); + front.getStyleClass().add("block-front"); + front.setFill(null); + unusedArea = new Rectangle(0, 0, CUBE_SIZE, CUBE_SIZE); + unusedArea.getStyleClass().add("block-unused"); + usedArea = new Rectangle(0, 0, CUBE_SIZE, CUBE_SIZE); + usedArea.getStyleClass().add("block-used"); + + Group frontFaceGroup = new Group(front, unusedArea, usedArea); + + Polygon top = new Polygon(0, 0, CUBE_SIZE, 0, CUBE_SIZE - depth - perspective, -depth, -depth, -depth); + top.getStyleClass().add("block-top"); + top.setStroke(null); + + Polygon left = new Polygon(0, 0, -depth, -depth, -depth, CUBE_SIZE - depth - perspective, 0, CUBE_SIZE); + left.getStyleClass().add("block-left"); + left.setStroke(null); + + updateFill(); + + heightText.getStyleClass().add("block-height"); + heightText.setFont(new Font(11)); + heightText.setX(((CUBE_SIZE * 0.7) - heightText.getLayoutBounds().getWidth()) / 2); + heightText.setY(-24); + + medianFeeText.getStyleClass().add("block-text"); + medianFeeText.setFont(Font.font(null, FontWeight.BOLD, 11)); + medianFeeText.setX((CUBE_SIZE - medianFeeText.getLayoutBounds().getWidth()) / 2); + medianFeeText.setY(16); + + txCountText.getStyleClass().add("block-text"); + txCountText.setFont(new Font(10)); + txCountText.setOpacity(0.7); + txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2); + txCountText.setY(34); + + elapsedText.getStyleClass().add("block-text"); + elapsedText.setFont(new Font(10)); + elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2); + elapsedText.setY(50); + + getChildren().addAll(frontFaceGroup, top, left, heightText, medianFeeText, txCountText, elapsedText); + } + + private void updateFill() { + if(isConfirmed()) { + getStyleClass().removeAll("block-unconfirmed"); + if(!getStyleClass().contains("block-confirmed")) { + getStyleClass().add("block-confirmed"); + } + double startY = 1 - weightProperty.doubleValue() / (Transaction.MAX_BLOCK_SIZE_VBYTES * Transaction.WITNESS_SCALE_FACTOR); + double startYAbsolute = startY * BlockCube.CUBE_SIZE; + unusedArea.setHeight(startYAbsolute); + unusedArea.setStyle(null); + usedArea.setY(startYAbsolute); + usedArea.setHeight(CUBE_SIZE - startYAbsolute); + usedArea.setVisible(true); + heightText.setVisible(true); + } else { + getStyleClass().removeAll("block-confirmed"); + if(!getStyleClass().contains("block-unconfirmed")) { + getStyleClass().add("block-unconfirmed"); + } + usedArea.setVisible(false); + unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";"); + heightText.setVisible(false); + } + } + + public void pulse() { + if(isConfirmed()) { + return; + } + + if(unusedArea != null) { + unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";"); + } + + Timeline timeline = new Timeline( + new KeyFrame(Duration.ZERO, new KeyValue(opacityProperty(), 1.0)), + new KeyFrame(Duration.millis(500), new KeyValue(opacityProperty(), 0.7)), + new KeyFrame(Duration.millis(1000), new KeyValue(opacityProperty(), 1.0)) + ); + + timeline.setCycleCount(1); + timeline.play(); + } + + private static long calculateElapsedSeconds(long timestampUtc) { + Instant timestampInstant = Instant.ofEpochMilli(timestampUtc); + Instant nowInstant = Instant.now(); + return ChronoUnit.SECONDS.between(timestampInstant, nowInstant); + } + + public static String getElapsed(long timestampUtc) { + long elapsed = calculateElapsedSeconds(timestampUtc); + if(elapsed < 60) { + return "Just now"; + } else if(elapsed < 3600) { + return Math.round(elapsed / 60f) + "m ago"; + } else if(elapsed < 86400) { + return Math.round(elapsed / 3600f) + "h ago"; + } else { + return Math.round(elapsed / 86400d) + "d ago"; + } + } + + private String getFeeRateStyleName() { + double rate = getMedianFee(); + int[] feeRateInterval = getFeeRateInterval(rate); + if(feeRateInterval[1] == Integer.MAX_VALUE) { + return "VSIZE2000-2200_COLOR"; + } + int[] nextRateInterval = getFeeRateInterval(rate * 2); + String from = "VSIZE" + feeRateInterval[0] + "-" + feeRateInterval[1] + "_COLOR"; + String to = "VSIZE" + nextRateInterval[0] + "-" + (nextRateInterval[1] == Integer.MAX_VALUE ? "2200" : nextRateInterval[1]) + "_COLOR"; + return "linear-gradient(from 75% 0% to 100% 0%, " + from + " 0%, " + to + " 100%, " + from +")"; + } + + private int[] getFeeRateInterval(double medianFee) { + for(int i = 0; i < MEMPOOL_FEE_RATES_INTERVALS.size(); i++) { + int feeRate = MEMPOOL_FEE_RATES_INTERVALS.get(i); + int nextFeeRate = (i == MEMPOOL_FEE_RATES_INTERVALS.size() - 1 ? Integer.MAX_VALUE : MEMPOOL_FEE_RATES_INTERVALS.get(i + 1)); + if(feeRate <= medianFee && nextFeeRate > medianFee) { + return new int[] { feeRate, nextFeeRate }; + } + } + + return new int[] { 1, 2 }; + } + + public int getWeight() { + return weightProperty.get(); + } + + public IntegerProperty weightProperty() { + return weightProperty; + } + + public void setWeight(int weight) { + weightProperty.set(weight); + } + + public double getMedianFee() { + return medianFeeProperty.get(); + } + + public DoubleProperty medianFee() { + return medianFeeProperty; + } + + public void setMedianFee(double medianFee) { + medianFeeProperty.set(medianFee); + } + + public int getHeight() { + return heightProperty.get(); + } + + public IntegerProperty heightProperty() { + return heightProperty; + } + + public void setHeight(int height) { + heightProperty.set(height); + } + + public int getTxCount() { + return txCountProperty.get(); + } + + public IntegerProperty txCountProperty() { + return txCountProperty; + } + + public void setTxCount(int txCount) { + txCountProperty.set(txCount); + } + + public long getTimestamp() { + return timestampProperty.get(); + } + + public LongProperty timestampProperty() { + return timestampProperty; + } + + public void setTimestamp(long timestamp) { + timestampProperty.set(timestamp); + } + + public String getElapsed() { + return elapsedProperty.get(); + } + + public StringProperty elapsedProperty() { + return elapsedProperty; + } + + public void setElapsed(String elapsed) { + elapsedProperty.set(elapsed); + } + + public boolean isConfirmed() { + return confirmedProperty.get(); + } + + public BooleanProperty confirmedProperty() { + return confirmedProperty; + } + + public void setConfirmed(boolean confirmed) { + confirmedProperty.set(confirmed); + } + + public static BlockCube fromBlockSummary(BlockSummary blockSummary) { + return new BlockCube(blockSummary.getWeight().orElse(0), blockSummary.getMedianFee().orElse(0.0d), blockSummary.getHeight(), + blockSummary.getTransactionCount().orElse(0), blockSummary.getTimestamp().getTime(), true); + } +} \ No newline at end of file diff --git a/src/main/java/com/sparrowwallet/sparrow/event/RecentBlocksView.java b/src/main/java/com/sparrowwallet/sparrow/event/RecentBlocksView.java new file mode 100644 index 00000000..c2c26dd4 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/RecentBlocksView.java @@ -0,0 +1,150 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.sparrow.BlockSummary; +import io.reactivex.Observable; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; +import javafx.animation.TranslateTransition; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.layout.Pane; +import javafx.scene.shape.Line; +import javafx.scene.shape.Rectangle; +import javafx.util.Duration; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class RecentBlocksView extends Pane { + private static final double CUBE_SPACING = 100; + private static final double ANIMATION_DURATION_MILLIS = 1000; + private static final double SEPARATOR_X = 74; + + private final CompositeDisposable disposables = new CompositeDisposable(); + + private final ObjectProperty> cubesProperty = new SimpleObjectProperty<>(new ArrayList<>()); + + public RecentBlocksView() { + cubesProperty.addListener((_, _, newValue) -> { + if(newValue != null && newValue.size() == 3) { + drawView(); + } + }); + + Rectangle clip = new Rectangle(-20, -40, CUBE_SPACING * 3 - 20, 100); + setClip(clip); + + Observable intervalObservable = Observable.interval(1, TimeUnit.MINUTES); + disposables.add(intervalObservable.observeOn(JavaFxScheduler.platform()).subscribe(_ -> { + for(BlockCube cube : getCubes()) { + cube.setElapsed(BlockCube.getElapsed(cube.getTimestamp())); + } + })); + } + + public void drawView() { + createSeparator(); + + for(int i = 0; i < 3; i++) { + BlockCube cube = getCubes().get(i); + cube.setTranslateX(i * CUBE_SPACING); + getChildren().add(cube); + } + } + + private void createSeparator() { + Line separator = new Line(SEPARATOR_X, -9, SEPARATOR_X, 80); + separator.getStyleClass().add("blocks-separator"); + separator.getStrokeDashArray().addAll(5.0, 5.0); // Create dotted line pattern + separator.setStrokeWidth(1.0); + getChildren().add(separator); + } + + public void update(List latestBlocks, Double currentFeeRate) { + if(getCubes().isEmpty()) { + List cubes = new ArrayList<>(); + cubes.add(new BlockCube(null, currentFeeRate, null, null, 0L, false)); + cubes.addAll(latestBlocks.stream().map(BlockCube::fromBlockSummary).limit(2).toList()); + setCubes(cubes); + } else { + int knownTip = getCubes().stream().mapToInt(BlockCube::getHeight).max().orElse(0); + int latestTip = latestBlocks.stream().mapToInt(BlockSummary::getHeight).max().orElse(0); + if(latestTip > knownTip) { + addNewBlock(latestBlocks, currentFeeRate); + } else { + for(int i = 1; i < getCubes().size() && i < latestBlocks.size(); i++) { + BlockCube blockCube = getCubes().get(i); + BlockSummary latestBlock = latestBlocks.get(i); + blockCube.setConfirmed(true); + blockCube.setHeight(latestBlock.getHeight()); + blockCube.setTimestamp(latestBlock.getTimestamp().getTime()); + blockCube.setWeight(latestBlock.getWeight().orElse(0)); + blockCube.setMedianFee(latestBlock.getMedianFee().orElse(0.0d)); + blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0)); + } + updateFeeRate(currentFeeRate); + } + } + } + + public void addNewBlock(List latestBlocks, Double currentFeeRate) { + if(getCubes().isEmpty()) { + return; + } + + for(int i = 0; i < getCubes().size() && i < latestBlocks.size(); i++) { + BlockCube blockCube = getCubes().get(i); + BlockSummary latestBlock = latestBlocks.get(i); + blockCube.setConfirmed(true); + blockCube.setHeight(latestBlock.getHeight()); + blockCube.setTimestamp(latestBlock.getTimestamp().getTime()); + blockCube.setWeight(latestBlock.getWeight().orElse(0)); + blockCube.setMedianFee(latestBlock.getMedianFee().orElse(0.0d)); + blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0)); + } + + add(new BlockCube(null, currentFeeRate, null, null, 0L, false)); + } + + public void add(BlockCube newCube) { + newCube.setTranslateX(-CUBE_SPACING); + getChildren().add(newCube); + getCubes().getFirst().setConfirmed(true); + getCubes().addFirst(newCube); + animateCubes(); + if(getCubes().size() > 4) { + BlockCube lastCube = getCubes().getLast(); + getChildren().remove(lastCube); + getCubes().remove(lastCube); + } + } + + public void updateFeeRate(Double currentFeeRate) { + if(!getCubes().isEmpty()) { + BlockCube firstCube = getCubes().getFirst(); + firstCube.setMedianFee(currentFeeRate); + } + } + + private void animateCubes() { + for(int i = 0; i < getCubes().size(); i++) { + BlockCube cube = getCubes().get(i); + TranslateTransition transition = new TranslateTransition(Duration.millis(ANIMATION_DURATION_MILLIS), cube); + transition.setToX(i * CUBE_SPACING); + transition.play(); + } + } + + public List getCubes() { + return cubesProperty.get(); + } + + public ObjectProperty> cubesProperty() { + return cubesProperty; + } + + public void setCubes(List cubes) { + this.cubesProperty.set(cubes); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 8d5dda01..b9b8e871 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -1945,13 +1945,18 @@ public class ElectrumServer { if(startHeight == 0 || totalBlocks > 1 || startHeight > maxHeight + 1) { if(isBlockstorm(totalBlocks)) { - for(int height = maxHeight + 1; height < endHeight; height++) { - blockSummaryMap.put(height, new BlockSummary(height, new Date())); + int start = Math.max(maxHeight + 1, endHeight - 15); + for(int height = start; height <= endHeight; height++) { + blockSummaryMap.put(height, new BlockSummary(height, new Date(), 1.0d, 0, 0)); } } else { blockSummaryMap.putAll(electrumServer.getRecentBlockSummaryMap()); } - } else { + } + + List events = new ArrayList<>(newBlockEvents); + events.removeIf(event -> blockSummaryMap.containsKey(event.getHeight())); + if(!events.isEmpty()) { for(NewBlockEvent event : newBlockEvents) { blockSummaryMap.putAll(electrumServer.getBlockSummaryMap(event.getHeight(), event.getBlockHeader())); } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java index 9e7bbd79..4510f8b5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java @@ -285,7 +285,7 @@ public enum FeeRatesSource { } } - protected record MempoolBlockSummary(String id, Integer height, Long timestamp, Integer tx_count, MempoolBlockSummaryExtras extras) { + 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(); } @@ -294,7 +294,7 @@ public enum FeeRatesSource { if(height == null || timestamp == null) { throw new IllegalStateException("Height = " + height + ", timestamp = " + timestamp + ": both must be specified"); } - return new BlockSummary(height, new Date(timestamp * 1000), getMedianFee(), tx_count); + return new BlockSummary(height, new Date(timestamp * 1000), getMedianFee(), tx_count, weight); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/FeeRatesSelection.java b/src/main/java/com/sparrowwallet/sparrow/wallet/FeeRatesSelection.java index 9e75e9d8..dc14e3f0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/FeeRatesSelection.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/FeeRatesSelection.java @@ -1,7 +1,7 @@ package com.sparrowwallet.sparrow.wallet; public enum FeeRatesSelection { - BLOCK_TARGET("Block Target"), MEMPOOL_SIZE("Mempool Size"); + BLOCK_TARGET("Block Target"), MEMPOOL_SIZE("Mempool Size"), RECENT_BLOCKS("Recent Blocks"); private final String name; diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index bbf8e8f7..c3542619 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -13,10 +13,7 @@ import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.wallet.*; -import com.sparrowwallet.sparrow.UnitFormat; -import com.sparrowwallet.sparrow.AppServices; -import com.sparrowwallet.sparrow.CurrencyRate; -import com.sparrowwallet.sparrow.EventManager; +import com.sparrowwallet.sparrow.*; import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; @@ -28,6 +25,7 @@ import com.sparrowwallet.sparrow.paynym.PayNymService; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.application.Platform; +import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; @@ -78,6 +76,9 @@ public class SendController extends WalletFormController implements Initializabl @FXML private ToggleButton mempoolSizeToggle; + @FXML + private ToggleButton recentBlocksToggle; + @FXML private Field targetBlocksField; @@ -117,6 +118,9 @@ public class SendController extends WalletFormController implements Initializabl @FXML private MempoolSizeFeeRatesChart mempoolSizeFeeRatesChart; + @FXML + private RecentBlocksView recentBlocksView; + @FXML private TransactionDiagram transactionDiagram; @@ -162,6 +166,8 @@ public class SendController extends WalletFormController implements Initializabl private final ObjectProperty replacedTransactionProperty = new SimpleObjectProperty<>(null); + private final ObjectProperty feeRatesSelectionProperty = new SimpleObjectProperty<>(null); + private final List opReturnsList = new ArrayList<>(); private final Set excludedChangeNodes = new HashSet<>(); @@ -299,6 +305,7 @@ public class SendController extends WalletFormController implements Initializabl feeRange.valueProperty().addListener(feeRangeListener); blockTargetFeeRatesChart.managedProperty().bind(blockTargetFeeRatesChart.visibleProperty()); + blockTargetFeeRatesChart.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.BLOCK_TARGET)); blockTargetFeeRatesChart.initialize(); Map targetBlocksFeeRates = getTargetBlocksFeeRates(); if(targetBlocksFeeRates != null) { @@ -308,20 +315,41 @@ public class SendController extends WalletFormController implements Initializabl } mempoolSizeFeeRatesChart.managedProperty().bind(mempoolSizeFeeRatesChart.visibleProperty()); - mempoolSizeFeeRatesChart.visibleProperty().bind(blockTargetFeeRatesChart.visibleProperty().not()); + mempoolSizeFeeRatesChart.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.MEMPOOL_SIZE)); mempoolSizeFeeRatesChart.initialize(); Map> mempoolHistogram = getMempoolHistogram(); if(mempoolHistogram != null) { mempoolSizeFeeRatesChart.update(mempoolHistogram); } + recentBlocksView.managedProperty().bind(recentBlocksView.visibleProperty()); + 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()); + } + + feeRatesSelectionProperty.addListener((_, oldValue, newValue) -> { + boolean isBlockTargetSelection = (newValue == FeeRatesSelection.BLOCK_TARGET); + boolean wasBlockTargetSelection = (oldValue == FeeRatesSelection.BLOCK_TARGET || oldValue == null); + targetBlocksField.setVisible(isBlockTargetSelection); + if(isBlockTargetSelection) { + setTargetBlocks(getTargetBlocks(getFeeRangeRate())); + updateTransaction(); + } else if(wasBlockTargetSelection) { + setFeeRangeRate(getTargetBlocksFeeRates().get(getTargetBlocks())); + updateTransaction(); + } + }); + FeeRatesSelection feeRatesSelection = Config.get().getFeeRatesSelection(); - feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.MEMPOOL_SIZE : feeRatesSelection); + feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.RECENT_BLOCKS : feeRatesSelection); cpfpFeeRate.managedProperty().bind(cpfpFeeRate.visibleProperty()); cpfpFeeRate.setVisible(false); setDefaultFeeRate(); - updateFeeRateSelection(feeRatesSelection); - feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle); + feeRatesSelectionProperty.set(feeRatesSelection); + feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : + (feeRatesSelection == FeeRatesSelection.MEMPOOL_SIZE ? mempoolSizeToggle : recentBlocksToggle)); feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> { if(newValue != null) { FeeRatesSelection newFeeRatesSelection = (FeeRatesSelection)newValue.getUserData(); @@ -723,24 +751,13 @@ public class SendController extends WalletFormController implements Initializabl return List.of(spentTxoFilter, new FrozenTxoFilter(), new CoinbaseTxoFilter(getWalletForm().getWallet())); } - private void updateFeeRateSelection(FeeRatesSelection feeRatesSelection) { - boolean blockTargetSelection = (feeRatesSelection == FeeRatesSelection.BLOCK_TARGET); - targetBlocksField.setVisible(blockTargetSelection); - blockTargetFeeRatesChart.setVisible(blockTargetSelection); - if(blockTargetSelection) { - setTargetBlocks(getTargetBlocks(getFeeRangeRate())); - } else { - setFeeRangeRate(getTargetBlocksFeeRates().get(getTargetBlocks())); - } - updateTransaction(); - } - private void setDefaultFeeRate() { int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1); int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget); Double defaultRate = getTargetBlocksFeeRates().get(defaultTarget); targetBlocks.setValue(index); blockTargetFeeRatesChart.select(defaultTarget); + recentBlocksView.updateFeeRate(defaultRate); setFeeRangeRate(defaultRate); setFeeRate(getFeeRangeRate()); if(Network.get().equals(Network.MAINNET) && defaultRate == getFallbackFeeRate()) { @@ -1411,10 +1428,15 @@ public class SendController extends WalletFormController implements Initializabl @Subscribe public void feeRateSelectionChanged(FeeRatesSelectionChangedEvent event) { if(event.getWallet() == getWalletForm().getWallet()) { - updateFeeRateSelection(event.getFeeRateSelection()); + feeRatesSelectionProperty.set(event.getFeeRateSelection()); } } + @Subscribe + public void blockSummary(BlockSummaryEvent event) { + Platform.runLater(() -> recentBlocksView.update(AppServices.getBlockSummaries().values().stream().sorted().toList(), AppServices.getDefaultFeeRate())); + } + @Subscribe public void spendUtxos(SpendUtxoEvent event) { if((event.getUtxos() == null || !event.getUtxos().isEmpty()) && event.getWallet().equals(getWalletForm().getWallet())) { diff --git a/src/main/resources/com/sparrowwallet/sparrow/darktheme.css b/src/main/resources/com/sparrowwallet/sparrow/darktheme.css index e4926d22..bc8b8fa6 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/darktheme.css +++ b/src/main/resources/com/sparrowwallet/sparrow/darktheme.css @@ -343,4 +343,32 @@ HorizontalHeaderColumn > TableColumnHeader.column-header.table-column{ #grid .spreadsheet-cell.selection { -fx-text-fill: -fx-base; +} + +.root .block-height { + -fx-fill: derive(lightgray, -20%); +} + +.root .blocks-separator { + -fx-stroke: derive(lightgray, -20%); +} + +.root .block-confirmed .block-unused { + -fx-fill: #5a5a65; +} + +.root .block-confirmed .block-top { + -fx-fill: #474c5e; +} + +.root .block-confirmed .block-left { + -fx-fill: #3c4055; +} + +.root .block-unconfirmed .block-top { + -fx-fill: #635b57; +} + +.root .block-unconfirmed .block-left { + -fx-fill: #4e4846; } \ No newline at end of file diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css index c0f4f67e..d05f4ae2 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css @@ -132,6 +132,66 @@ -fx-text-fill: -fx-accent; } +.block-used { + -fx-fill: linear-gradient(from 0% 0% to 0% 100%, -top 0%, -bottom 100%); +} + +.block-mainnet { + -top: rgb(155, 79, 174); + -bottom: rgb(77, 96, 154); +} + +.block-testnet { + -top: rgb(30, 136, 229); + -bottom: rgba(57, 73, 171); +} + +.block-signet { + -top: rgb(136, 14, 79); + -bottom: rgb(64, 7, 39); +} + +.block-regtest { + -top: rgb(0, 137, 123); + -bottom: rgb(0, 96, 100); +} + +.block-confirmed .block-unused { + -fx-fill: #8c8c98; +} + +.block-confirmed .block-top { + -fx-fill: #696d7c; +} + +.block-confirmed .block-left { + -fx-fill: #616475; +} + +.block-unconfirmed .block-top { + -fx-fill: #807976; +} + +.block-unconfirmed .block-left { + -fx-fill: #6f6a69; +} + +.block-unconfirmed .block-unused { + -fx-opacity: 70%; +} + +.block-height { + -fx-fill: derive(-fx-text-background-color, 40%); +} + +.block-text { + -fx-fill: #ffffff; +} + +.blocks-separator { + -fx-stroke: -fx-text-background-color; +} + .vsizeChart { VSIZE1-2_COLOR: rgb(216, 27, 96); VSIZE2-3_COLOR: rgb(142, 36, 170); @@ -164,3 +224,45 @@ VSIZE600-700_COLOR: rgb(51, 105, 30); VSIZE700-800_COLOR: rgb(130, 119, 23); } + +.block-cube { + VSIZE1-2_COLOR: #557d00; + VSIZE2-3_COLOR: #5d7d01; + VSIZE3-4_COLOR: #637d02; + VSIZE4-5_COLOR: #6d7d04; + VSIZE5-6_COLOR: #757d05; + VSIZE6-8_COLOR: #7d7d06; + VSIZE8-10_COLOR: #867d08; + VSIZE10-12_COLOR: #8c7d09; + VSIZE12-15_COLOR: #957d0b; + VSIZE15-20_COLOR: #9b7d0c; + VSIZE20-30_COLOR: #a67d0e; + VSIZE30-40_COLOR: #aa7d0f; + VSIZE40-50_COLOR: #b27d10; + VSIZE50-60_COLOR: #bb7d11; + VSIZE60-70_COLOR: #bf7d12; + VSIZE70-80_COLOR: #bf7815; + VSIZE80-90_COLOR: #bf7319; + VSIZE90-100_COLOR: #be6c1e; + VSIZE100-125_COLOR: #be6820; + VSIZE125-150_COLOR: #bd6125; + VSIZE150-175_COLOR: #bd5c28; + VSIZE175-200_COLOR: #bc552d; + VSIZE200-250_COLOR: #bc4f30; + VSIZE250-300_COLOR: #bc4a34; + VSIZE300-350_COLOR: #bb4339; + VSIZE350-400_COLOR: #bb3d3c; + VSIZE400-500_COLOR: #bb373f; + VSIZE500-600_COLOR: #ba3243; + VSIZE600-700_COLOR: #b92b48; + VSIZE700-800_COLOR: #b9254b; + VSIZE800-900_COLOR: #b8214d; + VSIZE900-1000_COLOR: #b71d4f; + VSIZE1000-1200_COLOR: #b61951; + VSIZE1200-1400_COLOR: #b41453; + VSIZE1400-1600_COLOR: #b30e55; + VSIZE1600-1800_COLOR: #b10857; + VSIZE1800-2000_COLOR: #b00259; + VSIZE2000-2200_COLOR: #ae005b; +} + diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml index 04f01998..cf9bf633 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml @@ -26,6 +26,7 @@ +
@@ -80,6 +81,14 @@ + + + + + + + + @@ -140,6 +149,7 @@ + From b1ab157ee32054a2920653fbb96aa8e9dec40343 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 14 May 2025 10:52:21 +0200 Subject: [PATCH 02/13] cormorant: add block stats rpc call, and prefer for block summaries --- .../sparrow/net/BatchedElectrumServerRpc.java | 19 +++++++++++++- .../sparrowwallet/sparrow/net/BlockStats.java | 14 ++++++++++ .../sparrow/net/ElectrumServer.java | 23 +++++++++++++--- .../sparrow/net/ElectrumServerRpc.java | 2 ++ .../sparrow/net/ScriptHashTx.java | 2 +- .../sparrow/net/ServerCapability.java | 26 +++++++++++++++++-- .../sparrow/net/SimpleElectrumServerRpc.java | 23 ++++++++++++++++ .../sparrow/net/VerboseTransaction.java | 2 +- .../bitcoind/BitcoindClientService.java | 4 +++ .../electrum/ElectrumServerService.java | 12 +++++++++ 10 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/net/BlockStats.java diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java index 588541ed..63e92a3d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java @@ -4,7 +4,6 @@ import com.github.arteam.simplejsonrpc.client.JsonRpcClient; import com.github.arteam.simplejsonrpc.client.Transport; import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException; import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException; -import com.github.arteam.simplejsonrpc.core.domain.ErrorMessage; import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.EventManager; @@ -191,6 +190,24 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc { } } + @Override + @SuppressWarnings("unchecked") + public Map getBlockStats(Transport transport, Set blockHeights) { + PagedBatchRequestBuilder batchRequest = PagedBatchRequestBuilder.create(transport, idCounter).keysType(Integer.class).returnType(BlockStats.class); + + for(Integer height : blockHeights) { + batchRequest.add(height, "blockchain.block.stats", height); + } + + try { + return batchRequest.execute(); + } catch(JsonRpcBatchException e) { + return (Map)e.getSuccesses(); + } catch(Exception e) { + throw new ElectrumServerRpcException("Failed to retrieve block stats for block heights: " + blockHeights, e); + } + } + @Override @SuppressWarnings("unchecked") public Map getTransactions(Transport transport, Wallet wallet, Set txids) { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BlockStats.java b/src/main/java/com/sparrowwallet/sparrow/net/BlockStats.java new file mode 100644 index 00000000..a5d2d02f --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/net/BlockStats.java @@ -0,0 +1,14 @@ +package com.sparrowwallet.sparrow.net; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.sparrowwallet.sparrow.BlockSummary; + +import java.util.Date; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record BlockStats(int height, String blockhash, double[] feerate_percentiles, int total_weight, int txs, long time) { + public BlockSummary toBlockSummary() { + Double medianFee = feerate_percentiles != null && feerate_percentiles.length > 0 ? feerate_percentiles[feerate_percentiles.length / 2] : null; + return new BlockSummary(height, new Date(time * 1000), medianFee, txs, total_weight); + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index b9b8e871..a71b113d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -82,6 +82,8 @@ public class ElectrumServer { private static Server coreElectrumServer; + private static ServerCapability serverCapability; + private static final Pattern RPC_WALLET_LOADING_PATTERN = Pattern.compile(".*\"(Wallet loading failed[:.][^\"]*)\".*"); private static synchronized CloseableTransport getTransport() throws ServerException { @@ -981,6 +983,21 @@ public class ElectrumServer { } public Map getBlockSummaryMap(Integer height, BlockHeader blockHeader) throws ServerException { + if(serverCapability.supportsBlockStats()) { + if(height == null) { + Integer current = AppServices.getCurrentBlockHeight(); + if(current == null) { + return Collections.emptyMap(); + } + Set heights = IntStream.range(current - 1, current + 1).boxed().collect(Collectors.toSet()); + Map blockStats = electrumServerRpc.getBlockStats(getTransport(), heights); + return blockStats.keySet().stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> blockStats.get(v).toBlockSummary())); + } else { + Map blockStats = electrumServerRpc.getBlockStats(getTransport(), Set.of(height)); + return blockStats.keySet().stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> blockStats.get(v).toBlockSummary())); + } + } + FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource(); feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource); @@ -1010,7 +1027,7 @@ public class ElectrumServer { if(current == null) { return Collections.emptyMap(); } - Set references = IntStream.range(current - 4, current + 1) + Set references = IntStream.range(current - 1, current + 1) .mapToObj(i -> new BlockTransaction(null, i, null, null, null)).collect(Collectors.toSet()); Map blockHeaders = getBlockHeaders(null, references); return blockHeaders.keySet().stream() @@ -1219,7 +1236,7 @@ public class ElectrumServer { } if(server.startsWith("cormorant")) { - return new ServerCapability(true); + return new ServerCapability(true, false, true); } if(server.startsWith("electrs/")) { @@ -1405,7 +1422,7 @@ public class ElectrumServer { firstCall = false; //If electrumx is detected, we can upgrade to batched RPC. Electrs/EPS do not support batching. - ServerCapability serverCapability = getServerCapability(serverVersion); + serverCapability = getServerCapability(serverVersion); if(serverCapability.supportsBatching()) { log.debug("Upgrading to batched JSON-RPC"); electrumServerRpc = new BatchedElectrumServerRpc(electrumServerRpc.getIdCounterValue(), serverCapability.getMaxTargetBlocks()); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java index 92e2be52..fb82af61 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java @@ -26,6 +26,8 @@ public interface ElectrumServerRpc { Map getBlockHeaders(Transport transport, Wallet wallet, Set blockHeights); + Map getBlockStats(Transport transport, Set blockHeights); + Map getTransactions(Transport transport, Wallet wallet, Set txids); Map getVerboseTransactions(Transport transport, Set txids, String scriptHash); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ScriptHashTx.java b/src/main/java/com/sparrowwallet/sparrow/net/ScriptHashTx.java index cc1b8d81..ff1a5900 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ScriptHashTx.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ScriptHashTx.java @@ -4,7 +4,7 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.drongo.wallet.BlockTransactionHash; -class ScriptHashTx { +public class ScriptHashTx { public static final ScriptHashTx ERROR_TX = new ScriptHashTx() { @Override public BlockTransactionHash getBlockchainTransactionHash() { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java b/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java index 6bb33c6b..98c7099b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ServerCapability.java @@ -5,15 +5,29 @@ import com.sparrowwallet.sparrow.AppServices; public class ServerCapability { private final boolean supportsBatching; private final int maxTargetBlocks; + private final boolean supportsRecentMempool; + private final boolean supportsBlockStats; public ServerCapability(boolean supportsBatching) { - this.supportsBatching = supportsBatching; - this.maxTargetBlocks = AppServices.TARGET_BLOCKS_RANGE.getLast(); + this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast()); } public ServerCapability(boolean supportsBatching, int maxTargetBlocks) { this.supportsBatching = supportsBatching; this.maxTargetBlocks = maxTargetBlocks; + this.supportsRecentMempool = false; + this.supportsBlockStats = false; + } + + public ServerCapability(boolean supportsBatching, boolean supportsRecentMempool, boolean supportsBlockStats) { + this(supportsBatching, AppServices.TARGET_BLOCKS_RANGE.getLast(), supportsRecentMempool, supportsBlockStats); + } + + public ServerCapability(boolean supportsBatching, int maxTargetBlocks, boolean supportsRecentMempool, boolean supportsBlockStats) { + this.supportsBatching = supportsBatching; + this.maxTargetBlocks = maxTargetBlocks; + this.supportsRecentMempool = supportsRecentMempool; + this.supportsBlockStats = supportsBlockStats; } public boolean supportsBatching() { @@ -23,4 +37,12 @@ public class ServerCapability { public int getMaxTargetBlocks() { return maxTargetBlocks; } + + public boolean supportsRecentMempool() { + return supportsRecentMempool; + } + + public boolean supportsBlockStats() { + return supportsBlockStats; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java index d3947a48..7a16e03b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java @@ -177,6 +177,29 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc { return result; } + @Override + public Map getBlockStats(Transport transport, Set blockHeights) { + JsonRpcClient client = new JsonRpcClient(transport); + + Map result = new LinkedHashMap<>(); + for(Integer blockHeight : blockHeights) { + try { + BlockStats blockStats = new RetryLogic(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() -> + client.createRequest().returnAs(BlockStats.class).method("blockchain.block.stats").id(idCounter.incrementAndGet()).params(blockHeight).execute()); + result.put(blockHeight, blockStats); + } catch(ServerException e) { + //If there is an error with the server connection, don't keep trying - this may take too long given many blocks + throw new ElectrumServerRpcException("Failed to retrieve block stats for block height: " + blockHeight, e); + } catch(JsonRpcException e) { + log.warn("Failed to retrieve block stats for block height: " + blockHeight + (e.getErrorMessage() != null ? " (" + e.getErrorMessage().getMessage() + ")" : "")); + } catch(Exception e) { + log.warn("Failed to retrieve block stats for block height: " + blockHeight + " (" + e.getMessage() + ")"); + } + } + + return result; + } + @Override public Map getTransactions(Transport transport, Wallet wallet, Set txids) { JsonRpcClient client = new JsonRpcClient(transport); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/VerboseTransaction.java b/src/main/java/com/sparrowwallet/sparrow/net/VerboseTransaction.java index 55121033..bedfc12d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/VerboseTransaction.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/VerboseTransaction.java @@ -10,7 +10,7 @@ import com.sparrowwallet.sparrow.AppServices; import java.util.Date; @JsonIgnoreProperties(ignoreUnknown = true) -class VerboseTransaction { +public class VerboseTransaction { public String blockhash; public long blocktime; public int confirmations; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClientService.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClientService.java index 008d3c14..398f15e9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClientService.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClientService.java @@ -7,6 +7,7 @@ import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcOptional; import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam; import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService; import com.sparrowwallet.drongo.protocol.Sha256Hash; +import com.sparrowwallet.sparrow.net.BlockStats; import java.util.List; import java.util.Map; @@ -48,6 +49,9 @@ public interface BitcoindClientService { @JsonRpcMethod("getblockheader") VerboseBlockHeader getBlockHeader(@JsonRpcParam("blockhash") String blockhash); + @JsonRpcMethod("getblockstats") + BlockStats getBlockStats(@JsonRpcParam("blockhash") int hash_or_height); + @JsonRpcMethod("getrawtransaction") Object getRawTransaction(@JsonRpcParam("txid") String txid, @JsonRpcParam("verbose") boolean verbose); 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 d9659dc1..9a3e71cf 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 @@ -10,6 +10,7 @@ import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.SparrowWallet; import com.sparrowwallet.sparrow.event.MempoolEntriesInitializedEvent; import com.sparrowwallet.drongo.Version; +import com.sparrowwallet.sparrow.net.BlockStats; import com.sparrowwallet.sparrow.net.cormorant.Cormorant; import com.sparrowwallet.sparrow.net.cormorant.bitcoind.*; import com.sparrowwallet.sparrow.net.cormorant.index.TxEntry; @@ -157,6 +158,17 @@ public class ElectrumServerService { } } + @JsonRpcMethod("blockchain.block.stats") + public BlockStats getBlockStats(@JsonRpcParam("height") int height) throws BitcoindIOException, BlockNotFoundException { + try { + return bitcoindClient.getBitcoindService().getBlockStats(height); + } catch(JsonRpcException e) { + throw new BlockNotFoundException(e.getErrorMessage()); + } catch(IllegalStateException e) { + throw new BitcoindIOException(e); + } + } + @JsonRpcMethod("blockchain.transaction.get") @SuppressWarnings("unchecked") public Object getTransaction(@JsonRpcParam("tx_hash") String tx_hash, @JsonRpcParam("verbose") @JsonRpcOptional boolean verbose) throws BitcoindIOException, TransactionNotFoundException { From af4c68a09c9cbb45ccd146571c30e5dff7db917b Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 14 May 2025 11:38:17 +0200 Subject: [PATCH 03/13] update tor resource library and switch to resource-filterjar plugin --- build.gradle | 12 +-- buildSrc/build.gradle | 4 - .../filterjar/FilterJarExtension.java | 69 ---------------- .../filterjar/FilterJarParameters.java | 10 --- .../filterjar/FilterJarPlugin.java | 33 -------- .../filterjar/FilterJarTransform.java | 79 ------------------- .../filterjar/JarFilterConfig.java | 19 ----- .../filterjar/JarFilterConfigImpl.java | 64 --------------- 8 files changed, 4 insertions(+), 286 deletions(-) delete mode 100644 buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarExtension.java delete mode 100644 buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarParameters.java delete mode 100644 buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarPlugin.java delete mode 100644 buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarTransform.java delete mode 100644 buildSrc/src/main/java/com/sparrowwallet/filterjar/JarFilterConfig.java delete mode 100644 buildSrc/src/main/java/com/sparrowwallet/filterjar/JarFilterConfigImpl.java diff --git a/build.gradle b/build.gradle index 567c66ae..decfad70 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { id 'org-openjfx-javafxplugin' id 'org.beryx.jlink' version '3.1.1' id 'org.gradlex.extra-java-module-info' version '1.9' - id 'com.sparrowwallet.filterjar' + id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.2' } def sparrowVersion = '2.1.4' @@ -77,7 +77,7 @@ dependencies { implementation('co.nstant.in:cbor:0.9') implementation('org.openpnp:openpnp-capture-java:0.0.28-5') implementation("io.matthewnelson.kmp-tor:runtime:2.2.1") - implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.0") + implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.2") implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') { exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common' } @@ -460,10 +460,6 @@ extraJavaModuleInfo { } } -String torOs = os.macOsX ? "macos" : (os.windows ? "mingw" : "linux-libc") -filterInfo { - filter('io.matthewnelson.kmp-tor', 'resource-lib-tor-gpl-jvm') { - include("io/matthewnelson/kmp/tor/resource/lib/tor/native/${torOs}/${releaseArch}") - exclude('io/matthewnelson/kmp/tor/resource/lib/tor/native/') - } +kmpTorResourceFilterJar { + keepTorCompilation("current","current") } \ No newline at end of file diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index f34a90b9..8cf7a830 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -20,9 +20,5 @@ gradlePlugin { id = "org-openjfx-javafxplugin" implementationClass = "org.openjfx.gradle.JavaFXPlugin" } - register("com.sparrowwallet.filterjar") { - id = "com.sparrowwallet.filterjar" - implementationClass = "com.sparrowwallet.filterjar.FilterJarPlugin" - } } } diff --git a/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarExtension.java b/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarExtension.java deleted file mode 100644 index a8f756ff..00000000 --- a/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarExtension.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.sparrowwallet.filterjar; - -import org.gradle.api.Action; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.ConfigurationContainer; -import org.gradle.api.attributes.Attribute; -import org.gradle.api.model.ObjectFactory; -import org.gradle.api.provider.MapProperty; -import org.gradle.api.tasks.SourceSet; - -import javax.inject.Inject; - -public abstract class FilterJarExtension { - static Attribute FILTERED_ATTRIBUTE = Attribute.of("filtered", Boolean.class); - - public abstract MapProperty getFilterConfigs(); - - @Inject - protected abstract ObjectFactory getObjects(); - - @Inject - protected abstract ConfigurationContainer getConfigurations(); - - public void filter(String group, String artifact, Action action) { - String name = group + ":" + artifact; - JarFilterConfigImpl config = new JarFilterConfigImpl(name, getObjects()); - config.setGroup(group); - config.setArtifact(artifact); - action.execute(config); - getFilterConfigs().put(name, config); - } - - /** - * Activate the plugin's functionality for dependencies of all scopes of the given source set - * (runtimeClasspath, compileClasspath, annotationProcessor). - * Note that the plugin activates the functionality for all source sets by default. - * Therefore, this method only has an effect for source sets for which a {@link #deactivate(Configuration)} - * has been performed. - * - * @param sourceSet the Source Set to activate (e.g. sourceSets.test) - */ - public void activate(SourceSet sourceSet) { - Configuration runtimeClasspath = getConfigurations().getByName(sourceSet.getRuntimeClasspathConfigurationName()); - Configuration compileClasspath = getConfigurations().getByName(sourceSet.getCompileClasspathConfigurationName()); - Configuration annotationProcessor = getConfigurations().getByName(sourceSet.getAnnotationProcessorConfigurationName()); - - activate(runtimeClasspath); - activate(compileClasspath); - activate(annotationProcessor); - } - - /** - * Activate the plugin's functionality for a single resolvable Configuration. - * - * @param resolvable a resolvable Configuration (e.g. configurations["customClasspath"]) - */ - public void activate(Configuration resolvable) { - resolvable.getAttributes().attribute(FILTERED_ATTRIBUTE, true); - } - - /** - * Deactivate the plugin's functionality for a single resolvable Configuration. - * - * @param resolvable a resolvable Configuration (e.g. configurations.annotationProcessor) - */ - public void deactivate(Configuration resolvable) { - resolvable.getAttributes().attribute(FILTERED_ATTRIBUTE, false); - } -} diff --git a/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarParameters.java b/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarParameters.java deleted file mode 100644 index 23565391..00000000 --- a/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarParameters.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.sparrowwallet.filterjar; - -import org.gradle.api.artifacts.transform.TransformParameters; -import org.gradle.api.provider.MapProperty; -import org.gradle.api.tasks.Input; - -public interface FilterJarParameters extends TransformParameters { - @Input - MapProperty getFilterConfigs(); -} diff --git a/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarPlugin.java b/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarPlugin.java deleted file mode 100644 index fd284bc0..00000000 --- a/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarPlugin.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.sparrowwallet.filterjar; - -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.tasks.SourceSetContainer; - -import static com.sparrowwallet.filterjar.FilterJarExtension.FILTERED_ATTRIBUTE; - -public class FilterJarPlugin implements Plugin { - @Override - public void apply(Project project) { - // Register the extension - FilterJarExtension extension = project.getExtensions().create("filterInfo", FilterJarExtension.class); - - project.getPlugins().withType(JavaPlugin.class).configureEach(_ -> { - // By default, activate plugin for all source sets - project.getExtensions().getByType(SourceSetContainer.class).all(extension::activate); - - // All jars have a filtered=false attribute by default - project.getDependencies().getArtifactTypes().maybeCreate("jar").getAttributes().attribute(FILTERED_ATTRIBUTE, false); - - // Register the transform - project.getDependencies().registerTransform(FilterJarTransform.class, transform -> { - transform.getFrom().attribute(FILTERED_ATTRIBUTE, false); - transform.getTo().attribute(FILTERED_ATTRIBUTE, true); - transform.parameters(params -> { - params.getFilterConfigs().putAll(extension.getFilterConfigs()); - }); - }); - }); - } -} diff --git a/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarTransform.java b/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarTransform.java deleted file mode 100644 index b8f9659d..00000000 --- a/buildSrc/src/main/java/com/sparrowwallet/filterjar/FilterJarTransform.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.sparrowwallet.filterjar; - - -import org.gradle.api.artifacts.transform.InputArtifact; -import org.gradle.api.artifacts.transform.TransformAction; -import org.gradle.api.artifacts.transform.TransformOutputs; -import org.gradle.api.file.FileSystemLocation; -import org.gradle.api.provider.Provider; -import org.gradle.api.tasks.PathSensitive; -import org.gradle.api.tasks.PathSensitivity; - -import java.io.File; -import java.util.Map; -import java.util.Set; -import java.util.HashSet; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; -import java.util.jar.JarOutputStream; -import java.nio.file.Files; - -public abstract class FilterJarTransform implements TransformAction { - @InputArtifact - @PathSensitive(PathSensitivity.NAME_ONLY) - public abstract Provider getInputArtifact(); - - @Override - public void transform(TransformOutputs outputs) { - File originalJar = getInputArtifact().get().getAsFile(); - String jarName = originalJar.getName(); - - // Get filter configurations from parameters - Map filterConfigs = getParameters().getFilterConfigs().get(); - - //Inclusions are prioritised ahead of exclusions - Set inclusions = new HashSet<>(); - Set exclusions = new HashSet<>(); - - // Check if this JAR matches any configured filters (simplified matching based on artifact name) - filterConfigs.forEach((key, config) -> { - if(jarName.contains(config.getArtifact())) { - inclusions.addAll(config.getInclusions()); - exclusions.addAll(config.getExclusions()); - } - }); - - try { - if(!exclusions.isEmpty()) { - filterJar(originalJar, getFilterJar(outputs, originalJar), inclusions, exclusions); - } else { - outputs.file(originalJar); - } - } catch(Exception e) { - throw new RuntimeException("Failed to transform jar: " + jarName, e); - } - } - - private void filterJar(File inputFile, File outputFile, Set inclusions, Set exclusions) throws Exception { - try(JarFile jarFile = new JarFile(inputFile); JarOutputStream jarOut = new JarOutputStream(Files.newOutputStream(outputFile.toPath()))) { - jarFile.entries().asIterator().forEachRemaining(entry -> { - String entryName = entry.getName(); - boolean shouldInclude = inclusions.stream().anyMatch(entryName::startsWith); - boolean shouldExclude = exclusions.stream().anyMatch(entryName::startsWith); - if(shouldInclude || !shouldExclude) { - try { - jarOut.putNextEntry(new JarEntry(entryName)); - jarFile.getInputStream(entry).transferTo(jarOut); - jarOut.closeEntry(); - } catch(Exception e) { - throw new RuntimeException("Error processing entry: " + entryName, e); - } - } - }); - } - } - - private File getFilterJar(TransformOutputs outputs, File originalJar) { - return outputs.file(originalJar.getName().substring(0, originalJar.getName().lastIndexOf('.')) + "-filtered.jar"); - } -} diff --git a/buildSrc/src/main/java/com/sparrowwallet/filterjar/JarFilterConfig.java b/buildSrc/src/main/java/com/sparrowwallet/filterjar/JarFilterConfig.java deleted file mode 100644 index 5bd792c7..00000000 --- a/buildSrc/src/main/java/com/sparrowwallet/filterjar/JarFilterConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.sparrowwallet.filterjar; - -import org.gradle.api.tasks.Input; - -import java.util.List; - -public interface JarFilterConfig { - @Input - String getGroup(); - - @Input - String getArtifact(); - - @Input - List getInclusions(); - - @Input - List getExclusions(); -} diff --git a/buildSrc/src/main/java/com/sparrowwallet/filterjar/JarFilterConfigImpl.java b/buildSrc/src/main/java/com/sparrowwallet/filterjar/JarFilterConfigImpl.java deleted file mode 100644 index 912ccd4d..00000000 --- a/buildSrc/src/main/java/com/sparrowwallet/filterjar/JarFilterConfigImpl.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.sparrowwallet.filterjar; - -import org.gradle.api.Named; -import org.gradle.api.model.ObjectFactory; -import javax.inject.Inject; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; - -public class JarFilterConfigImpl implements Named, JarFilterConfig, Serializable { - private final String name; - private String group; - private String artifact; - private final List inclusions; - private final List exclusions; - - @Inject - public JarFilterConfigImpl(String name, ObjectFactory objectFactory) { - this.name = name; - this.inclusions = new ArrayList<>(); - this.exclusions = new ArrayList<>(); - } - - @Override - public String getName() { - return name; - } - - @Override - public String getGroup() { - return group; - } - - public void setGroup(String group) { - this.group = group; - } - - @Override - public String getArtifact() { - return artifact; - } - - public void setArtifact(String artifact) { - this.artifact = artifact; - } - - @Override - public List getInclusions() { - return inclusions; - } - - public void include(String path) { - inclusions.add(path); - } - - @Override - public List getExclusions() { - return exclusions; - } - - public void exclude(String path) { - exclusions.add(path); - } -} From d0da85171cadb1f9bdb96bb2138fe6f1ea2a7f97 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 15 May 2025 09:28:42 +0200 Subject: [PATCH 04/13] rename sparrow package to sparrowwallet and sparrowserver on linux --- .github/workflows/package.yaml | 5 +- build.gradle | 47 ++++++++-- .../package/linux-headless/aarch64/control | 9 -- .../deploy/package/linux-headless/control | 12 +++ .../package/linux-headless/sparrow.spec | 85 +++++++++++++++++++ .../deploy/package/linux-headless/x64/control | 9 -- src/main/deploy/package/linux/Sparrow.desktop | 4 +- src/main/deploy/package/linux/control | 12 +++ src/main/deploy/package/linux/postinst | 8 +- src/main/deploy/package/linux/sparrow.spec | 25 +++--- .../sparrowwallet/sparrow/AppController.java | 6 +- 11 files changed, 170 insertions(+), 52 deletions(-) delete mode 100644 src/main/deploy/package/linux-headless/aarch64/control create mode 100644 src/main/deploy/package/linux-headless/control create mode 100755 src/main/deploy/package/linux-headless/sparrow.spec delete mode 100644 src/main/deploy/package/linux-headless/x64/control create mode 100644 src/main/deploy/package/linux/control diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index 30927fc7..67b1c3a0 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -30,7 +30,7 @@ jobs: - name: Package tar distribution if: ${{ runner.os == 'Linux' }} run: ./gradlew packageTarDistribution - - name: Upload Artifacts + - name: Upload Artifact uses: actions/upload-artifact@v4 with: name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }} @@ -43,9 +43,6 @@ jobs: - name: Package headless tar distribution if: ${{ runner.os == 'Linux' }} run: ./gradlew -Djava.awt.headless=true packageTarDistribution - - name: Rename Headless Artifacts - if: ${{ runner.os == 'Linux' }} - run: for f in build/jpackage/sparrow*; do mv -v "$f" "${f/sparrow/sparrow-server}"; done; - name: Upload Headless Artifact if: ${{ runner.os == 'Linux' }} uses: actions/upload-artifact@v4 diff --git a/build.gradle b/build.gradle index decfad70..55ef59c8 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,6 @@ plugins { id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.2' } -def sparrowVersion = '2.1.4' def os = org.gradle.internal.os.OperatingSystem.current() def osName = os.getFamilyName() if(os.macOsX) { @@ -20,8 +19,8 @@ if(System.getProperty("os.arch") == "aarch64") { } def headless = "true".equals(System.getProperty("java.awt.headless")) -group "com.sparrowwallet" -version "${sparrowVersion}" +group 'com.sparrowwallet' +version '2.1.4' repositories { mavenCentral() @@ -239,7 +238,7 @@ jlink { jpackage { imageName = "Sparrow" installerName = "Sparrow" - appVersion = "${sparrowVersion}" + appVersion = "${version}" skipInstaller = os.macOsX || properties.skipInstallers imageOptions = [] installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE'] @@ -250,11 +249,13 @@ jlink { } if(os.linux) { if(headless) { - installerOptions = ['--license-file', 'LICENSE', '--resource-dir', "src/main/deploy/package/linux-headless/${osArch}"] + installerName = "sparrowserver" + installerOptions = ['--license-file', 'LICENSE'] } else { - installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow'] + installerName = "sparrowwallet" + installerOptions += ['--linux-shortcut', '--linux-menu-group', 'Sparrow'] } - installerOptions += ['--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com'] + installerOptions += ['--resource-dir', layout.buildDirectory.dir('deploy/package').get().asFile.toString(), '--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com'] imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/'] } if(os.macOsX) { @@ -272,6 +273,7 @@ jlink { if(os.linux) { tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules') + tasks.jpackageImage.finalizedBy('prepareResourceDir') } else { tasks.jlink.finalizedBy('addUserWritePermission') } @@ -290,12 +292,39 @@ tasks.register('copyUdevRules', Copy) { include('*') } +tasks.register('prepareResourceDir', Copy) { + from("src/main/deploy/package/linux${headless ? '-headless' : ''}") + into(layout.buildDirectory.dir('deploy/package')) + include('*') + exclude('*.png') + filter { String line -> + if(line.contains('${size')) { + line = line.replace('${size}', getDirectorySize(layout.buildDirectory.dir('jpackage/Sparrow').get().asFile)) + } + return line.replace('${version}', "${version}").replace('${arch}', osArch == 'aarch64' ? 'arm64' : 'amd64') + } +} + +static def getDirectorySize(File directory) { + long size = 0 + if(directory.isFile()) { + size = directory.length() + } else if(directory.isDirectory()) { + directory.eachFileRecurse { file -> + if(file.isFile()) { + size += file.length() + } + } + } + return Long.toString(size/1024 as long) +} + tasks.register('removeGroupWritePermission', Exec) { commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow" } tasks.register('packageZipDistribution', Zip) { - archiveFileName = "Sparrow-${sparrowVersion}.zip" + archiveFileName = "Sparrow-${version}.zip" destinationDirectory = file("$buildDir/jpackage") preserveFileTimestamps = os.macOsX from("$buildDir/jpackage/") { @@ -306,7 +335,7 @@ tasks.register('packageZipDistribution', Zip) { tasks.register('packageTarDistribution', Tar) { dependsOn removeGroupWritePermission - archiveFileName = "sparrow-${sparrowVersion}-${releaseArch}.tar.gz" + archiveFileName = "sparrow${headless ? 'server': 'wallet'}-${version}-${releaseArch}.tar.gz" destinationDirectory = file("$buildDir/jpackage") compression = Compression.GZIP from("$buildDir/jpackage/") { diff --git a/src/main/deploy/package/linux-headless/aarch64/control b/src/main/deploy/package/linux-headless/aarch64/control deleted file mode 100644 index e85fd2da..00000000 --- a/src/main/deploy/package/linux-headless/aarch64/control +++ /dev/null @@ -1,9 +0,0 @@ -Package: sparrow -Version: 2.1.4-1 -Section: utils -Maintainer: Craig Raw -Priority: optional -Architecture: arm64 -Provides: sparrow -Description: Sparrow -Depends: libc6, zlib1g diff --git a/src/main/deploy/package/linux-headless/control b/src/main/deploy/package/linux-headless/control new file mode 100644 index 00000000..636b57e9 --- /dev/null +++ b/src/main/deploy/package/linux-headless/control @@ -0,0 +1,12 @@ +Package: sparrowserver +Version: ${version}-1 +Section: utils +Maintainer: Craig Raw +Priority: optional +Architecture: ${arch} +Conflicts: sparrow (<= 2.1.4) +Replaces: sparrow (<= 2.1.4) +Provides: sparrowserver +Description: Sparrow Server +Depends: libc6, zlib1g +Installed-Size: ${size} diff --git a/src/main/deploy/package/linux-headless/sparrow.spec b/src/main/deploy/package/linux-headless/sparrow.spec new file mode 100755 index 00000000..ba937616 --- /dev/null +++ b/src/main/deploy/package/linux-headless/sparrow.spec @@ -0,0 +1,85 @@ +Summary: Sparrow Server +Name: sparrowserver +Version: ${version} +Release: 1 +License: ASL 2.0 +Vendor: Unknown + +%if "x" != "x" +URL: https://sparrowwallet.com +%endif + +%if "x/opt" != "x" +Prefix: /opt +%endif + +Provides: sparrowserver +Obsoletes: sparrow <= 2.1.4 + +%if "xutils" != "x" +Group: utils +%endif + +Autoprov: 0 +Autoreq: 0 + +#comment line below to enable effective jar compression +#it could easily get your package size from 40 to 15Mb but +#build time will substantially increase and it may require unpack200/system java to install +%define __jar_repack %{nil} + +# on RHEL we got unwanted improved debugging enhancements +%define _build_id_links none + +%define package_filelist %{_builddir}/%{name}.files +%define app_filelist %{_builddir}/%{name}.app.files +%define filesystem_filelist %{_builddir}/%{name}.filesystem.files + +%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib + +%description +Sparrow Server + +%global __os_install_post %{nil} + +%prep + +%build + +%install +rm -rf %{buildroot} +install -d -m 755 %{buildroot}/opt/sparrowserver +cp -r %{_sourcedir}/opt/sparrowserver/* %{buildroot}/opt/sparrowserver +if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then + install -d -m 755 %{buildroot}/lib/systemd/system + cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system +fi +%if "x%{_rpmdir}/../../LICENSE" != "x" + %define license_install_file %{_defaultlicensedir}/%{name}-%{version}/%{basename:%{_rpmdir}/../../LICENSE} + install -d -m 755 "%{buildroot}%{dirname:%{license_install_file}}" + install -m 644 "%{_rpmdir}/../../LICENSE" "%{buildroot}%{license_install_file}" +%endif +(cd %{buildroot} && find . -path ./lib/systemd -prune -o -type d -print) | sed -e 's/^\.//' -e '/^$/d' | sort > %{app_filelist} +{ rpm -ql filesystem || echo %{default_filesystem}; } | sort > %{filesystem_filelist} +comm -23 %{app_filelist} %{filesystem_filelist} > %{package_filelist} +sed -i -e 's/.*/%dir "&"/' %{package_filelist} +(cd %{buildroot} && find . -not -type d) | sed -e 's/^\.//' -e 's/.*/"&"/' >> %{package_filelist} +%if "x%{_rpmdir}/../../LICENSE" != "x" + sed -i -e 's|"%{license_install_file}"||' -e '/^$/d' %{package_filelist} +%endif + +%files -f %{package_filelist} +%if "x%{_rpmdir}/../../LICENSE" != "x" + %license "%{license_install_file}" +%endif + +%post +package_type=rpm + +%pre +package_type=rpm + +%preun +package_type=rpm + +%clean diff --git a/src/main/deploy/package/linux-headless/x64/control b/src/main/deploy/package/linux-headless/x64/control deleted file mode 100644 index b45b95ca..00000000 --- a/src/main/deploy/package/linux-headless/x64/control +++ /dev/null @@ -1,9 +0,0 @@ -Package: sparrow -Version: 2.1.4-1 -Section: utils -Maintainer: Craig Raw -Priority: optional -Architecture: amd64 -Provides: sparrow -Description: Sparrow -Depends: libc6, zlib1g diff --git a/src/main/deploy/package/linux/Sparrow.desktop b/src/main/deploy/package/linux/Sparrow.desktop index 16b336fe..2072f939 100644 --- a/src/main/deploy/package/linux/Sparrow.desktop +++ b/src/main/deploy/package/linux/Sparrow.desktop @@ -1,8 +1,8 @@ [Desktop Entry] Name=Sparrow Comment=Sparrow -Exec=/opt/sparrow/bin/Sparrow %U -Icon=/opt/sparrow/lib/Sparrow.png +Exec=/opt/sparrowwallet/bin/Sparrow %U +Icon=/opt/sparrowwallet/lib/Sparrow.png Terminal=false Type=Application Categories=Finance;Network; diff --git a/src/main/deploy/package/linux/control b/src/main/deploy/package/linux/control new file mode 100644 index 00000000..2233ddaa --- /dev/null +++ b/src/main/deploy/package/linux/control @@ -0,0 +1,12 @@ +Package: sparrowwallet +Version: ${version}-1 +Section: utils +Maintainer: Craig Raw +Priority: optional +Architecture: ${arch} +Provides: sparrowwallet +Conflicts: sparrow (<= 2.1.4) +Replaces: sparrow (<= 2.1.4) +Description: Sparrow Wallet +Depends: libasound2, libbsd0, libc6, libmd0, libx11-6, libxau6, libxcb1, libxdmcp6, libxext6, libxi6, libxrender1, libxtst6, xdg-utils +Installed-Size: ${size} diff --git a/src/main/deploy/package/linux/postinst b/src/main/deploy/package/linux/postinst index 3dd9e0a4..6f6f10b3 100755 --- a/src/main/deploy/package/linux/postinst +++ b/src/main/deploy/package/linux/postinst @@ -1,5 +1,5 @@ #!/bin/sh -# postinst script for sparrow +# postinst script for sparrowwallet # # see: dh_installdeb(1) @@ -22,9 +22,9 @@ package_type=deb case "$1" in configure) - xdg-desktop-menu install /opt/sparrow/lib/sparrow-Sparrow.desktop - xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml - install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d + xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop + xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml + install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d if ! getent group plugdev > /dev/null; then groupadd plugdev fi diff --git a/src/main/deploy/package/linux/sparrow.spec b/src/main/deploy/package/linux/sparrow.spec index a8aa65e6..e49e9620 100755 --- a/src/main/deploy/package/linux/sparrow.spec +++ b/src/main/deploy/package/linux/sparrow.spec @@ -1,19 +1,20 @@ Summary: Sparrow -Name: sparrow -Version: 2.1.4 +Name: sparrowwallet +Version: ${version} Release: 1 License: ASL 2.0 Vendor: Unknown %if "x" != "x" -URL: +URL: https://sparrowwallet.com %endif %if "x/opt" != "x" Prefix: /opt %endif -Provides: sparrow +Provides: sparrowwallet +Obsoletes: sparrow <= 2.1.4 %if "xutils" != "x" Group: utils @@ -50,8 +51,8 @@ Sparrow Wallet %install rm -rf %{buildroot} -install -d -m 755 %{buildroot}/opt/sparrow -cp -r %{_sourcedir}/opt/sparrow/* %{buildroot}/opt/sparrow +install -d -m 755 %{buildroot}/opt/sparrowwallet +cp -r %{_sourcedir}/opt/sparrowwallet/* %{buildroot}/opt/sparrowwallet if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then install -d -m 755 %{buildroot}/lib/systemd/system cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system @@ -77,9 +78,9 @@ sed -i -e 's/.*/%dir "&"/' %{package_filelist} %post package_type=rpm -xdg-desktop-menu install /opt/sparrow/lib/sparrow-Sparrow.desktop -xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml -install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d +xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop +xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml +install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d if ! getent group plugdev > /dev/null; then groupadd plugdev fi @@ -251,9 +252,9 @@ desktop_trace () echo "$@" } -do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow.desktop xdg-desktop-menu uninstall /opt/sparrow/lib/sparrow-Sparrow.desktop -do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml xdg-mime uninstall /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml -do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow.desktop desktop_uninstall_default_mime_handler sparrow-Sparrow.desktop application/psbt application/bitcoin-transaction application/pgp-signature x-scheme-handler/bitcoin x-scheme-handler/auth47 x-scheme-handler/lightning +do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop xdg-desktop-menu uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop +do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml xdg-mime uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml +do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop desktop_uninstall_default_mime_handler sparrowwallet-Sparrow.desktop application/psbt application/bitcoin-transaction application/pgp-signature x-scheme-handler/bitcoin x-scheme-handler/auth47 x-scheme-handler/lightning %clean diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index baf5c498..5bfaf9fb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -572,16 +572,16 @@ public class AppController implements Initializable { public void installUdevRules(ActionEvent event) { String commands = """ - sudo install -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d + sudo install -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d sudo udevadm control --reload sudo udevadm trigger sudo groupadd -f plugdev sudo usermod -aG plugdev `whoami` """; String home = System.getProperty(JPACKAGE_APP_PATH); - if(home != null && !home.startsWith("/opt/sparrow") && home.endsWith("bin/Sparrow")) { + if(home != null && !home.startsWith("/opt/sparrowwallet") && home.endsWith("bin/Sparrow")) { home = home.replace("bin/Sparrow", ""); - commands = commands.replace("/opt/sparrow/", home); + commands = commands.replace("/opt/sparrowwallet/", home); } TextAreaDialog dialog = new TextAreaDialog(commands, false); From 055e3ac4966ace7253c193025614db227db8cd65 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 15 May 2025 10:00:03 +0200 Subject: [PATCH 05/13] followup --- build.gradle | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 55ef59c8..4566c653 100644 --- a/build.gradle +++ b/build.gradle @@ -296,12 +296,15 @@ tasks.register('prepareResourceDir', Copy) { from("src/main/deploy/package/linux${headless ? '-headless' : ''}") into(layout.buildDirectory.dir('deploy/package')) include('*') - exclude('*.png') - filter { String line -> - if(line.contains('${size')) { - line = line.replace('${size}', getDirectorySize(layout.buildDirectory.dir('jpackage/Sparrow').get().asFile)) + eachFile { file -> + if(file.name.equals('control') || file.name.endsWith('.spec')) { + filter { line -> + if(line.contains('${size}')) { + line = line.replace('${size}', getDirectorySize(layout.buildDirectory.dir('jpackage/Sparrow').get().asFile)) + } + return line.replace('${version}', "${version}").replace('${arch}', osArch == 'aarch64' ? 'arm64' : 'amd64') + } } - return line.replace('${version}', "${version}").replace('${arch}', osArch == 'aarch64' ? 'arm64' : 'amd64') } } From 1a4f0113c79a349d64a31a28759d5b9025703bf9 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 15 May 2025 10:49:09 +0200 Subject: [PATCH 06/13] followup 2 --- .../package/linux-headless/{sparrow.spec => sparrowserver.spec} | 0 .../deploy/package/linux/{sparrow.spec => sparrowwallet.spec} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/main/deploy/package/linux-headless/{sparrow.spec => sparrowserver.spec} (100%) rename src/main/deploy/package/linux/{sparrow.spec => sparrowwallet.spec} (100%) diff --git a/src/main/deploy/package/linux-headless/sparrow.spec b/src/main/deploy/package/linux-headless/sparrowserver.spec similarity index 100% rename from src/main/deploy/package/linux-headless/sparrow.spec rename to src/main/deploy/package/linux-headless/sparrowserver.spec diff --git a/src/main/deploy/package/linux/sparrow.spec b/src/main/deploy/package/linux/sparrowwallet.spec similarity index 100% rename from src/main/deploy/package/linux/sparrow.spec rename to src/main/deploy/package/linux/sparrowwallet.spec From b4d34aacc5b9e0e47a43b978f67cdc35df614994 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 15 May 2025 12:03:24 +0200 Subject: [PATCH 07/13] tweak block cube median fee font styling --- .../sparrowwallet/sparrow/event/BlockCube.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java b/src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java index 151202a0..b2a63339 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java @@ -13,6 +13,7 @@ import javafx.scene.shape.Rectangle; import javafx.scene.text.Font; import javafx.scene.text.FontWeight; import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; import javafx.util.Duration; import java.time.Instant; @@ -38,6 +39,8 @@ public class BlockCube extends Group { private final Text heightText = new Text(); private final Text medianFeeText = new Text(); + private final Text unitsText = new Text(" s/vb"); + private final TextFlow medianFeeTextFlow = new TextFlow(); private final Text txCountText = new Text(); private final Text elapsedText = new Text(); @@ -51,8 +54,8 @@ public class BlockCube extends Group { } }); this.medianFeeProperty.addListener((_, _, newValue) -> { - medianFeeText.setText("~" + Math.round(Math.max(newValue.doubleValue(), 1.0d)) + " s/vb"); - medianFeeText.setX((CUBE_SIZE - medianFeeText.getLayoutBounds().getWidth()) / 2); + medianFeeText.setText("~" + Math.round(Math.max(newValue.doubleValue(), 1.0d))); + medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeText.getLayoutBounds().getWidth() + unitsText.getLayoutBounds().getWidth())) / 2); }); this.txCountProperty.addListener((_, _, newValue) -> { txCountText.setText(newValue.intValue() == 0 ? "" : newValue + " txes"); @@ -128,8 +131,11 @@ public class BlockCube extends Group { medianFeeText.getStyleClass().add("block-text"); medianFeeText.setFont(Font.font(null, FontWeight.BOLD, 11)); - medianFeeText.setX((CUBE_SIZE - medianFeeText.getLayoutBounds().getWidth()) / 2); - medianFeeText.setY(16); + unitsText.getStyleClass().add("block-text"); + unitsText.setFont(new Font(10)); + medianFeeTextFlow.getChildren().addAll(medianFeeText, unitsText); + medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeText.getLayoutBounds().getWidth() + unitsText.getLayoutBounds().getWidth())) / 2); + medianFeeTextFlow.setTranslateY(7); txCountText.getStyleClass().add("block-text"); txCountText.setFont(new Font(10)); @@ -142,7 +148,7 @@ public class BlockCube extends Group { elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2); elapsedText.setY(50); - getChildren().addAll(frontFaceGroup, top, left, heightText, medianFeeText, txCountText, elapsedText); + getChildren().addAll(frontFaceGroup, top, left, heightText, medianFeeTextFlow, txCountText, elapsedText); } private void updateFill() { From 1605cd26190c2054127a03daf10531a2e382e815 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 15 May 2025 12:10:54 +0200 Subject: [PATCH 08/13] followup --- src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java b/src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java index b2a63339..b583e402 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java @@ -39,7 +39,7 @@ public class BlockCube extends Group { private final Text heightText = new Text(); private final Text medianFeeText = new Text(); - private final Text unitsText = new Text(" s/vb"); + private final Text unitsText = new Text(); private final TextFlow medianFeeTextFlow = new TextFlow(); private final Text txCountText = new Text(); private final Text elapsedText = new Text(); @@ -55,6 +55,7 @@ public class BlockCube extends Group { }); this.medianFeeProperty.addListener((_, _, newValue) -> { medianFeeText.setText("~" + Math.round(Math.max(newValue.doubleValue(), 1.0d))); + unitsText.setText(" s/vb"); medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeText.getLayoutBounds().getWidth() + unitsText.getLayoutBounds().getWidth())) / 2); }); this.txCountProperty.addListener((_, _, newValue) -> { From d4a1441d650933541de22ae23da476033c8124c2 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 15 May 2025 12:42:53 +0200 Subject: [PATCH 09/13] more recent blocks tweaks --- .../sparrow/{event => control}/BlockCube.java | 9 +++++---- .../sparrow/{event => control}/RecentBlocksView.java | 2 +- .../sparrow/net/BatchedElectrumServerRpc.java | 4 ++-- .../resources/com/sparrowwallet/sparrow/wallet/send.fxml | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) rename src/main/java/com/sparrowwallet/sparrow/{event => control}/BlockCube.java (97%) rename src/main/java/com/sparrowwallet/sparrow/{event => control}/RecentBlocksView.java (99%) diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java b/src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java similarity index 97% rename from src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java rename to src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java index b583e402..2cc3a2aa 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java @@ -1,4 +1,4 @@ -package com.sparrowwallet.sparrow.event; +package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.protocol.Transaction; @@ -26,7 +26,7 @@ public class BlockCube extends Group { public static final double CUBE_SIZE = 60; private final IntegerProperty weightProperty = new SimpleIntegerProperty(0); - private final DoubleProperty medianFeeProperty = new SimpleDoubleProperty(0); + private final DoubleProperty medianFeeProperty = new SimpleDoubleProperty(-1.0d); private final IntegerProperty heightProperty = new SimpleIntegerProperty(0); private final IntegerProperty txCountProperty = new SimpleIntegerProperty(0); private final LongProperty timestampProperty = new SimpleLongProperty(System.currentTimeMillis()); @@ -56,7 +56,8 @@ public class BlockCube extends Group { this.medianFeeProperty.addListener((_, _, newValue) -> { medianFeeText.setText("~" + Math.round(Math.max(newValue.doubleValue(), 1.0d))); unitsText.setText(" s/vb"); - medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeText.getLayoutBounds().getWidth() + unitsText.getLayoutBounds().getWidth())) / 2); + double unitsWidth = TextUtils.computeTextWidth(unitsText.getFont(), unitsText.getText(), 0.0d); + medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeText.getLayoutBounds().getWidth() + unitsWidth)) / 2); }); this.txCountProperty.addListener((_, _, newValue) -> { txCountText.setText(newValue.intValue() == 0 ? "" : newValue + " txes"); @@ -324,7 +325,7 @@ public class BlockCube extends Group { } public static BlockCube fromBlockSummary(BlockSummary blockSummary) { - return new BlockCube(blockSummary.getWeight().orElse(0), blockSummary.getMedianFee().orElse(0.0d), blockSummary.getHeight(), + return new BlockCube(blockSummary.getWeight().orElse(0), blockSummary.getMedianFee().orElse(1.0d), blockSummary.getHeight(), blockSummary.getTransactionCount().orElse(0), blockSummary.getTimestamp().getTime(), true); } } \ No newline at end of file diff --git a/src/main/java/com/sparrowwallet/sparrow/event/RecentBlocksView.java b/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java similarity index 99% rename from src/main/java/com/sparrowwallet/sparrow/event/RecentBlocksView.java rename to src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java index c2c26dd4..250025f0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/RecentBlocksView.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java @@ -1,4 +1,4 @@ -package com.sparrowwallet.sparrow.event; +package com.sparrowwallet.sparrow.control; import com.sparrowwallet.sparrow.BlockSummary; import io.reactivex.Observable; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java index 63e92a3d..8f98911a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java @@ -161,12 +161,12 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc { try { return batchRequest.execute(); } catch(JsonRpcBatchException e) { - log.warn("Failed to unsubscribe from script hashes: " + e.getErrors().keySet(), e); + log.info("Failed to unsubscribe from script hashes: " + e.getErrors().keySet(), e); Map unsubscribedScriptHashes = scriptHashes.stream().collect(Collectors.toMap(s -> s, _ -> true)); unsubscribedScriptHashes.keySet().removeIf(scriptHash -> e.getErrors().containsKey(scriptHash)); return unsubscribedScriptHashes; } catch(Exception e) { - log.warn("Failed to unsubscribe from script hashes: " + scriptHashes, e); + log.info("Failed to unsubscribe from script hashes: " + scriptHashes, e); return Collections.emptyMap(); } } diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml index cf9bf633..df592be1 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml @@ -26,7 +26,7 @@ - +
From 892885c0b1428a1fd9e2e28ffc33b60a1485f9ac Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 15 May 2025 15:01:09 +0200 Subject: [PATCH 10/13] make wallet summary table grow horizontally with dialog sizing --- .../com/sparrowwallet/sparrow/control/WalletSummaryDialog.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletSummaryDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletSummaryDialog.java index 78e83566..e40c09cc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletSummaryDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletSummaryDialog.java @@ -17,6 +17,7 @@ import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import java.util.ArrayList; @@ -103,6 +104,7 @@ public class WalletSummaryDialog extends Dialog { vBox.getChildren().add(table); hBox.getChildren().add(vBox); + HBox.setHgrow(vBox, Priority.ALWAYS); Wallet balanceWallet; if(allOpenWallets) { From af4a283b3f574d7e3bb4b9faaa139277eb06dc5e Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 16 May 2025 10:02:03 +0200 Subject: [PATCH 11/13] increase trezor device libusb timeout --- lark | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lark b/lark index d3ed65b8..5facb25e 160000 --- a/lark +++ b/lark @@ -1 +1 @@ -Subproject commit d3ed65b89e0b6273eac4e35b266986308a5e83a9 +Subproject commit 5facb25ede49c30650a8460dc04982650edb397f From c078aea3b4214418cbddc7450b7f93827233c497 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 16 May 2025 17:00:40 +0200 Subject: [PATCH 12/13] show total in transaction diagram when constructing multiple payment transactions --- .../sparrow/control/TransactionDiagram.java | 40 +++++++++++++++++-- .../sparrow/wallet/SendController.java | 8 +++- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index e04bb789..409f57d0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java @@ -8,15 +8,13 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.TransactionOutput; import com.sparrowwallet.drongo.uri.BitcoinURI; import com.sparrowwallet.drongo.wallet.*; -import com.sparrowwallet.sparrow.UnitFormat; -import com.sparrowwallet.sparrow.AppServices; -import com.sparrowwallet.sparrow.EventManager; -import com.sparrowwallet.sparrow.Theme; +import com.sparrowwallet.sparrow.*; import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent; import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.GlyphUtils; import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.net.ExchangeSource; import com.sparrowwallet.sparrow.wallet.OptimizationStrategy; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; @@ -229,6 +227,12 @@ public class TransactionDiagram extends GridPane { getChildren().clear(); getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane); + if(!isFinal() && walletTx.getPayments().size() > 1) { + Pane totalsPane = getTotalsPane(); + GridPane.setConstraints(totalsPane, 2, 0, 3, 1); + getChildren().add(totalsPane); + } + if(contextMenu == null) { contextMenu = new ContextMenu(); MenuItem menuItem = new MenuItem("Save as Image..."); @@ -839,6 +843,34 @@ public class TransactionDiagram extends GridPane { return txPane; } + private Pane getTotalsPane() { + VBox totalsBox = new VBox(); + totalsBox.setPadding(new Insets(0, 0, 15, 0)); + totalsBox.setAlignment(Pos.CENTER); + + long amount = walletTx.getPayments().stream().mapToLong(Payment::getAmount).sum(); + long count = walletTx.getPayments().size(); + + HBox coinLabelBox = new HBox(); + coinLabelBox.setAlignment(Pos.CENTER); + CoinLabel totalCoinLabel = new CoinLabel(); + totalCoinLabel.setValue(amount); + coinLabelBox.getChildren().addAll(totalCoinLabel, new Label(" in "), new Label(Long.toString(count)), new Label(" payments")); + totalsBox.getChildren().addAll(createSpacer(), coinLabelBox); + + CurrencyRate currencyRate = AppServices.getFiatCurrencyExchangeRate(); + if(currencyRate != null && currencyRate.isAvailable() && Config.get().getExchangeSource() != ExchangeSource.NONE) { + HBox fiatLabelBox = new HBox(); + fiatLabelBox.setAlignment(Pos.CENTER); + FiatLabel fiatLabel = new FiatLabel(); + fiatLabel.set(currencyRate, amount); + fiatLabelBox.getChildren().add(fiatLabel); + totalsBox.getChildren().add(fiatLabelBox); + } + + return totalsBox; + } + private void saveAsImage() { Stage window = new Stage(); FileChooser fileChooser = new FileChooser(); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index c3542619..2f2cd848 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -981,7 +981,7 @@ public class SendController extends WalletFormController implements Initializabl } private void setFiatFeeAmount(CurrencyRate currencyRate, Long amount) { - if(amount != null && currencyRate != null && currencyRate.isAvailable()) { + if(amount != null && currencyRate != null && currencyRate.isAvailable() && Config.get().getExchangeSource() != ExchangeSource.NONE) { fiatFeeAmount.set(currencyRate, amount); } } @@ -1519,12 +1519,18 @@ public class SendController extends WalletFormController implements Initializabl if(event.getExchangeSource() == ExchangeSource.NONE) { fiatFeeAmount.setCurrency(null); fiatFeeAmount.setBtcRate(0.0); + if(paymentTabs.getTabs().size() > 1) { + updateTransaction(); + } } } @Subscribe public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) { setFiatFeeAmount(event.getCurrencyRate(), getFeeValueSats()); + if(paymentTabs.getTabs().size() > 1) { + updateTransaction(); + } } @Subscribe From 4ab9a9f681ef3184b35d3a98b0ebf859277c3bb7 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 16 May 2025 18:49:58 +0200 Subject: [PATCH 13/13] followup tweaks --- .../sparrow/control/TransactionDiagram.java | 16 ++++++++++------ .../sparrow/wallet/SendController.java | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index 409f57d0..b7510224 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java @@ -227,8 +227,9 @@ public class TransactionDiagram extends GridPane { getChildren().clear(); getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane); - if(!isFinal() && walletTx.getPayments().size() > 1) { - Pane totalsPane = getTotalsPane(); + List defaultPayments = getDefaultPayments(); + if(!isFinal() && defaultPayments.size() > 1) { + Pane totalsPane = getTotalsPane(defaultPayments); GridPane.setConstraints(totalsPane, 2, 0, 3, 1); getChildren().add(totalsPane); } @@ -620,6 +621,10 @@ public class TransactionDiagram extends GridPane { } } + private List getDefaultPayments() { + return walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).toList(); + } + private Pane getOutputsLines(List displayedPayments) { VBox pane = new VBox(); Group group = new Group(); @@ -843,19 +848,18 @@ public class TransactionDiagram extends GridPane { return txPane; } - private Pane getTotalsPane() { + private Pane getTotalsPane(List defaultPayments) { VBox totalsBox = new VBox(); totalsBox.setPadding(new Insets(0, 0, 15, 0)); totalsBox.setAlignment(Pos.CENTER); - long amount = walletTx.getPayments().stream().mapToLong(Payment::getAmount).sum(); - long count = walletTx.getPayments().size(); + long amount = defaultPayments.stream().mapToLong(Payment::getAmount).sum(); HBox coinLabelBox = new HBox(); coinLabelBox.setAlignment(Pos.CENTER); CoinLabel totalCoinLabel = new CoinLabel(); totalCoinLabel.setValue(amount); - coinLabelBox.getChildren().addAll(totalCoinLabel, new Label(" in "), new Label(Long.toString(count)), new Label(" payments")); + coinLabelBox.getChildren().addAll(totalCoinLabel, new Label(" in "), new Label(Long.toString(defaultPayments.size())), new Label(" payments")); totalsBox.getChildren().addAll(createSpacer(), coinLabelBox); CurrencyRate currencyRate = AppServices.getFiatCurrencyExchangeRate(); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 2f2cd848..916290d7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -1625,7 +1625,7 @@ public class SendController extends WalletFormController implements Initializabl } else if(payjoinPresent) { addLabel("Cannot fake coinjoin due to payjoin", getInfoGlyph()); } else { - if(utxoSelectorProperty().get() != null) { + if(utxoSelectorProperty().get() != null && !(utxoSelectorProperty().get() instanceof MaxUtxoSelector)) { addLabel("Cannot fake coinjoin due to coin control", getInfoGlyph()); } else { addLabel("Cannot fake coinjoin due to insufficient funds", getInfoGlyph());