diff --git a/.github/workflows/package.yaml b/.github/workflows/package.yaml index 67b1c3a0..88833611 100644 --- a/.github/workflows/package.yaml +++ b/.github/workflows/package.yaml @@ -30,6 +30,9 @@ jobs: - name: Package tar distribution if: ${{ runner.os == 'Linux' }} run: ./gradlew packageTarDistribution + - name: Repackage deb distribution + if: ${{ runner.os == 'Linux' }} + run: ./repackage.sh - name: Upload Artifact uses: actions/upload-artifact@v4 with: @@ -43,6 +46,9 @@ jobs: - name: Package headless tar distribution if: ${{ runner.os == 'Linux' }} run: ./gradlew -Djava.awt.headless=true packageTarDistribution + - name: Repackage headless deb distribution + if: ${{ runner.os == 'Linux' }} + run: ./repackage.sh - name: Upload Headless Artifact if: ${{ runner.os == 'Linux' }} uses: actions/upload-artifact@v4 diff --git a/build.gradle b/build.gradle index 4566c653..9e482db0 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 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.2' + id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.16.3' } def os = org.gradle.internal.os.OperatingSystem.current() @@ -20,7 +20,7 @@ if(System.getProperty("os.arch") == "aarch64") { def headless = "true".equals(System.getProperty("java.awt.headless")) group 'com.sparrowwallet' -version '2.1.4' +version '2.2.2' repositories { mavenCentral() @@ -76,7 +76,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.2") + implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.16.3") implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') { exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common' } diff --git a/docs/reproducible.md b/docs/reproducible.md index 7d1fe345..5df236eb 100644 --- a/docs/reproducible.md +++ b/docs/reproducible.md @@ -83,7 +83,7 @@ sudo apt install -y rpm fakeroot binutils First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify: ```shell -GIT_TAG="2.1.3" +GIT_TAG="2.2.1" ``` The project can then be initially cloned as follows: diff --git a/repackage.sh b/repackage.sh new file mode 100755 index 00000000..6f88cbac --- /dev/null +++ b/repackage.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -e # Exit on any error + +# Define paths +BUILD_DIR="build" +JPACKAGE_DIR="$BUILD_DIR/jpackage" +TEMP_DIR="$BUILD_DIR/repackage" + +# Find the .deb file in build/jpackage (assuming there is only one) +DEB_FILE=$(find "$JPACKAGE_DIR" -type f -name "*.deb" -print -quit) + +# Check if a .deb file was found +if [ -z "$DEB_FILE" ]; then + echo "Error: No .deb file found in $JPACKAGE_DIR" + exit 1 +fi + +# Extract the filename from the path for later use +DEB_FILENAME=$(basename "$DEB_FILE") + +echo "Found .deb file: $DEB_FILENAME" + +# Create a temp directory inside build to avoid file conflicts +mkdir -p "$TEMP_DIR" +cd "$TEMP_DIR" + +# Extract the .deb file contents +ar x "../../$DEB_FILE" + +# Decompress zst files to tar +unzstd control.tar.zst +unzstd data.tar.zst + +# Compress tar files to xz +xz -c control.tar > control.tar.xz +xz -c data.tar > data.tar.xz + +# Remove the original .deb file +rm "../../$DEB_FILE" + +# Create the new .deb file with xz compression in the original location +ar cr "../../$DEB_FILE" debian-binary control.tar.xz data.tar.xz + +# Clean up temp files +cd ../.. +rm -rf "$TEMP_DIR" + +echo "Repackaging complete: $DEB_FILENAME" diff --git a/src/main/deploy/package/osx/Info.plist b/src/main/deploy/package/osx/Info.plist index a23ec9e6..dfb179de 100644 --- a/src/main/deploy/package/osx/Info.plist +++ b/src/main/deploy/package/osx/Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.1.4 + 2.2.2 CFBundleSignature ???? diff --git a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java index 547804c7..776ddc53 100644 --- a/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java +++ b/src/main/java/com/sparrowwallet/sparrow/SparrowWallet.java @@ -18,7 +18,7 @@ import java.util.*; public class SparrowWallet { public static final String APP_ID = "sparrow"; public static final String APP_NAME = "Sparrow"; - public static final String APP_VERSION = "2.1.4"; + public static final String APP_VERSION = "2.2.2"; public static final String APP_VERSION_SUFFIX = ""; public static final String APP_HOME_PROPERTY = "sparrow.home"; public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK"; diff --git a/src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java b/src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java index 2cc3a2aa..d02e449c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java @@ -3,6 +3,8 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.Network; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.sparrow.BlockSummary; +import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.net.FeeRatesSource; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; @@ -15,6 +17,7 @@ import javafx.scene.text.FontWeight; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import javafx.util.Duration; +import org.girod.javafx.svgimage.SVGImage; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -26,12 +29,13 @@ 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(-1.0d); + private final DoubleProperty medianFeeProperty = new SimpleDoubleProperty(-Double.MAX_VALUE); 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 final ObjectProperty feeRatesSource = new SimpleObjectProperty<>(null); private Polygon front; private Rectangle unusedArea; @@ -43,10 +47,12 @@ public class BlockCube extends Group { private final TextFlow medianFeeTextFlow = new TextFlow(); private final Text txCountText = new Text(); private final Text elapsedText = new Text(); + private final Group feeRateIcon = new Group(); public BlockCube(Integer weight, Double medianFee, Integer height, Integer txCount, Long timestamp, boolean confirmed) { getStyleClass().addAll("block-" + Network.getCanonical().getName(), "block-cube"); this.confirmedProperty.set(confirmed); + this.feeRatesSource.set(Config.get().getFeeRatesSource()); this.weightProperty.addListener((_, _, _) -> { if(front != null) { @@ -54,10 +60,11 @@ public class BlockCube extends Group { } }); this.medianFeeProperty.addListener((_, _, newValue) -> { - medianFeeText.setText("~" + Math.round(Math.max(newValue.doubleValue(), 1.0d))); - unitsText.setText(" s/vb"); + medianFeeText.setText(newValue.doubleValue() < 0.0d ? "" : "~" + Math.round(Math.max(newValue.doubleValue(), 1.0d))); + unitsText.setText(newValue.doubleValue() < 0.0d ? "" : " s/vb"); + double medianFeeWidth = TextUtils.computeTextWidth(medianFeeText.getFont(), medianFeeText.getText(), 0.0d); double unitsWidth = TextUtils.computeTextWidth(unitsText.getFont(), unitsText.getText(), 0.0d); - medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeText.getLayoutBounds().getWidth() + unitsWidth)) / 2); + medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeWidth + unitsWidth)) / 2); }); this.txCountProperty.addListener((_, _, newValue) -> { txCountText.setText(newValue.intValue() == 0 ? "" : newValue + " txes"); @@ -79,6 +86,11 @@ public class BlockCube extends Group { updateFill(); } }); + this.feeRatesSource.addListener((_, _, _) -> { + if(front != null) { + updateFill(); + } + }); this.medianFeeText.textProperty().addListener((_, _, _) -> { pulse(); }); @@ -145,12 +157,15 @@ public class BlockCube extends Group { txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2); txCountText.setY(34); + feeRateIcon.setTranslateX(((CUBE_SIZE * 0.7) - 14) / 2); + feeRateIcon.setTranslateY(-36); + 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, medianFeeTextFlow, txCountText, elapsedText); + getChildren().addAll(frontFaceGroup, top, left, heightText, medianFeeTextFlow, txCountText, feeRateIcon, elapsedText); } private void updateFill() { @@ -167,6 +182,7 @@ public class BlockCube extends Group { usedArea.setHeight(CUBE_SIZE - startYAbsolute); usedArea.setVisible(true); heightText.setVisible(true); + feeRateIcon.getChildren().clear(); } else { getStyleClass().removeAll("block-confirmed"); if(!getStyleClass().contains("block-unconfirmed")) { @@ -175,6 +191,14 @@ public class BlockCube extends Group { usedArea.setVisible(false); unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";"); heightText.setVisible(false); + if(feeRatesSource.get() != null) { + SVGImage svgImage = feeRatesSource.get().getSVGImage(); + if(svgImage != null) { + feeRateIcon.getChildren().setAll(feeRatesSource.get().getSVGImage()); + } else { + feeRateIcon.getChildren().clear(); + } + } } } @@ -324,8 +348,20 @@ public class BlockCube extends Group { confirmedProperty.set(confirmed); } + public FeeRatesSource getFeeRatesSource() { + return feeRatesSource.get(); + } + + public ObjectProperty feeRatesSourceProperty() { + return feeRatesSource; + } + + public void setFeeRatesSource(FeeRatesSource feeRatesSource) { + this.feeRatesSource.set(feeRatesSource); + } + public static BlockCube fromBlockSummary(BlockSummary blockSummary) { - return new BlockCube(blockSummary.getWeight().orElse(0), blockSummary.getMedianFee().orElse(1.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/control/RecentBlocksView.java b/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java index 250025f0..db47bd84 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/RecentBlocksView.java @@ -1,12 +1,15 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.sparrow.BlockSummary; +import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.net.FeeRatesSource; 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.control.Tooltip; import javafx.scene.layout.Pane; import javafx.scene.shape.Line; import javafx.scene.shape.Rectangle; @@ -14,8 +17,12 @@ import javafx.util.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; +import static com.sparrowwallet.sparrow.AppServices.TARGET_BLOCKS_RANGE; +import static com.sparrowwallet.sparrow.control.BlockCube.CUBE_SIZE; + public class RecentBlocksView extends Pane { private static final double CUBE_SPACING = 100; private static final double ANIMATION_DURATION_MILLIS = 1000; @@ -24,6 +31,7 @@ public class RecentBlocksView extends Pane { private final CompositeDisposable disposables = new CompositeDisposable(); private final ObjectProperty> cubesProperty = new SimpleObjectProperty<>(new ArrayList<>()); + private final Tooltip tooltip = new Tooltip(); public RecentBlocksView() { cubesProperty.addListener((_, _, newValue) -> { @@ -41,6 +49,16 @@ public class RecentBlocksView extends Pane { cube.setElapsed(BlockCube.getElapsed(cube.getTimestamp())); } })); + + updateFeeRatesSource(Config.get().getFeeRatesSource()); + Tooltip.install(this, tooltip); + } + + public void updateFeeRatesSource(FeeRatesSource feeRatesSource) { + tooltip.setText("Fee rate estimate from " + feeRatesSource.getDescription()); + if(getCubes() != null && !getCubes().isEmpty()) { + getCubes().getFirst().setFeeRatesSource(feeRatesSource); + } } public void drawView() { @@ -54,7 +72,7 @@ public class RecentBlocksView extends Pane { } private void createSeparator() { - Line separator = new Line(SEPARATOR_X, -9, SEPARATOR_X, 80); + Line separator = new Line(SEPARATOR_X, -9, SEPARATOR_X, CUBE_SIZE); separator.getStyleClass().add("blocks-separator"); separator.getStrokeDashArray().addAll(5.0, 5.0); // Create dotted line pattern separator.setStrokeWidth(1.0); @@ -73,14 +91,14 @@ public class RecentBlocksView extends Pane { if(latestTip > knownTip) { addNewBlock(latestBlocks, currentFeeRate); } else { - for(int i = 1; i < getCubes().size() && i < latestBlocks.size(); i++) { + for(int i = 1; i < getCubes().size() && i <= latestBlocks.size(); i++) { BlockCube blockCube = getCubes().get(i); - BlockSummary latestBlock = latestBlocks.get(i); + BlockSummary latestBlock = latestBlocks.get(i - 1); 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.setMedianFee(latestBlock.getMedianFee().orElse(-1.0d)); blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0)); } updateFeeRate(currentFeeRate); @@ -100,7 +118,7 @@ public class RecentBlocksView extends Pane { blockCube.setHeight(latestBlock.getHeight()); blockCube.setTimestamp(latestBlock.getTimestamp().getTime()); blockCube.setWeight(latestBlock.getWeight().orElse(0)); - blockCube.setMedianFee(latestBlock.getMedianFee().orElse(0.0d)); + blockCube.setMedianFee(latestBlock.getMedianFee().orElse(-1.0d)); blockCube.setTxCount(latestBlock.getTransactionCount().orElse(0)); } @@ -120,6 +138,12 @@ public class RecentBlocksView extends Pane { } } + public void updateFeeRate(Map targetBlockFeeRates) { + int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1); + Double defaultRate = targetBlockFeeRates.get(defaultTarget); + updateFeeRate(defaultRate); + } + public void updateFeeRate(Double currentFeeRate) { if(!getCubes().isEmpty()) { BlockCube firstCube = getCubes().getFirst(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index b7510224..623fa920 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java @@ -227,9 +227,9 @@ public class TransactionDiagram extends GridPane { getChildren().clear(); getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane); - List defaultPayments = getDefaultPayments(); - if(!isFinal() && defaultPayments.size() > 1) { - Pane totalsPane = getTotalsPane(defaultPayments); + List userPayments = getUserPayments(); + if(!isFinal() && userPayments.size() > 1) { + Pane totalsPane = getTotalsPane(userPayments); GridPane.setConstraints(totalsPane, 2, 0, 3, 1); getChildren().add(totalsPane); } @@ -621,8 +621,8 @@ public class TransactionDiagram extends GridPane { } } - private List getDefaultPayments() { - return walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).toList(); + private List getUserPayments() { + return walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT || payment.getType() == Payment.Type.ANCHOR).toList(); } private Pane getOutputsLines(List displayedPayments) { @@ -848,18 +848,18 @@ public class TransactionDiagram extends GridPane { return txPane; } - private Pane getTotalsPane(List defaultPayments) { + private Pane getTotalsPane(List userPayments) { VBox totalsBox = new VBox(); totalsBox.setPadding(new Insets(0, 0, 15, 0)); totalsBox.setAlignment(Pos.CENTER); - long amount = defaultPayments.stream().mapToLong(Payment::getAmount).sum(); + long amount = userPayments.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(defaultPayments.size())), new Label(" payments")); + coinLabelBox.getChildren().addAll(totalCoinLabel, new Label(" in "), new Label(Long.toString(userPayments.size())), new Label(" payments")); totalsBox.getChildren().addAll(createSpacer(), coinLabelBox); CurrencyRate currencyRate = AppServices.getFiatCurrencyExchangeRate(); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BlockExplorer.java b/src/main/java/com/sparrowwallet/sparrow/net/BlockExplorer.java index bfc37850..ed68907a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/BlockExplorer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/BlockExplorer.java @@ -1,12 +1,22 @@ package com.sparrowwallet.sparrow.net; +import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.io.Server; +import org.girod.javafx.svgimage.SVGImage; +import org.girod.javafx.svgimage.SVGLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URL; +import java.util.Locale; public enum BlockExplorer { MEMPOOL_SPACE("https://mempool.space"), BLOCKSTREAM_INFO("https://blockstream.info"), NONE("http://none"); + private static final Logger log = LoggerFactory.getLogger(BlockExplorer.class); + private final Server server; BlockExplorer(String url) { @@ -16,4 +26,17 @@ public enum BlockExplorer { public Server getServer() { return server; } + + public static SVGImage getSVGImage(Server server) { + try { + URL url = AppServices.class.getResource("/image/blockexplorer/" + server.getHost().toLowerCase(Locale.ROOT) + "-icon.svg"); + if(url != null) { + return SVGLoader.load(url); + } + } catch(Exception e) { + log.error("Could not load block explorer image for " + server.getHost()); + } + + return null; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index a71b113d..83441f97 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -261,7 +261,7 @@ public class ElectrumServer { return 0; }); - return txos.stream().map(txo -> new ScriptHashTx(txo.getHeight(), txo.getHashAsString(), txo.getFee())).toList(); + return txos.stream().map(txo -> new ScriptHashTx(txo.getHeight(), txo.getHashAsString(), txo.getFee() == null ? 0 : txo.getFee())).toList(); } private static String getScriptHashStatus(List scriptHashTxes) { @@ -439,7 +439,7 @@ public class ElectrumServer { blkTx.getTransaction().getInputs().stream().map(txInput -> getPrevOutput(wallet, txInput)) .filter(Objects::nonNull).map(ElectrumServer::getScriptHash).anyMatch(scriptHash::equals)) { List scriptHashTxes = new ArrayList<>(getScriptHashes(scriptHash, node)); - scriptHashTxes.add(new ScriptHashTx(0, txid.toString(), blkTx.getFee())); + scriptHashTxes.add(new ScriptHashTx(0, txid.toString(), blkTx.getFee() == null ? 0 : blkTx.getFee())); String status = getScriptHashStatus(scriptHashTxes); if(Objects.equals(status, statuses.getLast())) { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java b/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java index 29bc2561..2a55b48d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ExchangeSource.java @@ -9,9 +9,12 @@ import javafx.concurrent.ScheduledService; import javafx.concurrent.Service; import javafx.concurrent.Task; import org.apache.commons.lang3.time.DateUtils; +import org.girod.javafx.svgimage.SVGImage; +import org.girod.javafx.svgimage.SVGLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.URL; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.time.Instant; @@ -297,6 +300,19 @@ public enum ExchangeSource { return name; } + public SVGImage getSVGImage() { + try { + URL url = AppServices.class.getResource("/image/exchangesource/" + name.toLowerCase(Locale.ROOT) + "-icon.svg"); + if(url != null) { + return SVGLoader.load(url); + } + } catch(Exception e) { + log.error("Could not load exchange source image for " + name); + } + + return null; + } + public static class CurrenciesService extends Service> { private final ExchangeSource exchangeSource; diff --git a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java index 4510f8b5..5b99f782 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/FeeRatesSource.java @@ -7,9 +7,12 @@ import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.drongo.wallet.BlockTransactionHash; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.BlockSummary; +import org.girod.javafx.svgimage.SVGImage; +import org.girod.javafx.svgimage.SVGLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.URL; import java.util.*; public enum FeeRatesSource { @@ -275,6 +278,27 @@ public enum FeeRatesSource { return name; } + public String getDescription() { + return switch(this) { + case ELECTRUM_SERVER -> "server"; + case MINIMUM -> "settings"; + default -> getName().toLowerCase(Locale.ROOT); + }; + } + + public SVGImage getSVGImage() { + try { + URL url = AppServices.class.getResource("/image/feeratesource/" + getDescription() + "-icon.svg"); + if(url != null) { + return SVGLoader.load(url); + } + } catch(Exception e) { + log.error("Could not load fee rates source image for " + name); + } + + return null; + } + protected record ThreeTierRates(Double fastestFee, Double halfHourFee, Double hourFee, Double minimumFee) {} private record OxtRates(OxtRatesData[] data) {} diff --git a/src/main/java/com/sparrowwallet/sparrow/settings/GeneralSettingsController.java b/src/main/java/com/sparrowwallet/sparrow/settings/GeneralSettingsController.java index 9a650501..e0de8135 100644 --- a/src/main/java/com/sparrowwallet/sparrow/settings/GeneralSettingsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/settings/GeneralSettingsController.java @@ -87,6 +87,8 @@ public class GeneralSettingsController extends SettingsDetailController { config.setFeeRatesSource(feeRatesSource.getValue()); } + feeRatesSource.setCellFactory(_ -> new FeeRatesSourceListCell()); + feeRatesSource.setButtonCell(feeRatesSource.getCellFactory().call(null)); feeRatesSource.valueProperty().addListener((observable, oldValue, newValue) -> { config.setFeeRatesSource(newValue); EventManager.get().post(new FeeRatesSourceChangedEvent(newValue)); @@ -96,25 +98,8 @@ public class GeneralSettingsController extends SettingsDetailController { currenciesLoadWarning.setVisible(false); blockExplorers.setItems(getBlockExplorerList()); - blockExplorers.setConverter(new StringConverter<>() { - @Override - public String toString(Server server) { - if(server == null || server == BlockExplorer.NONE.getServer()) { - return "None"; - } - - if(server == CUSTOM_BLOCK_EXPLORER) { - return "Custom..."; - } - - return server.getHost(); - } - - @Override - public Server fromString(String string) { - return null; - } - }); + blockExplorers.setCellFactory(_ -> new BlockExplorerListCell()); + blockExplorers.setButtonCell(blockExplorers.getCellFactory().call(null)); blockExplorers.valueProperty().addListener((observable, oldValue, newValue) -> { if(newValue != null) { if(newValue == CUSTOM_BLOCK_EXPLORER) { @@ -258,14 +243,50 @@ public class GeneralSettingsController extends SettingsDetailController { fiatCurrency.valueProperty().addListener(fiatCurrencyListener); } + private static class FeeRatesSourceListCell extends ListCell { + @Override + protected void updateItem(FeeRatesSource item, boolean empty) { + super.updateItem(item, empty); + if(empty || item == null) { + setText(null); + setGraphic(null); + } else { + setText(item.toString()); + setGraphic(item.getSVGImage()); + setGraphicTextGap(8.0d); + } + } + } + + private static class BlockExplorerListCell extends ListCell { + @Override + protected void updateItem(Server server, boolean empty) { + super.updateItem(server, empty); + if(empty || server == null || server == BlockExplorer.NONE.getServer()) { + setText("None"); + setGraphic(null); + } else if(server == CUSTOM_BLOCK_EXPLORER) { + setText("Custom..."); + setGraphic(null); + } else { + setText(server.getHost()); + setGraphic(BlockExplorer.getSVGImage(server)); + setGraphicTextGap(8.0d); + } + } + } + private static class ExchangeSourceButtonCell extends ListCell { @Override protected void updateItem(ExchangeSource exchangeSource, boolean empty) { super.updateItem(exchangeSource, empty); if(exchangeSource == null || empty) { setText(""); + setGraphic(null); } else { setText(exchangeSource.getName()); + setGraphic(exchangeSource.getSVGImage()); + setGraphicTextGap(8.0d); } } } @@ -276,12 +297,15 @@ public class GeneralSettingsController extends SettingsDetailController { super.updateItem(exchangeSource, empty); if(exchangeSource == null || empty) { setText(""); + setGraphic(null); } else { String text = exchangeSource.getName(); if(exchangeSource.getDescription() != null && !exchangeSource.getDescription().isEmpty()) { text += " (" + exchangeSource.getDescription() + ")"; } setText(text); + setGraphic(exchangeSource.getSVGImage()); + setGraphicTextGap(8.0d); } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 916290d7..720c6b04 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -1411,6 +1411,7 @@ public class SendController extends WalletFormController implements Initializabl setFeeRatePriority(getFeeRangeRate()); } feeRange.updateTrackHighlight(); + recentBlocksView.updateFeeRate(event.getTargetBlockFeeRates()); if(updateDefaultFeeRate) { if(getFeeRate() != null && Long.valueOf((long)getFallbackFeeRate()).equals(getFeeRate().longValue())) { @@ -1600,6 +1601,11 @@ public class SendController extends WalletFormController implements Initializabl } } + @Subscribe + public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) { + recentBlocksView.updateFeeRatesSource(event.getFeeRateSource()); + } + private class PrivacyAnalysisTooltip extends VBox { private final List