From 171bf24133259a4676b0519f7a954c4fc40fc784 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 20 Jun 2023 16:15:19 +0200 Subject: [PATCH] optimize fetching mempool entries for fee histogram when connected to bitcoin core, fix and improve mempool fee rates chart --- .../sparrowwallet/sparrow/AppServices.java | 10 +- .../control/MempoolSizeFeeRatesChart.java | 114 ++++++++++++++++-- .../sparrow/event/FeeRatesUpdatedEvent.java | 9 +- .../event/MempoolEntriesInitializedEvent.java | 8 ++ .../event/MempoolRateSizesUpdatedEvent.java | 17 +++ .../sparrow/net/BatchedElectrumServerRpc.java | 14 +-- .../sparrow/net/ElectrumServer.java | 11 +- .../sparrow/net/ElectrumServerRpc.java | 2 +- .../sparrow/net/MempoolRateSize.java | 8 +- .../sparrow/net/SimpleElectrumServerRpc.java | 14 +-- .../sparrow/net/cormorant/Cormorant.java | 2 +- .../cormorant/bitcoind/BitcoindClient.java | 93 +++++++++++++- .../bitcoind/BitcoindClientService.java | 4 + .../electrum/ElectrumServerService.java | 29 ++++- .../sparrow/wallet/SendController.java | 6 +- .../SparrowUtxoConfigPersister.java | 3 +- .../com/sparrowwallet/sparrow/wallet/send.css | 35 +++++- 17 files changed, 326 insertions(+), 53 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/MempoolEntriesInitializedEvent.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/MempoolRateSizesUpdatedEvent.java diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index b6579f49..4cbe5cba 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -669,6 +669,10 @@ public class AppServices { } private void addMempoolRateSizes(Set rateSizes) { + if(rateSizes.isEmpty()) { + return; + } + LocalDateTime dateMinute = LocalDateTime.now().truncatedTo(ChronoUnit.MINUTES); if(mempoolHistogram.isEmpty()) { mempoolHistogram.put(Date.from(dateMinute.minusMinutes(1).atZone(ZoneId.systemDefault()).toInstant()), rateSizes); @@ -1022,8 +1026,6 @@ public class AppServices { public void newConnection(ConnectionEvent event) { currentBlockHeight = event.getBlockHeight(); 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); latestBlockHeader = event.getBlockHeader(); Config.get().addRecentServer(); @@ -1046,6 +1048,10 @@ public class AppServices { @Subscribe public void feesUpdated(FeeRatesUpdatedEvent event) { targetBlockFeeRates = event.getTargetBlockFeeRates(); + } + + @Subscribe + public void mempoolRateSizes(MempoolRateSizesUpdatedEvent event) { addMempoolRateSizes(event.getMempoolRateSizes()); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java b/src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java index 4c5f76e3..771a7f10 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java @@ -1,19 +1,31 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.Theme; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; +import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.net.MempoolRateSize; import javafx.application.Platform; import javafx.beans.NamedArg; import javafx.collections.FXCollections; +import javafx.event.EventHandler; +import javafx.geometry.Insets; import javafx.geometry.Point2D; +import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.Node; +import javafx.scene.Scene; import javafx.scene.chart.*; +import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; -import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; +import javafx.scene.input.MouseButton; +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.StringConverter; import org.controlsfx.glyphfont.Glyph; @@ -29,14 +41,80 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart { private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm"); public static final int MAX_PERIOD_HOURS = 2; private static final double Y_VALUE_BREAK_MVB = 3.0; + private static final List 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 MempoolSizeFeeRatesChart expandedChart; + private final EventHandler 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 xAxis, @NamedArg("yAxis") Axis yAxis) { super(xAxis, yAxis); + setOnMouseClicked(expandedChartHandler); } public void initialize() { + getStyleClass().add("vsizeChart"); setCreateSymbols(false); setCursor(Cursor.CROSSHAIR); setVerticalGridLinesVisible(false); @@ -78,17 +156,18 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart { } }); - long previousFeeRate = 0; - for(Long feeRate : AppServices.FEE_RATES_RANGE) { + for(int i = 0; i < FEE_RATES_INTERVALS.size(); i++) { + 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 series = new XYChart.Series<>(); - series.setName(feeRate + "+ sats/vB"); + series.setName(feeRate + "-" + (nextFeeRate == Integer.MAX_VALUE ? 900 : nextFeeRate)); long seriesTotalVSize = 0; for(Date date : periodRateSizes.keySet()) { Set rateSizes = periodRateSizes.get(date); long totalVSize = 0; for(MempoolRateSize rateSize : rateSizes) { - if(rateSize.getFee() > previousFeeRate && rateSize.getFee() <= feeRate) { + if(rateSize.getFee() >= feeRate && rateSize.getFee() < nextFeeRate) { totalVSize += rateSize.getVSize(); } } @@ -100,8 +179,19 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart { if(seriesTotalVSize > 0) { getData().add(series); } + } - previousFeeRate = feeRate; + for(int i = 0; i < getData().size(); i++) { + Series series = getData().get(i); + Set 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()); @@ -131,6 +221,10 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart { numberAxis.setTickLabelsVisible(false); numberAxis.setOpacity(0); } + + if(expandedChart != null) { + expandedChart.update(mempoolRateSizes); + } } private Map> getPeriodRateSizes(Map> mempoolRateSizes) { @@ -200,11 +294,9 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart { double mvb = kvb / 1000; 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"); - 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); - if(i < 8) { - circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1)); - } + circle.setStyle("-fx-text-fill: VSIZE" + series.getName() + "_COLOR; -fx-opacity: 0.7;"); label.setGraphic(circle); getChildren().add(label); } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java index 75db7224..82a99958 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/FeeRatesUpdatedEvent.java @@ -5,20 +5,15 @@ import com.sparrowwallet.sparrow.net.MempoolRateSize; import java.util.Map; import java.util.Set; -public class FeeRatesUpdatedEvent { +public class FeeRatesUpdatedEvent extends MempoolRateSizesUpdatedEvent { private final Map targetBlockFeeRates; - private final Set mempoolRateSizes; public FeeRatesUpdatedEvent(Map targetBlockFeeRates, Set mempoolRateSizes) { + super(mempoolRateSizes); this.targetBlockFeeRates = targetBlockFeeRates; - this.mempoolRateSizes = mempoolRateSizes; } public Map getTargetBlockFeeRates() { return targetBlockFeeRates; } - - public Set getMempoolRateSizes() { - return mempoolRateSizes; - } } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/MempoolEntriesInitializedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/MempoolEntriesInitializedEvent.java new file mode 100644 index 00000000..07f67d7d --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/MempoolEntriesInitializedEvent.java @@ -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 { +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/MempoolRateSizesUpdatedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/MempoolRateSizesUpdatedEvent.java new file mode 100644 index 00000000..8c5ea857 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/MempoolRateSizesUpdatedEvent.java @@ -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 mempoolRateSizes; + + public MempoolRateSizesUpdatedEvent(Set mempoolRateSizes) { + this.mempoolRateSizes = mempoolRateSizes; + } + + public Set getMempoolRateSizes() { + return mempoolRateSizes; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java index ce101b90..7d1f8775 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java @@ -12,7 +12,7 @@ import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.math.BigInteger; +import java.math.BigDecimal; import java.util.*; import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; @@ -235,16 +235,16 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc { } @Override - public Map getFeeRateHistogram(Transport transport) { + public Map getFeeRateHistogram(Transport transport) { try { JsonRpcClient client = new JsonRpcClient(transport); - BigInteger[][] feesArray = new RetryLogic(DEFAULT_MAX_ATTEMPTS, RETRY_DELAY_SECS, IllegalStateException.class).getResult(() -> - client.createRequest().returnAs(BigInteger[][].class).method("mempool.get_fee_histogram").id(idCounter.incrementAndGet()).execute()); + BigDecimal[][] feesArray = new RetryLogic(DEFAULT_MAX_ATTEMPTS, RETRY_DELAY_SECS, IllegalStateException.class).getResult(() -> + client.createRequest().returnAs(BigDecimal[][].class).method("mempool.get_fee_histogram").id(idCounter.incrementAndGet()).execute()); - Map feeRateHistogram = new TreeMap<>(); - for(BigInteger[] feePair : feesArray) { + Map feeRateHistogram = new TreeMap<>(); + for(BigDecimal[] feePair : feesArray) { if(feePair[0].longValue() > 0) { - feeRateHistogram.put(feePair[0].longValue(), feePair[1].longValue()); + feeRateHistogram.put(feePair[0].doubleValue(), feePair[1].longValue()); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java index 055025b0..7db6652e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServer.java @@ -836,9 +836,9 @@ public class ElectrumServer { } public Set getMempoolRateSizes() throws ServerException { - Map feeRateHistogram = electrumServerRpc.getFeeRateHistogram(getTransport()); + Map feeRateHistogram = electrumServerRpc.getFeeRateHistogram(getTransport()); Set mempoolRateSizes = new TreeSet<>(); - for(Long fee : feeRateHistogram.keySet()) { + for(Double fee : feeRateHistogram.keySet()) { mempoolRateSizes.add(new MempoolRateSize(fee, feeRateHistogram.get(fee))); } @@ -1331,6 +1331,13 @@ public class ElectrumServer { bwtStartLock.unlock(); } } + + @Subscribe + public void mempoolEntriesInitialized(MempoolEntriesInitializedEvent event) throws ServerException { + ElectrumServer electrumServer = new ElectrumServer(); + Set mempoolRateSizes = electrumServer.getMempoolRateSizes(); + EventManager.get().post(new MempoolRateSizesUpdatedEvent(mempoolRateSizes)); + } } public static class ReadRunnable implements Runnable { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java index 7e0eac05..cc30e196 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/ElectrumServerRpc.java @@ -30,7 +30,7 @@ public interface ElectrumServerRpc { Map getFeeEstimates(Transport transport, List targetBlocks); - Map getFeeRateHistogram(Transport transport); + Map getFeeRateHistogram(Transport transport); Double getMinimumRelayFee(Transport transport); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/MempoolRateSize.java b/src/main/java/com/sparrowwallet/sparrow/net/MempoolRateSize.java index b0df8a55..1718b638 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/MempoolRateSize.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/MempoolRateSize.java @@ -3,15 +3,15 @@ package com.sparrowwallet.sparrow.net; import java.util.Objects; public class MempoolRateSize implements Comparable { - private final long fee; + private final double fee; private final long vSize; - public MempoolRateSize(long fee, long vSize) { + public MempoolRateSize(double fee, long vSize) { this.fee = fee; this.vSize = vSize; } - public long getFee() { + public double getFee() { return fee; } @@ -38,7 +38,7 @@ public class MempoolRateSize implements Comparable { @Override public int compareTo(MempoolRateSize other) { - return Long.compare(fee, other.fee); + return Double.compare(fee, other.fee); } @Override diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java index 25184386..2cb870b2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java @@ -13,7 +13,7 @@ import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.math.BigInteger; +import java.math.BigDecimal; import java.util.*; import java.util.concurrent.atomic.AtomicLong; @@ -260,16 +260,16 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc { } @Override - public Map getFeeRateHistogram(Transport transport) { + public Map getFeeRateHistogram(Transport transport) { try { JsonRpcClient client = new JsonRpcClient(transport); - BigInteger[][] feesArray = new RetryLogic(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() -> - client.createRequest().returnAs(BigInteger[][].class).method("mempool.get_fee_histogram").id(idCounter.incrementAndGet()).execute()); + BigDecimal[][] feesArray = new RetryLogic(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(() -> + client.createRequest().returnAs(BigDecimal[][].class).method("mempool.get_fee_histogram").id(idCounter.incrementAndGet()).execute()); - Map feeRateHistogram = new TreeMap<>(); - for(BigInteger[] feePair : feesArray) { + Map feeRateHistogram = new TreeMap<>(); + for(BigDecimal[] feePair : feesArray) { if(feePair[0].longValue() > 0) { - feeRateHistogram.put(feePair[0].longValue(), feePair[1].longValue()); + feeRateHistogram.put(feePair[0].doubleValue(), feePair[1].longValue()); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java index ceb59f65..c59b8f93 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/Cormorant.java @@ -35,7 +35,7 @@ public class Cormorant { } public Server start() throws CormorantBitcoindException { - bitcoindClient = new BitcoindClient(); + bitcoindClient = new BitcoindClient(useWallets); bitcoindClient.initialize(); Thread importThread = new Thread(() -> { diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java index 36ee0d39..977ad127 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClient.java @@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.net.cormorant.bitcoind; import com.github.arteam.simplejsonrpc.client.JsonRpcClient; import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException; +import com.google.common.collect.Sets; import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.OutputDescriptor; 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.drongo.protocol.*; import javafx.application.Platform; +import javafx.concurrent.Service; +import javafx.concurrent.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.time.Duration; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -60,6 +64,7 @@ public class BitcoindClient { private Exception lastPollException; + private final boolean useWallets; private boolean pruned; private boolean legacyWalletExists; @@ -76,7 +81,11 @@ public class BitcoindClient { private final List pruneWarnedDescriptors = new ArrayList<>(); - public BitcoindClient() { + private final Map mempoolEntries = new ConcurrentHashMap<>(); + private MempoolEntriesState mempoolEntriesState = MempoolEntriesState.UNINITIALIZED; + private long timerTaskCount; + + public BitcoindClient(boolean useWallets) { BitcoindTransport bitcoindTransport; Config config = Config.get(); @@ -87,6 +96,7 @@ public class BitcoindClient { } this.jsonRpcClient = new JsonRpcClient(bitcoindTransport); + this.useWallets = useWallets; } public void initialize() throws CormorantBitcoindException { @@ -516,6 +526,64 @@ public class BitcoindClient { } } + public void initializeMempoolEntries() { + mempoolEntriesState = MempoolEntriesState.INITIALIZING; + + long start = System.currentTimeMillis(); + Set 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 txids = getBitcoindService().getRawMempool(); + + Set removed = new HashSet<>(Sets.difference(mempoolEntries.keySet(), txids)); + mempoolEntries.keySet().removeAll(removed); + + Set 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 getMempoolEntries() { + return mempoolEntries; + } + + public MempoolEntriesState getMempoolEntriesState() { + return mempoolEntriesState; + } + + public InitializeMempoolEntriesService getInitializeMempoolEntriesService() { + return new InitializeMempoolEntriesService(); + } + + public boolean isUseWallets() { + return useWallets; + } + public Store getStore() { return store; } @@ -566,6 +634,10 @@ public class BitcoindClient { } } + if(mempoolEntriesState == MempoolEntriesState.INITIALIZED && (++timerTaskCount+1) % 12 == 0) { + updateMempoolEntries(); + } + ListSinceBlock listSinceBlock = getListSinceBlock(lastBlock); String currentBlock = lastBlock; updateStore(listSinceBlock); @@ -591,7 +663,7 @@ public class BitcoindClient { } } catch(Exception e) { lastPollException = e; - log.warn("Error polling Bitcoin Core: " + e.getMessage()); + log.warn("Error polling Bitcoin Core", e); if(syncing) { syncingLock.lock(); @@ -627,4 +699,21 @@ public class BitcoindClient { return rescanSince == null ? "now" : rescanSince.getTime() / 1000; } } + + public class InitializeMempoolEntriesService extends Service { + @Override + protected Task createTask() { + return new Task<>() { + @Override + protected Void call() { + initializeMempoolEntries(); + return null; + } + }; + } + } + + public enum MempoolEntriesState { + UNINITIALIZED, INITIALIZING, INITIALIZED + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClientService.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClientService.java index 6be17652..1276ce58 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClientService.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/bitcoind/BitcoindClientService.java @@ -9,6 +9,7 @@ import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService; import java.util.List; import java.util.Map; +import java.util.Set; @JsonRpcService @JsonRpcParams(ParamsType.ARRAY) @@ -22,6 +23,9 @@ public interface BitcoindClientService { @JsonRpcMethod("estimatesmartfee") FeeInfo estimateSmartFee(@JsonRpcParam("conf_target") int blocks); + @JsonRpcMethod("getrawmempool") + Set getRawMempool(); + @JsonRpcMethod("getrawmempool") Map getRawMempool(@JsonRpcParam("verbose") boolean verbose); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerService.java b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerService.java index 52fe1ef0..f10a7427 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerService.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/cormorant/electrum/ElectrumServerService.java @@ -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.JsonRpcParam; 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.event.MempoolEntriesInitializedEvent; import com.sparrowwallet.sparrow.net.Version; import com.sparrowwallet.sparrow.net.cormorant.Cormorant; import com.sparrowwallet.sparrow.net.cormorant.bitcoind.*; @@ -60,9 +63,23 @@ public class ElectrumServerService { } @JsonRpcMethod("mempool.get_fee_histogram") - public List> getFeeHistogram() throws BitcoindIOException { - try { - Map mempoolEntries = bitcoindClient.getBitcoindService().getRawMempool(true); + public List> getFeeHistogram() { + BitcoindClient.MempoolEntriesState mempoolEntriesState = bitcoindClient.getMempoolEntriesState(); + 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 mempoolEntries = bitcoindClient.getMempoolEntries(); List vsizeFeerates = mempoolEntries.values().stream().map(entry -> new VsizeFeerate(entry.vsize(), entry.fees().base())).sorted().toList(); @@ -85,8 +102,6 @@ public class ElectrumServerService { } return histogram; - } catch(IllegalStateException e) { - throw new BitcoindIOException(e); } } @@ -204,7 +219,9 @@ public class ElectrumServerService { public VsizeFeerate(int vsize, double fee) { 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 diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index c7c45adf..1e2de465 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -1508,7 +1508,6 @@ public class SendController extends WalletFormController implements Initializabl public void feeRatesUpdated(FeeRatesUpdatedEvent event) { blockTargetFeeRatesChart.update(event.getTargetBlockFeeRates()); blockTargetFeeRatesChart.select(getTargetBlocks()); - mempoolSizeFeeRatesChart.update(getMempoolHistogram()); if(targetBlocksField.isVisible()) { setFeeRate(event.getTargetBlockFeeRates().get(getTargetBlocks())); } else { @@ -1517,6 +1516,11 @@ public class SendController extends WalletFormController implements Initializabl addFeeRangeTrackHighlight(0); } + @Subscribe + public void mempoolRateSizesUpdated(MempoolRateSizesUpdatedEvent event) { + mempoolSizeFeeRatesChart.update(getMempoolHistogram()); + } + @Subscribe public void feeRateSelectionChanged(FeeRatesSelectionChangedEvent event) { if(event.getWallet() == getWalletForm().getWallet()) { diff --git a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataPersister/SparrowUtxoConfigPersister.java b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataPersister/SparrowUtxoConfigPersister.java index 33784b6a..0282e0f1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataPersister/SparrowUtxoConfigPersister.java +++ b/src/main/java/com/sparrowwallet/sparrow/whirlpool/dataPersister/SparrowUtxoConfigPersister.java @@ -16,6 +16,7 @@ import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; public class SparrowUtxoConfigPersister extends UtxoConfigPersister { @@ -38,7 +39,7 @@ public class SparrowUtxoConfigPersister extends UtxoConfigPersister { Map utxoConfigs = wallet.getUtxoMixes().entrySet().stream() .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"); }, - HashMap::new)); + ConcurrentHashMap::new)); return new UtxoConfigData(utxoConfigs); } diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css index 60e8df40..89a7958e 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.css @@ -130,4 +130,37 @@ #transactionDiagram .useradd-icon { -fx-text-fill: -fx-accent; -} \ No newline at end of file +} + +.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); +}