mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2025-11-05 11:56:37 +00:00
add recent blocks view
This commit is contained in:
parent
e697313259
commit
94b27ba7e8
11 changed files with 696 additions and 40 deletions
|
|
@ -305,12 +305,6 @@ public class AppServices {
|
||||||
if(event != null) {
|
if(event != null) {
|
||||||
EventManager.get().post(event);
|
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 -> {
|
connectionService.setOnFailed(failEvent -> {
|
||||||
//Close connection here to create a new transport next time we try
|
//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<NewBlockEvent> newBlockEvents) {
|
private void fetchBlockSummaries(List<NewBlockEvent> newBlockEvents) {
|
||||||
if(isConnected()) {
|
if(isConnected()) {
|
||||||
ElectrumServer.BlockSummaryService blockSummaryService = new ElectrumServer.BlockSummaryService(newBlockEvents);
|
ElectrumServer.BlockSummaryService blockSummaryService = new ElectrumServer.BlockSummaryService(newBlockEvents);
|
||||||
|
|
@ -1216,6 +1217,12 @@ public class AppServices {
|
||||||
latestBlockHeader = event.getBlockHeader();
|
latestBlockHeader = event.getBlockHeader();
|
||||||
Config.get().addRecentServer();
|
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)) {
|
if(!blockSummaries.containsKey(currentBlockHeight)) {
|
||||||
fetchBlockSummaries(Collections.emptyList());
|
fetchBlockSummaries(Collections.emptyList());
|
||||||
}
|
}
|
||||||
|
|
@ -1259,10 +1266,8 @@ public class AppServices {
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
||||||
//Perform once-off fee rates retrieval to immediately change displayed rates
|
//Perform once-off fee rates retrieval to immediately change displayed rates
|
||||||
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
|
fetchFeeRates();
|
||||||
feeRatesService = createFeeRatesService();
|
fetchBlockSummaries(Collections.emptyList());
|
||||||
feeRatesService.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,23 @@ import java.time.temporal.ChronoUnit;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public class BlockSummary {
|
public class BlockSummary implements Comparable<BlockSummary> {
|
||||||
private final Integer height;
|
private final Integer height;
|
||||||
private final Date timestamp;
|
private final Date timestamp;
|
||||||
private final Double medianFee;
|
private final Double medianFee;
|
||||||
private final Integer transactionCount;
|
private final Integer transactionCount;
|
||||||
|
private final Integer weight;
|
||||||
|
|
||||||
public BlockSummary(Integer height, Date timestamp) {
|
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.height = height;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.medianFee = medianFee;
|
this.medianFee = medianFee;
|
||||||
this.transactionCount = transactionCount;
|
this.transactionCount = transactionCount;
|
||||||
|
this.weight = weight;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getHeight() {
|
public Integer getHeight() {
|
||||||
|
|
@ -38,6 +40,10 @@ public class BlockSummary {
|
||||||
return transactionCount == null ? Optional.empty() : Optional.of(transactionCount);
|
return transactionCount == null ? Optional.empty() : Optional.of(transactionCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<Integer> getWeight() {
|
||||||
|
return weight == null ? Optional.empty() : Optional.of(weight);
|
||||||
|
}
|
||||||
|
|
||||||
private static long calculateElapsedSeconds(long timestampUtc) {
|
private static long calculateElapsedSeconds(long timestampUtc) {
|
||||||
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
||||||
Instant nowInstant = Instant.now();
|
Instant nowInstant = Instant.now();
|
||||||
|
|
@ -62,4 +68,9 @@ public class BlockSummary {
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return getElapsed() + ":" + getMedianFee();
|
return getElapsed() + ":" + getMedianFee();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(BlockSummary o) {
|
||||||
|
return o.height - height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
323
src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java
Normal file
323
src/main/java/com/sparrowwallet/sparrow/event/BlockCube.java
Normal file
|
|
@ -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<Integer> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<List<BlockCube>> 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<Long> 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<BlockSummary> latestBlocks, Double currentFeeRate) {
|
||||||
|
if(getCubes().isEmpty()) {
|
||||||
|
List<BlockCube> 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<BlockSummary> 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<BlockCube> getCubes() {
|
||||||
|
return cubesProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<List<BlockCube>> cubesProperty() {
|
||||||
|
return cubesProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCubes(List<BlockCube> cubes) {
|
||||||
|
this.cubesProperty.set(cubes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1945,13 +1945,18 @@ public class ElectrumServer {
|
||||||
|
|
||||||
if(startHeight == 0 || totalBlocks > 1 || startHeight > maxHeight + 1) {
|
if(startHeight == 0 || totalBlocks > 1 || startHeight > maxHeight + 1) {
|
||||||
if(isBlockstorm(totalBlocks)) {
|
if(isBlockstorm(totalBlocks)) {
|
||||||
for(int height = maxHeight + 1; height < endHeight; height++) {
|
int start = Math.max(maxHeight + 1, endHeight - 15);
|
||||||
blockSummaryMap.put(height, new BlockSummary(height, new Date()));
|
for(int height = start; height <= endHeight; height++) {
|
||||||
|
blockSummaryMap.put(height, new BlockSummary(height, new Date(), 1.0d, 0, 0));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blockSummaryMap.putAll(electrumServer.getRecentBlockSummaryMap());
|
blockSummaryMap.putAll(electrumServer.getRecentBlockSummaryMap());
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
List<NewBlockEvent> events = new ArrayList<>(newBlockEvents);
|
||||||
|
events.removeIf(event -> blockSummaryMap.containsKey(event.getHeight()));
|
||||||
|
if(!events.isEmpty()) {
|
||||||
for(NewBlockEvent event : newBlockEvents) {
|
for(NewBlockEvent event : newBlockEvents) {
|
||||||
blockSummaryMap.putAll(electrumServer.getBlockSummaryMap(event.getHeight(), event.getBlockHeader()));
|
blockSummaryMap.putAll(electrumServer.getBlockSummaryMap(event.getHeight(), event.getBlockHeader()));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
public Double getMedianFee() {
|
||||||
return extras == null ? null : extras.medianFee();
|
return extras == null ? null : extras.medianFee();
|
||||||
}
|
}
|
||||||
|
|
@ -294,7 +294,7 @@ public enum FeeRatesSource {
|
||||||
if(height == null || timestamp == null) {
|
if(height == null || timestamp == null) {
|
||||||
throw new IllegalStateException("Height = " + height + ", timestamp = " + timestamp + ": both must be specified");
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.sparrowwallet.sparrow.wallet;
|
package com.sparrowwallet.sparrow.wallet;
|
||||||
|
|
||||||
public enum FeeRatesSelection {
|
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;
|
private final String name;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,7 @@ import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
import com.sparrowwallet.drongo.protocol.*;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.UnitFormat;
|
import com.sparrowwallet.sparrow.*;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
|
||||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
|
||||||
import com.sparrowwallet.sparrow.control.*;
|
import com.sparrowwallet.sparrow.control.*;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
|
@ -28,6 +25,7 @@ import com.sparrowwallet.sparrow.paynym.PayNymService;
|
||||||
import javafx.animation.KeyFrame;
|
import javafx.animation.KeyFrame;
|
||||||
import javafx.animation.Timeline;
|
import javafx.animation.Timeline;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.*;
|
import javafx.beans.property.*;
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
|
|
@ -78,6 +76,9 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
@FXML
|
@FXML
|
||||||
private ToggleButton mempoolSizeToggle;
|
private ToggleButton mempoolSizeToggle;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ToggleButton recentBlocksToggle;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Field targetBlocksField;
|
private Field targetBlocksField;
|
||||||
|
|
||||||
|
|
@ -117,6 +118,9 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
@FXML
|
@FXML
|
||||||
private MempoolSizeFeeRatesChart mempoolSizeFeeRatesChart;
|
private MempoolSizeFeeRatesChart mempoolSizeFeeRatesChart;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private RecentBlocksView recentBlocksView;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private TransactionDiagram transactionDiagram;
|
private TransactionDiagram transactionDiagram;
|
||||||
|
|
||||||
|
|
@ -162,6 +166,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
private final ObjectProperty<BlockTransaction> replacedTransactionProperty = new SimpleObjectProperty<>(null);
|
private final ObjectProperty<BlockTransaction> replacedTransactionProperty = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
|
private final ObjectProperty<FeeRatesSelection> feeRatesSelectionProperty = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
private final List<byte[]> opReturnsList = new ArrayList<>();
|
private final List<byte[]> opReturnsList = new ArrayList<>();
|
||||||
|
|
||||||
private final Set<WalletNode> excludedChangeNodes = new HashSet<>();
|
private final Set<WalletNode> excludedChangeNodes = new HashSet<>();
|
||||||
|
|
@ -299,6 +305,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
feeRange.valueProperty().addListener(feeRangeListener);
|
feeRange.valueProperty().addListener(feeRangeListener);
|
||||||
|
|
||||||
blockTargetFeeRatesChart.managedProperty().bind(blockTargetFeeRatesChart.visibleProperty());
|
blockTargetFeeRatesChart.managedProperty().bind(blockTargetFeeRatesChart.visibleProperty());
|
||||||
|
blockTargetFeeRatesChart.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.BLOCK_TARGET));
|
||||||
blockTargetFeeRatesChart.initialize();
|
blockTargetFeeRatesChart.initialize();
|
||||||
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
|
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
|
||||||
if(targetBlocksFeeRates != null) {
|
if(targetBlocksFeeRates != null) {
|
||||||
|
|
@ -308,20 +315,41 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
|
|
||||||
mempoolSizeFeeRatesChart.managedProperty().bind(mempoolSizeFeeRatesChart.visibleProperty());
|
mempoolSizeFeeRatesChart.managedProperty().bind(mempoolSizeFeeRatesChart.visibleProperty());
|
||||||
mempoolSizeFeeRatesChart.visibleProperty().bind(blockTargetFeeRatesChart.visibleProperty().not());
|
mempoolSizeFeeRatesChart.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.MEMPOOL_SIZE));
|
||||||
mempoolSizeFeeRatesChart.initialize();
|
mempoolSizeFeeRatesChart.initialize();
|
||||||
Map<Date, Set<MempoolRateSize>> mempoolHistogram = getMempoolHistogram();
|
Map<Date, Set<MempoolRateSize>> mempoolHistogram = getMempoolHistogram();
|
||||||
if(mempoolHistogram != null) {
|
if(mempoolHistogram != null) {
|
||||||
mempoolSizeFeeRatesChart.update(mempoolHistogram);
|
mempoolSizeFeeRatesChart.update(mempoolHistogram);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recentBlocksView.managedProperty().bind(recentBlocksView.visibleProperty());
|
||||||
|
recentBlocksView.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.RECENT_BLOCKS));
|
||||||
|
List<BlockSummary> 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 = Config.get().getFeeRatesSelection();
|
||||||
feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.MEMPOOL_SIZE : feeRatesSelection);
|
feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.RECENT_BLOCKS : feeRatesSelection);
|
||||||
cpfpFeeRate.managedProperty().bind(cpfpFeeRate.visibleProperty());
|
cpfpFeeRate.managedProperty().bind(cpfpFeeRate.visibleProperty());
|
||||||
cpfpFeeRate.setVisible(false);
|
cpfpFeeRate.setVisible(false);
|
||||||
setDefaultFeeRate();
|
setDefaultFeeRate();
|
||||||
updateFeeRateSelection(feeRatesSelection);
|
feeRatesSelectionProperty.set(feeRatesSelection);
|
||||||
feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle);
|
feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle :
|
||||||
|
(feeRatesSelection == FeeRatesSelection.MEMPOOL_SIZE ? mempoolSizeToggle : recentBlocksToggle));
|
||||||
feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
if(newValue != null) {
|
if(newValue != null) {
|
||||||
FeeRatesSelection newFeeRatesSelection = (FeeRatesSelection)newValue.getUserData();
|
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()));
|
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() {
|
private void setDefaultFeeRate() {
|
||||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||||
int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget);
|
int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget);
|
||||||
Double defaultRate = getTargetBlocksFeeRates().get(defaultTarget);
|
Double defaultRate = getTargetBlocksFeeRates().get(defaultTarget);
|
||||||
targetBlocks.setValue(index);
|
targetBlocks.setValue(index);
|
||||||
blockTargetFeeRatesChart.select(defaultTarget);
|
blockTargetFeeRatesChart.select(defaultTarget);
|
||||||
|
recentBlocksView.updateFeeRate(defaultRate);
|
||||||
setFeeRangeRate(defaultRate);
|
setFeeRangeRate(defaultRate);
|
||||||
setFeeRate(getFeeRangeRate());
|
setFeeRate(getFeeRangeRate());
|
||||||
if(Network.get().equals(Network.MAINNET) && defaultRate == getFallbackFeeRate()) {
|
if(Network.get().equals(Network.MAINNET) && defaultRate == getFallbackFeeRate()) {
|
||||||
|
|
@ -1411,10 +1428,15 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void feeRateSelectionChanged(FeeRatesSelectionChangedEvent event) {
|
public void feeRateSelectionChanged(FeeRatesSelectionChangedEvent event) {
|
||||||
if(event.getWallet() == getWalletForm().getWallet()) {
|
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
|
@Subscribe
|
||||||
public void spendUtxos(SpendUtxoEvent event) {
|
public void spendUtxos(SpendUtxoEvent event) {
|
||||||
if((event.getUtxos() == null || !event.getUtxos().isEmpty()) && event.getWallet().equals(getWalletForm().getWallet())) {
|
if((event.getUtxos() == null || !event.getUtxos().isEmpty()) && event.getWallet().equals(getWalletForm().getWallet())) {
|
||||||
|
|
|
||||||
|
|
@ -343,4 +343,32 @@ HorizontalHeaderColumn > TableColumnHeader.column-header.table-column{
|
||||||
|
|
||||||
#grid .spreadsheet-cell.selection {
|
#grid .spreadsheet-cell.selection {
|
||||||
-fx-text-fill: -fx-base;
|
-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;
|
||||||
}
|
}
|
||||||
|
|
@ -132,6 +132,66 @@
|
||||||
-fx-text-fill: -fx-accent;
|
-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 {
|
.vsizeChart {
|
||||||
VSIZE1-2_COLOR: rgb(216, 27, 96);
|
VSIZE1-2_COLOR: rgb(216, 27, 96);
|
||||||
VSIZE2-3_COLOR: rgb(142, 36, 170);
|
VSIZE2-3_COLOR: rgb(142, 36, 170);
|
||||||
|
|
@ -164,3 +224,45 @@
|
||||||
VSIZE600-700_COLOR: rgb(51, 105, 30);
|
VSIZE600-700_COLOR: rgb(51, 105, 30);
|
||||||
VSIZE700-800_COLOR: rgb(130, 119, 23);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
<?import com.sparrowwallet.sparrow.wallet.OptimizationStrategy?>
|
<?import com.sparrowwallet.sparrow.wallet.OptimizationStrategy?>
|
||||||
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
|
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
|
||||||
<?import com.sparrowwallet.sparrow.control.FeeRangeSlider?>
|
<?import com.sparrowwallet.sparrow.control.FeeRangeSlider?>
|
||||||
|
<?import com.sparrowwallet.sparrow.event.RecentBlocksView?>
|
||||||
|
|
||||||
<BorderPane stylesheets="@send.css, @wallet.css, @../script.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.SendController">
|
<BorderPane stylesheets="@send.css, @wallet.css, @../script.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.SendController">
|
||||||
<center>
|
<center>
|
||||||
|
|
@ -80,6 +81,14 @@
|
||||||
<FeeRatesSelection fx:constant="MEMPOOL_SIZE"/>
|
<FeeRatesSelection fx:constant="MEMPOOL_SIZE"/>
|
||||||
</userData>
|
</userData>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
<ToggleButton fx:id="recentBlocksToggle" text="Recent Blocks" toggleGroup="$feeSelectionToggleGroup">
|
||||||
|
<tooltip>
|
||||||
|
<Tooltip text="Show recent and upcoming blocks"/>
|
||||||
|
</tooltip>
|
||||||
|
<userData>
|
||||||
|
<FeeRatesSelection fx:constant="RECENT_BLOCKS"/>
|
||||||
|
</userData>
|
||||||
|
</ToggleButton>
|
||||||
</buttons>
|
</buttons>
|
||||||
</SegmentedButton>
|
</SegmentedButton>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|
@ -140,6 +149,7 @@
|
||||||
<NumberAxis side="LEFT" />
|
<NumberAxis side="LEFT" />
|
||||||
</yAxis>
|
</yAxis>
|
||||||
</MempoolSizeFeeRatesChart>
|
</MempoolSizeFeeRatesChart>
|
||||||
|
<RecentBlocksView fx:id="recentBlocksView" styleClass="feeRatesChart" AnchorPane.topAnchor="10" AnchorPane.leftAnchor="74" translateY="30" minHeight="135"/>
|
||||||
</AnchorPane>
|
</AnchorPane>
|
||||||
</GridPane>
|
</GridPane>
|
||||||
<AnchorPane>
|
<AnchorPane>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue