optimize fetching mempool entries for fee histogram when connected to bitcoin core, fix and improve mempool fee rates chart

This commit is contained in:
Craig Raw 2023-06-20 16:15:19 +02:00
parent 3242f00812
commit 171bf24133
17 changed files with 326 additions and 53 deletions

View file

@ -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());
} }

View file

@ -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);
} }

View file

@ -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;
}
} }

View file

@ -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 {
}

View file

@ -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;
}
}

View file

@ -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());
} }
} }

View file

@ -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 {

View file

@ -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);

View file

@ -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

View file

@ -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());
} }
} }

View file

@ -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(() -> {

View file

@ -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
}
} }

View file

@ -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);

View file

@ -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

View file

@ -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()) {

View file

@ -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);
} }

View file

@ -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);
}