Compare commits

...
5 Commits
12 changed files with 137 additions and 17 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ if(System.getProperty("os.arch") == "aarch64") {
def headless = "true".equals(System.getProperty("java.awt.headless"))
group 'com.sparrowwallet'
version '2.2.1'
version '2.2.2'
repositories {
mavenCentral()
+1 -1
View File
@@ -83,7 +83,7 @@ sudo apt install -y rpm fakeroot binutils
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
```shell
GIT_TAG="2.2.0"
GIT_TAG="2.2.1"
```
The project can then be initially cloned as follows:
+1 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.2.1</string>
<string>2.2.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -136,6 +136,8 @@ public class AppServices {
private static Map<Integer, Double> targetBlockFeeRates;
private static Double nextBlockMedianFeeRate;
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
private static Double minimumRelayFeeRate;
@@ -748,6 +750,10 @@ public class AppServices {
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
}
public static Double getNextBlockMedianFeeRate() {
return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate;
}
public static double getFallbackFeeRate() {
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
}
@@ -1249,11 +1255,13 @@ public class AppServices {
if(AppServices.currentBlockHeight != null) {
blockSummaries.keySet().removeIf(height -> AppServices.currentBlockHeight - height > 5);
}
nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
}
@Subscribe
public void feesUpdated(FeeRatesUpdatedEvent event) {
targetBlockFeeRates = event.getTargetBlockFeeRates();
nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
}
@Subscribe
@@ -18,7 +18,7 @@ import java.util.*;
public class SparrowWallet {
public static final String APP_ID = "sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "2.2.1";
public static final String APP_VERSION = "2.2.2";
public static final String APP_VERSION_SUFFIX = "";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
@@ -52,7 +52,10 @@ public class BlockCube extends Group {
public BlockCube(Integer weight, Double medianFee, Integer height, Integer txCount, Long timestamp, boolean confirmed) {
getStyleClass().addAll("block-" + Network.getCanonical().getName(), "block-cube");
this.confirmedProperty.set(confirmed);
this.feeRatesSource.set(Config.get().getFeeRatesSource());
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
this.feeRatesSource.set(feeRatesSource);
this.weightProperty.addListener((_, _, _) -> {
if(front != null) {
@@ -198,6 +201,8 @@ public class BlockCube extends Group {
} else {
feeRateIcon.getChildren().clear();
}
} else {
feeRateIcon.getChildren().clear();
}
}
}
@@ -17,8 +17,10 @@ import javafx.util.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.sparrowwallet.sparrow.AppServices.TARGET_BLOCKS_RANGE;
import static com.sparrowwallet.sparrow.control.BlockCube.CUBE_SIZE;
public class RecentBlocksView extends Pane {
@@ -48,12 +50,14 @@ public class RecentBlocksView extends Pane {
}
}));
updateFeeRatesSource(Config.get().getFeeRatesSource());
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
updateFeeRatesSource(feeRatesSource);
Tooltip.install(this, tooltip);
}
public void updateFeeRatesSource(FeeRatesSource feeRatesSource) {
tooltip.setText("Fee rate estimate from " + feeRatesSource.getDescription());
tooltip.setText("Fee rates from " + feeRatesSource.getDescription());
if(getCubes() != null && !getCubes().isEmpty()) {
getCubes().getFirst().setFeeRatesSource(feeRatesSource);
}
@@ -104,7 +108,7 @@ public class RecentBlocksView extends Pane {
}
}
public void addNewBlock(List<BlockSummary> latestBlocks, Double currentFeeRate) {
private void addNewBlock(List<BlockSummary> latestBlocks, Double currentFeeRate) {
if(getCubes().isEmpty()) {
return;
}
@@ -136,6 +140,14 @@ public class RecentBlocksView extends Pane {
}
}
public void updateFeeRate(Map<Integer, Double> targetBlockFeeRates) {
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
if(targetBlockFeeRates.get(defaultTarget) != null) {
Double defaultRate = targetBlockFeeRates.get(defaultTarget);
updateFeeRate(defaultRate);
}
}
public void updateFeeRate(Double currentFeeRate) {
if(!getCubes().isEmpty()) {
BlockCube firstCube = getCubes().getFirst();
@@ -6,12 +6,18 @@ import java.util.Map;
public class BlockSummaryEvent {
private final Map<Integer, BlockSummary> blockSummaryMap;
private final Double nextBlockMedianFeeRate;
public BlockSummaryEvent(Map<Integer, BlockSummary> blockSummaryMap) {
public BlockSummaryEvent(Map<Integer, BlockSummary> blockSummaryMap, Double nextBlockMedianFeeRate) {
this.blockSummaryMap = blockSummaryMap;
this.nextBlockMedianFeeRate = nextBlockMedianFeeRate;
}
public Map<Integer, BlockSummary> getBlockSummaryMap() {
return blockSummaryMap;
}
public Double getNextBlockMedianFeeRate() {
return nextBlockMedianFeeRate;
}
}
@@ -7,13 +7,23 @@ import java.util.Set;
public class FeeRatesUpdatedEvent extends MempoolRateSizesUpdatedEvent {
private final Map<Integer, Double> targetBlockFeeRates;
private final Double nextBlockMedianFeeRate;
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes) {
this(targetBlockFeeRates, mempoolRateSizes, null);
}
public FeeRatesUpdatedEvent(Map<Integer, Double> targetBlockFeeRates, Set<MempoolRateSize> mempoolRateSizes, Double nextBlockMedianFeeRate) {
super(mempoolRateSizes);
this.targetBlockFeeRates = targetBlockFeeRates;
this.nextBlockMedianFeeRate = nextBlockMedianFeeRate;
}
public Map<Integer, Double> getTargetBlockFeeRates() {
return targetBlockFeeRates;
}
public Double getNextBlockMedianFeeRate() {
return nextBlockMedianFeeRate;
}
}
@@ -936,6 +936,20 @@ public class ElectrumServer {
return targetBlocksFeeRatesSats;
}
public Double getNextBlockMedianFeeRate() {
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
if(feeRatesSource.supportsNetwork(Network.get())) {
try {
return feeRatesSource.getNextBlockMedianFeeRate();
} catch(Exception e) {
return null;
}
}
return null;
}
public Map<Integer, Double> getDefaultFeeEstimates(List<Integer> targetBlocks) throws ServerException {
try {
Map<Integer, Double> targetBlocksFeeRatesBtcKb = electrumServerRpc.getFeeEstimates(getTransport(), targetBlocks);
@@ -1048,6 +1062,10 @@ public class ElectrumServer {
List<BlockTransactionHash> recentTransactions = feeRatesSource.getRecentMempoolTransactions();
Map<BlockTransactionHash, Transaction> setReferences = new HashMap<>();
setReferences.put(recentTransactions.getFirst(), null);
Random random = new Random();
if(random.nextBoolean()) {
setReferences.put(recentTransactions.get(random.nextInt(recentTransactions.size())), null);
}
Map<Sha256Hash, BlockTransaction> transactions = getTransactions(null, setReferences, Collections.emptyMap());
return transactions.values().stream().filter(blxTx -> blxTx.getTransaction() != null).toList();
} catch(Exception e) {
@@ -1456,8 +1474,9 @@ public class ElectrumServer {
if(elapsed > FEE_RATES_PERIOD) {
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
Double nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate();
feeRatesRetrievedAt = System.currentTimeMillis();
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes, nextBlockMedianFeeRate);
}
} else {
closeConnection();
@@ -1935,7 +1954,8 @@ public class ElectrumServer {
protected FeeRatesUpdatedEvent call() throws ServerException {
ElectrumServer electrumServer = new ElectrumServer();
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
return new FeeRatesUpdatedEvent(blockTargetFeeRates, null);
Double nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate();
return new FeeRatesUpdatedEvent(blockTargetFeeRates, null, nextBlockMedianFeeRate);
}
};
}
@@ -1985,7 +2005,11 @@ public class ElectrumServer {
subscribeRecent(electrumServer);
}
return new BlockSummaryEvent(blockSummaryMap);
Double nextBlockMedianFeeRate = null;
if(!isBlockstorm(totalBlocks)) {
nextBlockMedianFeeRate = electrumServer.getNextBlockMedianFeeRate();
}
return new BlockSummaryEvent(blockSummaryMap, nextBlockMedianFeeRate);
}
};
}
@@ -2005,12 +2029,15 @@ public class ElectrumServer {
Map<String, String> subscribeScriptHashes = new HashMap<>();
List<BlockTransaction> recentTransactions = electrumServer.getRecentMempoolTransactions();
for(BlockTransaction blkTx : recentTransactions) {
for(int i = 0; i < blkTx.getTransaction().getOutputs().size() && subscribeScriptHashes.size() < 10; i++) {
for(int i = 0; i < blkTx.getTransaction().getOutputs().size(); i++) {
TransactionOutput txOutput = blkTx.getTransaction().getOutputs().get(i);
String scriptHash = getScriptHash(txOutput);
if(!subscribedScriptHashes.containsKey(scriptHash)) {
subscribeScriptHashes.put("m/" + i, getScriptHash(txOutput));
}
if(Math.random() < 0.1d) {
break;
}
}
}
@@ -2023,14 +2050,24 @@ public class ElectrumServer {
}
}
if(!recentTransactions.isEmpty()) {
broadcastRecent(electrumServer, recentTransactions);
}
}
private void broadcastRecent(ElectrumServer electrumServer, List<BlockTransaction> recentTransactions) {
ScheduledService<Void> broadcastService = new ScheduledService<>() {
@Override
protected Task<Void> createTask() {
return new Task<>() {
@Override
protected Void call() throws Exception {
for(BlockTransaction blkTx : recentTransactions) {
electrumServer.broadcastTransaction(blkTx.getTransaction());
if(!recentTransactions.isEmpty()) {
Random random = new Random();
if(random.nextBoolean()) {
BlockTransaction blkTx = recentTransactions.get(random.nextInt(recentTransactions.size()));
electrumServer.broadcastTransaction(blkTx.getTransaction());
}
}
return null;
}
@@ -34,6 +34,12 @@ public enum FeeRatesSource {
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
}
@Override
public Double getNextBlockMedianFeeRate() throws Exception {
String url = getApiUrl() + "v1/fees/mempool-blocks";
return requestNextBlockMedianFeeRate(this, url);
}
@Override
public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception {
String url = getApiUrl() + "v1/block/" + Utils.bytesToHex(blockId.getReversedBytes());
@@ -130,6 +136,10 @@ public enum FeeRatesSource {
public abstract Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates);
public Double getNextBlockMedianFeeRate() throws Exception {
throw new UnsupportedOperationException(name + " does not support retrieving the next block median fee rate");
}
public BlockSummary getBlockSummary(Sha256Hash blockId) throws Exception {
throw new UnsupportedOperationException(name + " does not support block summaries");
}
@@ -199,6 +209,30 @@ public enum FeeRatesSource {
return httpClientService.requestJson(url, ThreeTierRates.class, null);
}
protected static Double requestNextBlockMedianFeeRate(FeeRatesSource feeRatesSource, String url) throws Exception {
if(log.isInfoEnabled()) {
log.info("Requesting next block median fee rate from " + url);
}
HttpClientService httpClientService = AppServices.getHttpClientService();
try {
MempoolBlock[] mempoolBlocks = feeRatesSource.requestMempoolBlocks(url, httpClientService);
return mempoolBlocks.length > 0 ? mempoolBlocks[0].medianFee : null;
} catch (Exception e) {
if(log.isDebugEnabled()) {
log.warn("Error retrieving next block median fee rate from " + url, e);
} else {
log.warn("Error retrieving next block median fee rate from " + url + " (" + e.getMessage() + ")");
}
throw e;
}
}
protected MempoolBlock[] requestMempoolBlocks(String url, HttpClientService httpClientService) throws Exception {
return httpClientService.requestJson(url, MempoolBlock[].class, null);
}
protected static BlockSummary requestBlockSummary(FeeRatesSource feeRatesSource, String url) throws Exception {
if(log.isInfoEnabled()) {
log.info("Requesting block summary from " + url);
@@ -309,6 +343,8 @@ public enum FeeRatesSource {
}
}
protected record MempoolBlock(Integer nTx, Double medianFee) {}
protected record MempoolBlockSummary(String id, Integer height, Long timestamp, Integer tx_count, Integer weight, MempoolBlockSummaryExtras extras) {
public Double getMedianFee() {
return extras == null ? null : extras.medianFee();
@@ -326,7 +326,7 @@ public class SendController extends WalletFormController implements Initializabl
recentBlocksView.visibleProperty().bind(Bindings.equal(feeRatesSelectionProperty, FeeRatesSelection.RECENT_BLOCKS));
List<BlockSummary> blockSummaries = AppServices.getBlockSummaries().values().stream().sorted().toList();
if(!blockSummaries.isEmpty()) {
recentBlocksView.update(blockSummaries, AppServices.getDefaultFeeRate());
recentBlocksView.update(blockSummaries, AppServices.getNextBlockMedianFeeRate());
}
feeRatesSelectionProperty.addListener((_, oldValue, newValue) -> {
@@ -1412,6 +1412,12 @@ public class SendController extends WalletFormController implements Initializabl
}
feeRange.updateTrackHighlight();
if(event.getNextBlockMedianFeeRate() != null) {
recentBlocksView.updateFeeRate(event.getNextBlockMedianFeeRate());
} else {
recentBlocksView.updateFeeRate(event.getTargetBlockFeeRates());
}
if(updateDefaultFeeRate) {
if(getFeeRate() != null && Long.valueOf((long)getFallbackFeeRate()).equals(getFeeRate().longValue())) {
setDefaultFeeRate();
@@ -1434,7 +1440,7 @@ public class SendController extends WalletFormController implements Initializabl
@Subscribe
public void blockSummary(BlockSummaryEvent event) {
Platform.runLater(() -> recentBlocksView.update(AppServices.getBlockSummaries().values().stream().sorted().toList(), AppServices.getDefaultFeeRate()));
Platform.runLater(() -> recentBlocksView.update(AppServices.getBlockSummaries().values().stream().sorted().toList(), AppServices.getNextBlockMedianFeeRate()));
}
@Subscribe