mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
optimize fetching mempool entries for fee histogram when connected to bitcoin core, fix and improve mempool fee rates chart
This commit is contained in:
parent
3242f00812
commit
171bf24133
17 changed files with 326 additions and 53 deletions
|
@ -669,6 +669,10 @@ public class AppServices {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addMempoolRateSizes(Set<MempoolRateSize> rateSizes) {
|
private void addMempoolRateSizes(Set<MempoolRateSize> rateSizes) {
|
||||||
|
if(rateSizes.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
LocalDateTime dateMinute = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES);
|
LocalDateTime dateMinute = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES);
|
||||||
if(mempoolHistogram.isEmpty()) {
|
if(mempoolHistogram.isEmpty()) {
|
||||||
mempoolHistogram.put(Date.from(dateMinute.minusMinutes(1).atZone(ZoneId.systemDefault()).toInstant()), rateSizes);
|
mempoolHistogram.put(Date.from(dateMinute.minusMinutes(1).atZone(ZoneId.systemDefault()).toInstant()), rateSizes);
|
||||||
|
@ -1022,8 +1026,6 @@ public class AppServices {
|
||||||
public void newConnection(ConnectionEvent event) {
|
public void newConnection(ConnectionEvent event) {
|
||||||
currentBlockHeight = event.getBlockHeight();
|
currentBlockHeight = event.getBlockHeight();
|
||||||
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
||||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
|
||||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
|
||||||
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
|
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
|
||||||
latestBlockHeader = event.getBlockHeader();
|
latestBlockHeader = event.getBlockHeader();
|
||||||
Config.get().addRecentServer();
|
Config.get().addRecentServer();
|
||||||
|
@ -1046,6 +1048,10 @@ public class AppServices {
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void feesUpdated(FeeRatesUpdatedEvent event) {
|
public void feesUpdated(FeeRatesUpdatedEvent event) {
|
||||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void mempoolRateSizes(MempoolRateSizesUpdatedEvent event) {
|
||||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,31 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.Theme;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.NamedArg;
|
import javafx.beans.NamedArg;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.event.EventHandler;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Point2D;
|
import javafx.geometry.Point2D;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Cursor;
|
import javafx.scene.Cursor;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.chart.*;
|
import javafx.scene.chart.*;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.*;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.StageStyle;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
@ -29,14 +41,80 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||||
private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm");
|
private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm");
|
||||||
public static final int MAX_PERIOD_HOURS = 2;
|
public static final int MAX_PERIOD_HOURS = 2;
|
||||||
private static final double Y_VALUE_BREAK_MVB = 3.0;
|
private static final double Y_VALUE_BREAK_MVB = 3.0;
|
||||||
|
private static final List<Integer> 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);
|
||||||
|
|
||||||
private Tooltip tooltip;
|
private Tooltip tooltip;
|
||||||
|
|
||||||
|
private MempoolSizeFeeRatesChart expandedChart;
|
||||||
|
private final EventHandler<MouseEvent> expandedChartHandler = new EventHandler<>() {
|
||||||
|
@Override
|
||||||
|
public void handle(MouseEvent event) {
|
||||||
|
if(!event.isConsumed() && event.getButton() != MouseButton.SECONDARY) {
|
||||||
|
Stage stage = new Stage(StageStyle.UNDECORATED);
|
||||||
|
stage.setTitle("Mempool by vBytes");
|
||||||
|
stage.initOwner(MempoolSizeFeeRatesChart.this.getScene().getWindow());
|
||||||
|
stage.initModality(Modality.WINDOW_MODAL);
|
||||||
|
stage.setResizable(false);
|
||||||
|
|
||||||
|
StackPane scenePane = new StackPane();
|
||||||
|
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||||
|
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
||||||
|
}
|
||||||
|
|
||||||
|
scenePane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||||
|
if(Config.get().getTheme() == Theme.DARK) {
|
||||||
|
scenePane.getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
|
||||||
|
}
|
||||||
|
scenePane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
||||||
|
scenePane.getStylesheets().add(AppServices.class.getResource("wallet/send.css").toExternalForm());
|
||||||
|
|
||||||
|
VBox vBox = new VBox(20);
|
||||||
|
vBox.setPadding(new Insets(20, 20, 20, 20));
|
||||||
|
|
||||||
|
expandedChart = new MempoolSizeFeeRatesChart();
|
||||||
|
expandedChart.initialize();
|
||||||
|
expandedChart.getStyleClass().add("vsizeChart");
|
||||||
|
expandedChart.update(AppServices.getMempoolHistogram());
|
||||||
|
expandedChart.setLegendVisible(false);
|
||||||
|
expandedChart.setAnimated(false);
|
||||||
|
expandedChart.setPrefWidth(700);
|
||||||
|
|
||||||
|
HBox buttonBox = new HBox();
|
||||||
|
buttonBox.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
Button button = new Button("Close");
|
||||||
|
button.setOnAction(e -> {
|
||||||
|
stage.close();
|
||||||
|
});
|
||||||
|
buttonBox.getChildren().add(button);
|
||||||
|
vBox.getChildren().addAll(expandedChart, buttonBox);
|
||||||
|
scenePane.getChildren().add(vBox);
|
||||||
|
|
||||||
|
Scene scene = new Scene(scenePane);
|
||||||
|
AppServices.onEscapePressed(scene, stage::close);
|
||||||
|
AppServices.setStageIcon(stage);
|
||||||
|
stage.setScene(scene);
|
||||||
|
stage.setOnShowing(e -> {
|
||||||
|
AppServices.moveToActiveWindowScreen(stage, 800, 460);
|
||||||
|
});
|
||||||
|
stage.setOnHidden(e -> {
|
||||||
|
expandedChart = null;
|
||||||
|
});
|
||||||
|
stage.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public MempoolSizeFeeRatesChart() {
|
||||||
|
super(new CategoryAxis(), new NumberAxis());
|
||||||
|
}
|
||||||
|
|
||||||
public MempoolSizeFeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
|
public MempoolSizeFeeRatesChart(@NamedArg("xAxis") Axis<String> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
|
||||||
super(xAxis, yAxis);
|
super(xAxis, yAxis);
|
||||||
|
setOnMouseClicked(expandedChartHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
|
getStyleClass().add("vsizeChart");
|
||||||
setCreateSymbols(false);
|
setCreateSymbols(false);
|
||||||
setCursor(Cursor.CROSSHAIR);
|
setCursor(Cursor.CROSSHAIR);
|
||||||
setVerticalGridLinesVisible(false);
|
setVerticalGridLinesVisible(false);
|
||||||
|
@ -78,17 +156,18 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
long previousFeeRate = 0;
|
for(int i = 0; i < FEE_RATES_INTERVALS.size(); i++) {
|
||||||
for(Long feeRate : AppServices.FEE_RATES_RANGE) {
|
int feeRate = FEE_RATES_INTERVALS.get(i);
|
||||||
|
int nextFeeRate = (i == FEE_RATES_INTERVALS.size() - 1 ? Integer.MAX_VALUE : FEE_RATES_INTERVALS.get(i+1));
|
||||||
XYChart.Series<String, Number> series = new XYChart.Series<>();
|
XYChart.Series<String, Number> series = new XYChart.Series<>();
|
||||||
series.setName(feeRate + "+ sats/vB");
|
series.setName(feeRate + "-" + (nextFeeRate == Integer.MAX_VALUE ? 900 : nextFeeRate));
|
||||||
long seriesTotalVSize = 0;
|
long seriesTotalVSize = 0;
|
||||||
|
|
||||||
for(Date date : periodRateSizes.keySet()) {
|
for(Date date : periodRateSizes.keySet()) {
|
||||||
Set<MempoolRateSize> rateSizes = periodRateSizes.get(date);
|
Set<MempoolRateSize> rateSizes = periodRateSizes.get(date);
|
||||||
long totalVSize = 0;
|
long totalVSize = 0;
|
||||||
for(MempoolRateSize rateSize : rateSizes) {
|
for(MempoolRateSize rateSize : rateSizes) {
|
||||||
if(rateSize.getFee() > previousFeeRate && rateSize.getFee() <= feeRate) {
|
if(rateSize.getFee() >= feeRate && rateSize.getFee() < nextFeeRate) {
|
||||||
totalVSize += rateSize.getVSize();
|
totalVSize += rateSize.getVSize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,8 +179,19 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||||
if(seriesTotalVSize > 0) {
|
if(seriesTotalVSize > 0) {
|
||||||
getData().add(series);
|
getData().add(series);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
previousFeeRate = feeRate;
|
for(int i = 0; i < getData().size(); i++) {
|
||||||
|
Series<String, Number> series = getData().get(i);
|
||||||
|
Set<Node> nodes = lookupAll(".series" + i);
|
||||||
|
for(Node node : nodes) {
|
||||||
|
if(node.getStyleClass().contains("chart-series-area-line")) {
|
||||||
|
node.setStyle("-fx-stroke: VSIZE" + series.getName() + "_COLOR; -fx-opacity: 0.2;");
|
||||||
|
} else {
|
||||||
|
node.setStyle("-fx-fill: VSIZE" + series.getName() + "_COLOR; -fx-opacity: 0.5;");
|
||||||
|
}
|
||||||
|
node.getStyleClass().remove("default-color" + i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final double maxMvB = getMaxMvB(getData());
|
final double maxMvB = getMaxMvB(getData());
|
||||||
|
@ -131,6 +221,10 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||||
numberAxis.setTickLabelsVisible(false);
|
numberAxis.setTickLabelsVisible(false);
|
||||||
numberAxis.setOpacity(0);
|
numberAxis.setOpacity(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(expandedChart != null) {
|
||||||
|
expandedChart.update(mempoolRateSizes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<Date, Set<MempoolRateSize>> getPeriodRateSizes(Map<Date, Set<MempoolRateSize>> mempoolRateSizes) {
|
private Map<Date, Set<MempoolRateSize>> getPeriodRateSizes(Map<Date, Set<MempoolRateSize>> mempoolRateSizes) {
|
||||||
|
@ -200,11 +294,9 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||||
double mvb = kvb / 1000;
|
double mvb = kvb / 1000;
|
||||||
if(mvb >= 0.01 || (maxMvB < Y_VALUE_BREAK_MVB && mvb > 0.001)) {
|
if(mvb >= 0.01 || (maxMvB < Y_VALUE_BREAK_MVB && mvb > 0.001)) {
|
||||||
String amount = (maxMvB < Y_VALUE_BREAK_MVB ? (int)kvb + " kvB" : String.format("%.2f", mvb) + " MvB");
|
String amount = (maxMvB < Y_VALUE_BREAK_MVB ? (int)kvb + " kvB" : String.format("%.2f", mvb) + " MvB");
|
||||||
Label label = new Label(series.getName() + ": " + amount);
|
Label label = new Label(series.getName() + " sats/vB: " + amount);
|
||||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
|
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
|
||||||
if(i < 8) {
|
circle.setStyle("-fx-text-fill: VSIZE" + series.getName() + "_COLOR; -fx-opacity: 0.7;");
|
||||||
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1));
|
|
||||||
}
|
|
||||||
label.setGraphic(circle);
|
label.setGraphic(circle);
|
||||||
getChildren().add(label);
|
getChildren().add(label);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,20 +5,15 @@ import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
public class FeeRatesUpdatedEvent {
|
public class FeeRatesUpdatedEvent extends MempoolRateSizesUpdatedEvent {
|
||||||
private final Map<Integer, Double> targetBlockFeeRates;
|
private final Map<Integer, Double> targetBlockFeeRates;
|
||||||
private final Set<MempoolRateSize> mempoolRateSizes;
|
|
||||||
|
|
||||||
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) {
|
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) {
|
||||||
|
super(mempoolRateSizes);
|
||||||
this.targetBlockFeeRates = targetBlockFeeRates;
|
this.targetBlockFeeRates = targetBlockFeeRates;
|
||||||
this.mempoolRateSizes = mempoolRateSizes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<Integer, Double> getTargetBlockFeeRates() {
|
public Map<Integer, Double> getTargetBlockFeeRates() {
|
||||||
return targetBlockFeeRates;
|
return targetBlockFeeRates;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<MempoolRateSize> getMempoolRateSizes() {
|
|
||||||
return mempoolRateSizes;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The event is posted when the first set of mempool entries (txid and vsizes) have been retrieved from the node.
|
||||||
|
* Cormorant only.
|
||||||
|
*/
|
||||||
|
public class MempoolEntriesInitializedEvent {
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class MempoolRateSizesUpdatedEvent {
|
||||||
|
private final Set<MempoolRateSize> mempoolRateSizes;
|
||||||
|
|
||||||
|
public MempoolRateSizesUpdatedEvent(Set<MempoolRateSize> mempoolRateSizes) {
|
||||||
|
this.mempoolRateSizes = mempoolRateSizes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<MempoolRateSize> getMempoolRateSizes() {
|
||||||
|
return mempoolRateSizes;
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigDecimal;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -235,16 +235,16 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<Long, Long> getFeeRateHistogram(Transport transport) {
|
public Map<Double, Long> getFeeRateHistogram(Transport transport) {
|
||||||
try {
|
try {
|
||||||
JsonRpcClient client = new JsonRpcClient(transport);
|
JsonRpcClient client = new JsonRpcClient(transport);
|
||||||
BigInteger[][] feesArray = new RetryLogic<BigInteger[][]>(DEFAULT_MAX_ATTEMPTS, RETRY_DELAY_SECS, IllegalStateException.class).getResult(() ->
|
BigDecimal[][] feesArray = new RetryLogic<BigDecimal[][]>(DEFAULT_MAX_ATTEMPTS, RETRY_DELAY_SECS, IllegalStateException.class).getResult(() ->
|
||||||
client.createRequest().returnAs(BigInteger[][].class).method("mempool.get_fee_histogram").id(idCounter.incrementAndGet()).execute());
|
client.createRequest().returnAs(BigDecimal[][].class).method("mempool.get_fee_histogram").id(idCounter.incrementAndGet()).execute());
|
||||||
|
|
||||||
Map<Long, Long> feeRateHistogram = new TreeMap<>();
|
Map<Double, Long> feeRateHistogram = new TreeMap<>();
|
||||||
for(BigInteger[] feePair : feesArray) {
|
for(BigDecimal[] feePair : feesArray) {
|
||||||
if(feePair[0].longValue() > 0) {
|
if(feePair[0].longValue() > 0) {
|
||||||
feeRateHistogram.put(feePair[0].longValue(), feePair[1].longValue());
|
feeRateHistogram.put(feePair[0].doubleValue(), feePair[1].longValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -836,9 +836,9 @@ public class ElectrumServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<MempoolRateSize> getMempoolRateSizes() throws ServerException {
|
public Set<MempoolRateSize> getMempoolRateSizes() throws ServerException {
|
||||||
Map<Long, Long> feeRateHistogram = electrumServerRpc.getFeeRateHistogram(getTransport());
|
Map<Double, Long> feeRateHistogram = electrumServerRpc.getFeeRateHistogram(getTransport());
|
||||||
Set<MempoolRateSize> mempoolRateSizes = new TreeSet<>();
|
Set<MempoolRateSize> mempoolRateSizes = new TreeSet<>();
|
||||||
for(Long fee : feeRateHistogram.keySet()) {
|
for(Double fee : feeRateHistogram.keySet()) {
|
||||||
mempoolRateSizes.add(new MempoolRateSize(fee, feeRateHistogram.get(fee)));
|
mempoolRateSizes.add(new MempoolRateSize(fee, feeRateHistogram.get(fee)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1331,6 +1331,13 @@ public class ElectrumServer {
|
||||||
bwtStartLock.unlock();
|
bwtStartLock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void mempoolEntriesInitialized(MempoolEntriesInitializedEvent event) throws ServerException {
|
||||||
|
ElectrumServer electrumServer = new ElectrumServer();
|
||||||
|
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||||
|
EventManager.get().post(new MempoolRateSizesUpdatedEvent(mempoolRateSizes));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ReadRunnable implements Runnable {
|
public static class ReadRunnable implements Runnable {
|
||||||
|
|
|
@ -30,7 +30,7 @@ public interface ElectrumServerRpc {
|
||||||
|
|
||||||
Map<Integer, Double> getFeeEstimates(Transport transport, List<Integer> targetBlocks);
|
Map<Integer, Double> getFeeEstimates(Transport transport, List<Integer> targetBlocks);
|
||||||
|
|
||||||
Map<Long, Long> getFeeRateHistogram(Transport transport);
|
Map<Double, Long> getFeeRateHistogram(Transport transport);
|
||||||
|
|
||||||
Double getMinimumRelayFee(Transport transport);
|
Double getMinimumRelayFee(Transport transport);
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,15 @@ package com.sparrowwallet.sparrow.net;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public class MempoolRateSize implements Comparable<MempoolRateSize> {
|
public class MempoolRateSize implements Comparable<MempoolRateSize> {
|
||||||
private final long fee;
|
private final double fee;
|
||||||
private final long vSize;
|
private final long vSize;
|
||||||
|
|
||||||
public MempoolRateSize(long fee, long vSize) {
|
public MempoolRateSize(double fee, long vSize) {
|
||||||
this.fee = fee;
|
this.fee = fee;
|
||||||
this.vSize = vSize;
|
this.vSize = vSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getFee() {
|
public double getFee() {
|
||||||
return fee;
|
return fee;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ public class MempoolRateSize implements Comparable<MempoolRateSize> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(MempoolRateSize other) {
|
public int compareTo(MempoolRateSize other) {
|
||||||
return Long.compare(fee, other.fee);
|
return Double.compare(fee, other.fee);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -13,7 +13,7 @@ import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigDecimal;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
@ -260,16 +260,16 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<Long, Long> getFeeRateHistogram(Transport transport) {
|
public Map<Double, Long> getFeeRateHistogram(Transport transport) {
|
||||||
try {
|
try {
|
||||||
JsonRpcClient client = new JsonRpcClient(transport);
|
JsonRpcClient client = new JsonRpcClient(transport);
|
||||||
BigInteger[][] feesArray = new RetryLogic<BigInteger[][]>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
|
BigDecimal[][] feesArray = new RetryLogic<BigDecimal[][]>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() ->
|
||||||
client.createRequest().returnAs(BigInteger[][].class).method("mempool.get_fee_histogram").id(idCounter.incrementAndGet()).execute());
|
client.createRequest().returnAs(BigDecimal[][].class).method("mempool.get_fee_histogram").id(idCounter.incrementAndGet()).execute());
|
||||||
|
|
||||||
Map<Long, Long> feeRateHistogram = new TreeMap<>();
|
Map<Double, Long> feeRateHistogram = new TreeMap<>();
|
||||||
for(BigInteger[] feePair : feesArray) {
|
for(BigDecimal[] feePair : feesArray) {
|
||||||
if(feePair[0].longValue() > 0) {
|
if(feePair[0].longValue() > 0) {
|
||||||
feeRateHistogram.put(feePair[0].longValue(), feePair[1].longValue());
|
feeRateHistogram.put(feePair[0].doubleValue(), feePair[1].longValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ public class Cormorant {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Server start() throws CormorantBitcoindException {
|
public Server start() throws CormorantBitcoindException {
|
||||||
bitcoindClient = new BitcoindClient();
|
bitcoindClient = new BitcoindClient(useWallets);
|
||||||
bitcoindClient.initialize();
|
bitcoindClient.initialize();
|
||||||
|
|
||||||
Thread importThread = new Thread(() -> {
|
Thread importThread = new Thread(() -> {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.net.cormorant.bitcoind;
|
||||||
|
|
||||||
import com.github.arteam.simplejsonrpc.client.JsonRpcClient;
|
import com.github.arteam.simplejsonrpc.client.JsonRpcClient;
|
||||||
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
|
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
@ -25,11 +26,14 @@ import com.sparrowwallet.sparrow.net.cormorant.electrum.ScriptHashStatus;
|
||||||
import com.sparrowwallet.sparrow.net.cormorant.index.Store;
|
import com.sparrowwallet.sparrow.net.cormorant.index.Store;
|
||||||
import com.sparrowwallet.drongo.protocol.*;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.concurrent.Service;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.locks.Condition;
|
import java.util.concurrent.locks.Condition;
|
||||||
import java.util.concurrent.locks.Lock;
|
import java.util.concurrent.locks.Lock;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
@ -60,6 +64,7 @@ public class BitcoindClient {
|
||||||
|
|
||||||
private Exception lastPollException;
|
private Exception lastPollException;
|
||||||
|
|
||||||
|
private final boolean useWallets;
|
||||||
private boolean pruned;
|
private boolean pruned;
|
||||||
private boolean legacyWalletExists;
|
private boolean legacyWalletExists;
|
||||||
|
|
||||||
|
@ -76,7 +81,11 @@ public class BitcoindClient {
|
||||||
|
|
||||||
private final List<String> pruneWarnedDescriptors = new ArrayList<>();
|
private final List<String> pruneWarnedDescriptors = new ArrayList<>();
|
||||||
|
|
||||||
public BitcoindClient() {
|
private final Map<String, MempoolEntry> mempoolEntries = new ConcurrentHashMap<>();
|
||||||
|
private MempoolEntriesState mempoolEntriesState = MempoolEntriesState.UNINITIALIZED;
|
||||||
|
private long timerTaskCount;
|
||||||
|
|
||||||
|
public BitcoindClient(boolean useWallets) {
|
||||||
BitcoindTransport bitcoindTransport;
|
BitcoindTransport bitcoindTransport;
|
||||||
|
|
||||||
Config config = Config.get();
|
Config config = Config.get();
|
||||||
|
@ -87,6 +96,7 @@ public class BitcoindClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.jsonRpcClient = new JsonRpcClient(bitcoindTransport);
|
this.jsonRpcClient = new JsonRpcClient(bitcoindTransport);
|
||||||
|
this.useWallets = useWallets;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initialize() throws CormorantBitcoindException {
|
public void initialize() throws CormorantBitcoindException {
|
||||||
|
@ -516,6 +526,64 @@ public class BitcoindClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void initializeMempoolEntries() {
|
||||||
|
mempoolEntriesState = MempoolEntriesState.INITIALIZING;
|
||||||
|
|
||||||
|
long start = System.currentTimeMillis();
|
||||||
|
Set<String> txids = getBitcoindService().getRawMempool();
|
||||||
|
long end = System.currentTimeMillis();
|
||||||
|
|
||||||
|
if(end - start < 1000) {
|
||||||
|
//Fast system, fetch all mempool data at once
|
||||||
|
mempoolEntries.putAll(getBitcoindService().getRawMempool(true));
|
||||||
|
} else {
|
||||||
|
//Slow system, fetch mempool entries one-by-one to avoid risking a node crash
|
||||||
|
for(String txid : txids) {
|
||||||
|
try {
|
||||||
|
MempoolEntry mempoolEntry = getBitcoindService().getMempoolEntry(txid);
|
||||||
|
mempoolEntries.put(txid, mempoolEntry);
|
||||||
|
} catch(JsonRpcException e) {
|
||||||
|
//ignore, probably tx has been removed from mempool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mempoolEntriesState = MempoolEntriesState.INITIALIZED;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateMempoolEntries() {
|
||||||
|
Set<String> txids = getBitcoindService().getRawMempool();
|
||||||
|
|
||||||
|
Set<String> removed = new HashSet<>(Sets.difference(mempoolEntries.keySet(), txids));
|
||||||
|
mempoolEntries.keySet().removeAll(removed);
|
||||||
|
|
||||||
|
Set<String> added = Sets.difference(txids, mempoolEntries.keySet());
|
||||||
|
for(String txid : added) {
|
||||||
|
try {
|
||||||
|
MempoolEntry mempoolEntry = getBitcoindService().getMempoolEntry(txid);
|
||||||
|
mempoolEntries.put(txid, mempoolEntry);
|
||||||
|
} catch(JsonRpcException e) {
|
||||||
|
//ignore, probably tx has been removed from mempool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, MempoolEntry> getMempoolEntries() {
|
||||||
|
return mempoolEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MempoolEntriesState getMempoolEntriesState() {
|
||||||
|
return mempoolEntriesState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InitializeMempoolEntriesService getInitializeMempoolEntriesService() {
|
||||||
|
return new InitializeMempoolEntriesService();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUseWallets() {
|
||||||
|
return useWallets;
|
||||||
|
}
|
||||||
|
|
||||||
public Store getStore() {
|
public Store getStore() {
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
@ -566,6 +634,10 @@ public class BitcoindClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(mempoolEntriesState == MempoolEntriesState.INITIALIZED && (++timerTaskCount+1) % 12 == 0) {
|
||||||
|
updateMempoolEntries();
|
||||||
|
}
|
||||||
|
|
||||||
ListSinceBlock listSinceBlock = getListSinceBlock(lastBlock);
|
ListSinceBlock listSinceBlock = getListSinceBlock(lastBlock);
|
||||||
String currentBlock = lastBlock;
|
String currentBlock = lastBlock;
|
||||||
updateStore(listSinceBlock);
|
updateStore(listSinceBlock);
|
||||||
|
@ -591,7 +663,7 @@ public class BitcoindClient {
|
||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
lastPollException = e;
|
lastPollException = e;
|
||||||
log.warn("Error polling Bitcoin Core: " + e.getMessage());
|
log.warn("Error polling Bitcoin Core", e);
|
||||||
|
|
||||||
if(syncing) {
|
if(syncing) {
|
||||||
syncingLock.lock();
|
syncingLock.lock();
|
||||||
|
@ -627,4 +699,21 @@ public class BitcoindClient {
|
||||||
return rescanSince == null ? "now" : rescanSince.getTime() / 1000;
|
return rescanSince == null ? "now" : rescanSince.getTime() / 1000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class InitializeMempoolEntriesService extends Service<Void> {
|
||||||
|
@Override
|
||||||
|
protected Task<Void> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected Void call() {
|
||||||
|
initializeMempoolEntries();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum MempoolEntriesState {
|
||||||
|
UNINITIALIZED, INITIALIZING, INITIALIZED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@JsonRpcService
|
@JsonRpcService
|
||||||
@JsonRpcParams(ParamsType.ARRAY)
|
@JsonRpcParams(ParamsType.ARRAY)
|
||||||
|
@ -22,6 +23,9 @@ public interface BitcoindClientService {
|
||||||
@JsonRpcMethod("estimatesmartfee")
|
@JsonRpcMethod("estimatesmartfee")
|
||||||
FeeInfo estimateSmartFee(@JsonRpcParam("conf_target") int blocks);
|
FeeInfo estimateSmartFee(@JsonRpcParam("conf_target") int blocks);
|
||||||
|
|
||||||
|
@JsonRpcMethod("getrawmempool")
|
||||||
|
Set<String> getRawMempool();
|
||||||
|
|
||||||
@JsonRpcMethod("getrawmempool")
|
@JsonRpcMethod("getrawmempool")
|
||||||
Map<String, MempoolEntry> getRawMempool(@JsonRpcParam("verbose") boolean verbose);
|
Map<String, MempoolEntry> getRawMempool(@JsonRpcParam("verbose") boolean verbose);
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,10 @@ import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcMethod;
|
||||||
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcOptional;
|
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcOptional;
|
||||||
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam;
|
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam;
|
||||||
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService;
|
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.SparrowWallet;
|
import com.sparrowwallet.sparrow.SparrowWallet;
|
||||||
|
import com.sparrowwallet.sparrow.event.MempoolEntriesInitializedEvent;
|
||||||
import com.sparrowwallet.sparrow.net.Version;
|
import com.sparrowwallet.sparrow.net.Version;
|
||||||
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
|
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
|
||||||
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.*;
|
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.*;
|
||||||
|
@ -60,9 +63,23 @@ public class ElectrumServerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonRpcMethod("mempool.get_fee_histogram")
|
@JsonRpcMethod("mempool.get_fee_histogram")
|
||||||
public List<List<Number>> getFeeHistogram() throws BitcoindIOException {
|
public List<List<Number>> getFeeHistogram() {
|
||||||
try {
|
BitcoindClient.MempoolEntriesState mempoolEntriesState = bitcoindClient.getMempoolEntriesState();
|
||||||
Map<String, MempoolEntry> mempoolEntries = bitcoindClient.getBitcoindService().getRawMempool(true);
|
if(mempoolEntriesState != BitcoindClient.MempoolEntriesState.INITIALIZED) {
|
||||||
|
if(bitcoindClient.isUseWallets() && mempoolEntriesState == BitcoindClient.MempoolEntriesState.UNINITIALIZED) {
|
||||||
|
BitcoindClient.InitializeMempoolEntriesService initializeMempoolEntriesService = bitcoindClient.getInitializeMempoolEntriesService();
|
||||||
|
initializeMempoolEntriesService.setOnSucceeded(successEvent -> {
|
||||||
|
EventManager.get().post(new MempoolEntriesInitializedEvent());
|
||||||
|
});
|
||||||
|
initializeMempoolEntriesService.setOnFailed(failedEvent -> {
|
||||||
|
log.error("Failed to initialize mempool entries", failedEvent.getSource().getException());
|
||||||
|
});
|
||||||
|
initializeMempoolEntriesService.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Collections.emptyList();
|
||||||
|
} else {
|
||||||
|
Map<String, MempoolEntry> mempoolEntries = bitcoindClient.getMempoolEntries();
|
||||||
|
|
||||||
List<VsizeFeerate> vsizeFeerates = mempoolEntries.values().stream().map(entry -> new VsizeFeerate(entry.vsize(), entry.fees().base())).sorted().toList();
|
List<VsizeFeerate> vsizeFeerates = mempoolEntries.values().stream().map(entry -> new VsizeFeerate(entry.vsize(), entry.fees().base())).sorted().toList();
|
||||||
|
|
||||||
|
@ -85,8 +102,6 @@ public class ElectrumServerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
return histogram;
|
return histogram;
|
||||||
} catch(IllegalStateException e) {
|
|
||||||
throw new BitcoindIOException(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,7 +219,9 @@ public class ElectrumServerService {
|
||||||
|
|
||||||
public VsizeFeerate(int vsize, double fee) {
|
public VsizeFeerate(int vsize, double fee) {
|
||||||
this.vsize = vsize;
|
this.vsize = vsize;
|
||||||
this.feerate = fee / vsize * 100000000;
|
double feeRate = fee / vsize * Transaction.SATOSHIS_PER_BITCOIN;
|
||||||
|
//Round down to 0.1 sats/vb precision
|
||||||
|
this.feerate = Math.floor(10 * feeRate) / 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1508,7 +1508,6 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
public void feeRatesUpdated(FeeRatesUpdatedEvent event) {
|
public void feeRatesUpdated(FeeRatesUpdatedEvent event) {
|
||||||
blockTargetFeeRatesChart.update(event.getTargetBlockFeeRates());
|
blockTargetFeeRatesChart.update(event.getTargetBlockFeeRates());
|
||||||
blockTargetFeeRatesChart.select(getTargetBlocks());
|
blockTargetFeeRatesChart.select(getTargetBlocks());
|
||||||
mempoolSizeFeeRatesChart.update(getMempoolHistogram());
|
|
||||||
if(targetBlocksField.isVisible()) {
|
if(targetBlocksField.isVisible()) {
|
||||||
setFeeRate(event.getTargetBlockFeeRates().get(getTargetBlocks()));
|
setFeeRate(event.getTargetBlockFeeRates().get(getTargetBlocks()));
|
||||||
} else {
|
} else {
|
||||||
|
@ -1517,6 +1516,11 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
addFeeRangeTrackHighlight(0);
|
addFeeRangeTrackHighlight(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void mempoolRateSizesUpdated(MempoolRateSizesUpdatedEvent event) {
|
||||||
|
mempoolSizeFeeRatesChart.update(getMempoolHistogram());
|
||||||
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void feeRateSelectionChanged(FeeRatesSelectionChangedEvent event) {
|
public void feeRateSelectionChanged(FeeRatesSelectionChangedEvent event) {
|
||||||
if(event.getWallet() == getWalletForm().getWallet()) {
|
if(event.getWallet() == getWalletForm().getWallet()) {
|
||||||
|
|
|
@ -16,6 +16,7 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class SparrowUtxoConfigPersister extends UtxoConfigPersister {
|
public class SparrowUtxoConfigPersister extends UtxoConfigPersister {
|
||||||
|
@ -38,7 +39,7 @@ public class SparrowUtxoConfigPersister extends UtxoConfigPersister {
|
||||||
Map<String, UtxoConfigPersisted> utxoConfigs = wallet.getUtxoMixes().entrySet().stream()
|
Map<String, UtxoConfigPersisted> utxoConfigs = wallet.getUtxoMixes().entrySet().stream()
|
||||||
.collect(Collectors.toMap(entry -> entry.getKey().toString(), entry -> new UtxoConfigPersisted(entry.getValue().getMixesDone(), entry.getValue().getExpired()),
|
.collect(Collectors.toMap(entry -> entry.getKey().toString(), entry -> new UtxoConfigPersisted(entry.getValue().getMixesDone(), entry.getValue().getExpired()),
|
||||||
(u, v) -> { throw new IllegalStateException("Duplicate utxo config hashes"); },
|
(u, v) -> { throw new IllegalStateException("Duplicate utxo config hashes"); },
|
||||||
HashMap::new));
|
ConcurrentHashMap::new));
|
||||||
|
|
||||||
return new UtxoConfigData(utxoConfigs);
|
return new UtxoConfigData(utxoConfigs);
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,4 +130,37 @@
|
||||||
|
|
||||||
#transactionDiagram .useradd-icon {
|
#transactionDiagram .useradd-icon {
|
||||||
-fx-text-fill: -fx-accent;
|
-fx-text-fill: -fx-accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vsizeChart {
|
||||||
|
VSIZE1-2_COLOR: rgb(216, 27, 96);
|
||||||
|
VSIZE2-3_COLOR: rgb(142, 36, 170);
|
||||||
|
VSIZE3-4_COLOR: rgb(94, 53, 177);
|
||||||
|
VSIZE4-5_COLOR: rgb(57, 73, 171);
|
||||||
|
VSIZE5-6_COLOR: rgb(30, 136, 229);
|
||||||
|
VSIZE6-8_COLOR: rgb(3, 155, 229);
|
||||||
|
VSIZE8-10_COLOR: rgb(0, 172, 193);
|
||||||
|
VSIZE10-12_COLOR: rgb(0, 137, 123);
|
||||||
|
VSIZE12-15_COLOR: rgb(67, 160, 71);
|
||||||
|
VSIZE15-20_COLOR: rgb(124, 179, 66);
|
||||||
|
VSIZE20-30_COLOR: rgb(192, 202, 51);
|
||||||
|
VSIZE30-40_COLOR: rgb(253, 216, 53);
|
||||||
|
VSIZE40-50_COLOR: rgb(255, 179, 0);
|
||||||
|
VSIZE50-60_COLOR: rgb(251, 140, 0);
|
||||||
|
VSIZE60-70_COLOR: rgb(244, 81, 30);
|
||||||
|
VSIZE70-80_COLOR: rgb(109, 76, 65);
|
||||||
|
VSIZE80-90_COLOR: rgb(117, 117, 117);
|
||||||
|
VSIZE90-100_COLOR: rgb(84, 110, 122);
|
||||||
|
VSIZE100-125_COLOR: rgb(183, 28, 28);
|
||||||
|
VSIZE125-150_COLOR: rgb(136, 14, 79);
|
||||||
|
VSIZE150-175_COLOR: rgb(74, 20, 140);
|
||||||
|
VSIZE175-200_COLOR: rgb(49, 27, 146);
|
||||||
|
VSIZE200-250_COLOR: rgb(26, 35, 126);
|
||||||
|
VSIZE250-300_COLOR: rgb(13, 71, 161);
|
||||||
|
VSIZE300-350_COLOR: rgb(1, 87, 155);
|
||||||
|
VSIZE350-400_COLOR: rgb(0, 96, 100);
|
||||||
|
VSIZE400-500_COLOR: rgb(0, 77, 64);
|
||||||
|
VSIZE500-600_COLOR: rgb(27, 94, 32);
|
||||||
|
VSIZE600-700_COLOR: rgb(51, 105, 30);
|
||||||
|
VSIZE700-800_COLOR: rgb(130, 119, 23);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue