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