mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
add increase fee functionality for rbf transactions
This commit is contained in:
parent
94960567b5
commit
2ca8b91283
15 changed files with 240 additions and 58 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit 49799fc0c8b5245a7931d0437a68172f9b6efbbc
|
Subproject commit 6b20c6558ab7cef6f582461692232a7687fe26c8
|
|
@ -2,8 +2,11 @@ package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
|
||||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||||
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
|
import com.sparrowwallet.sparrow.AppController;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
@ -19,7 +22,7 @@ import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
class EntryCell extends TreeTableCell<Entry, Entry> {
|
class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
|
@ -46,10 +49,10 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
||||||
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
|
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
|
||||||
setText("Unconfirmed Parent");
|
setText("Unconfirmed Parent");
|
||||||
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry.getBlockTransaction()));
|
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
|
||||||
} else if(transactionEntry.getBlockTransaction().getHeight() == 0) {
|
} else if(transactionEntry.getBlockTransaction().getHeight() == 0) {
|
||||||
setText("Unconfirmed");
|
setText("Unconfirmed");
|
||||||
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry.getBlockTransaction()));
|
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
|
||||||
} else {
|
} else {
|
||||||
String date = DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate());
|
String date = DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate());
|
||||||
setText(date);
|
setText(date);
|
||||||
|
@ -60,6 +63,7 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString());
|
tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString());
|
||||||
setTooltip(tooltip);
|
setTooltip(tooltip);
|
||||||
|
|
||||||
|
HBox actionBox = new HBox();
|
||||||
Button viewTransactionButton = new Button("");
|
Button viewTransactionButton = new Button("");
|
||||||
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
|
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
|
||||||
searchGlyph.setFontSize(12);
|
searchGlyph.setFontSize(12);
|
||||||
|
@ -67,7 +71,21 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
viewTransactionButton.setOnAction(event -> {
|
viewTransactionButton.setOnAction(event -> {
|
||||||
EventManager.get().post(new ViewTransactionEvent(transactionEntry.getBlockTransaction()));
|
EventManager.get().post(new ViewTransactionEvent(transactionEntry.getBlockTransaction()));
|
||||||
});
|
});
|
||||||
setGraphic(viewTransactionButton);
|
actionBox.getChildren().add(viewTransactionButton);
|
||||||
|
|
||||||
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
|
if(blockTransaction.getHeight() <= 0 && blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
|
Button increaseFeeButton = new Button("");
|
||||||
|
Glyph increaseFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_MEDICAL);
|
||||||
|
increaseFeeGlyph.setFontSize(12);
|
||||||
|
increaseFeeButton.setGraphic(increaseFeeGlyph);
|
||||||
|
increaseFeeButton.setOnAction(event -> {
|
||||||
|
increaseFee(transactionEntry);
|
||||||
|
});
|
||||||
|
actionBox.getChildren().add(increaseFeeButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
setGraphic(actionBox);
|
||||||
} else if(entry instanceof NodeEntry) {
|
} else if(entry instanceof NodeEntry) {
|
||||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||||
Address address = nodeEntry.getAddress();
|
Address address = nodeEntry.getAddress();
|
||||||
|
@ -139,9 +157,9 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
utxoEntries = List.of(hashIndexEntry);
|
utxoEntries = List.of(hashIndexEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<HashIndexEntry> spendingUtxoEntries = utxoEntries;
|
final List<BlockTransactionHashIndex> spendingUtxos = utxoEntries.stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList());
|
||||||
EventManager.get().post(new SendActionEvent(utxoEntries));
|
EventManager.get().post(new SendActionEvent(hashIndexEntry.getWallet(), spendingUtxos));
|
||||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(spendingUtxoEntries)));
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(hashIndexEntry.getWallet(), spendingUtxos)));
|
||||||
});
|
});
|
||||||
actionBox.getChildren().add(spendUtxoButton);
|
actionBox.getChildren().add(spendUtxoButton);
|
||||||
}
|
}
|
||||||
|
@ -151,8 +169,63 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void increaseFee(TransactionEntry transactionEntry) {
|
||||||
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
|
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
||||||
|
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
|
||||||
|
.filter(e -> e instanceof HashIndexEntry)
|
||||||
|
.map(e -> (HashIndexEntry)e)
|
||||||
|
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
|
||||||
|
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
|
||||||
|
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
List<TransactionOutput> ourOutputs = transactionEntry.getChildren().stream()
|
||||||
|
.filter(e -> e instanceof HashIndexEntry)
|
||||||
|
.map(e -> (HashIndexEntry)e)
|
||||||
|
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT))
|
||||||
|
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
||||||
|
Transaction tx = blockTransaction.getTransaction();
|
||||||
|
int vSize = tx.getVirtualSize();
|
||||||
|
int inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
|
||||||
|
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet());
|
||||||
|
Collections.shuffle(walletUtxos);
|
||||||
|
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty()) {
|
||||||
|
//If there is insufficent change output, include another random UTXO so the fee can be increased
|
||||||
|
BlockTransactionHashIndex utxo = walletUtxos.remove(0);
|
||||||
|
utxos.add(utxo);
|
||||||
|
changeTotal += utxo.getValue();
|
||||||
|
vSize += inputSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TransactionOutput> externalOutputs = new ArrayList<>(blockTransaction.getTransaction().getOutputs());
|
||||||
|
externalOutputs.removeAll(ourOutputs);
|
||||||
|
List<Payment> payments = externalOutputs.stream().map(txOutput -> {
|
||||||
|
try {
|
||||||
|
return new Payment(txOutput.getScript().getToAddresses()[0], transactionEntry.getLabel(), txOutput.getValue(), false);
|
||||||
|
} catch(Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}).filter(Objects::nonNull).collect(Collectors.toList());
|
||||||
|
|
||||||
|
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||||
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, blockTransaction.getFee(), true)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Double getMaxFeeRate() {
|
||||||
|
if(AppController.getTargetBlockFeeRates().isEmpty()) {
|
||||||
|
return 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppController.getTargetBlockFeeRates().values().iterator().next();
|
||||||
|
}
|
||||||
|
|
||||||
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
||||||
public UnconfirmedTransactionContextMenu(BlockTransaction blockTransaction) {
|
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
|
||||||
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||||
copyTxid.setOnAction(AE -> {
|
copyTxid.setOnAction(AE -> {
|
||||||
hide();
|
hide();
|
||||||
|
@ -161,7 +234,17 @@ class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
getItems().addAll(copyTxid);
|
getItems().add(copyTxid);
|
||||||
|
|
||||||
|
if(blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
|
MenuItem increaseFee = new MenuItem("Increase Fee");
|
||||||
|
increaseFee.setOnAction(AE -> {
|
||||||
|
hide();
|
||||||
|
increaseFee(transactionEntry);
|
||||||
|
});
|
||||||
|
|
||||||
|
getItems().add(increaseFee);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -174,7 +174,9 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||||
if(mvb >= 0.01) {
|
if(mvb >= 0.01) {
|
||||||
Label label = new Label(series.getName() + ": " + String.format("%.2f", mvb) + " MvB");
|
Label label = new Label(series.getName() + ": " + String.format("%.2f", mvb) + " MvB");
|
||||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
|
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
|
||||||
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1));
|
if(i < 8) {
|
||||||
|
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1));
|
||||||
|
}
|
||||||
label.setGraphic(circle);
|
label.setGraphic(circle);
|
||||||
getChildren().add(label);
|
getChildren().add(label);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
package com.sparrowwallet.sparrow.event;
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class SendActionEvent {
|
public class SendActionEvent {
|
||||||
private final List<HashIndexEntry> utxoEntries;
|
private final Wallet wallet;
|
||||||
|
private final List<BlockTransactionHashIndex> utxos;
|
||||||
|
|
||||||
public SendActionEvent(List<HashIndexEntry> utxoEntries) {
|
public SendActionEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||||
this.utxoEntries = utxoEntries;
|
this.wallet = wallet;
|
||||||
|
this.utxos = utxos;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<HashIndexEntry> getUtxoEntries() {
|
public Wallet getWallet() {
|
||||||
return utxoEntries;
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BlockTransactionHashIndex> getUtxos() {
|
||||||
|
return utxos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,47 @@
|
||||||
package com.sparrowwallet.sparrow.event;
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Payment;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class SpendUtxoEvent {
|
public class SpendUtxoEvent {
|
||||||
private final List<HashIndexEntry> utxoEntries;
|
private final Wallet wallet;
|
||||||
|
private final List<BlockTransactionHashIndex> utxos;
|
||||||
|
private final List<Payment> payments;
|
||||||
|
private final Long fee;
|
||||||
|
private final boolean includeMempoolInputs;
|
||||||
|
|
||||||
public SpendUtxoEvent(List<HashIndexEntry> utxoEntries) {
|
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||||
this.utxoEntries = utxoEntries;
|
this(wallet, utxos, null, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<HashIndexEntry> getUtxoEntries() {
|
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, Long fee, boolean includeMempoolInputs) {
|
||||||
return utxoEntries;
|
this.wallet = wallet;
|
||||||
|
this.utxos = utxos;
|
||||||
|
this.payments = payments;
|
||||||
|
this.fee = fee;
|
||||||
|
this.includeMempoolInputs = includeMempoolInputs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Wallet getWallet() {
|
||||||
|
return wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BlockTransactionHashIndex> getUtxos() {
|
||||||
|
return utxos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Payment> getPayments() {
|
||||||
|
return payments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getFee() {
|
||||||
|
return fee;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isIncludeMempoolInputs() {
|
||||||
|
return includeMempoolInputs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||||
ELLIPSIS_H('\uf141'),
|
ELLIPSIS_H('\uf141'),
|
||||||
EYE('\uf06e'),
|
EYE('\uf06e'),
|
||||||
HAND_HOLDING('\uf4bd'),
|
HAND_HOLDING('\uf4bd'),
|
||||||
|
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||||
KEY('\uf084'),
|
KEY('\uf084'),
|
||||||
LAPTOP('\uf109'),
|
LAPTOP('\uf109'),
|
||||||
LOCK('\uf023'),
|
LOCK('\uf023'),
|
||||||
|
|
|
@ -138,7 +138,16 @@ public class ElectrumServer {
|
||||||
|
|
||||||
public Map<WalletNode, Set<BlockTransactionHash>> getHistory(Wallet wallet, Collection<WalletNode> nodes) throws ServerException {
|
public Map<WalletNode, Set<BlockTransactionHash>> getHistory(Wallet wallet, Collection<WalletNode> nodes) throws ServerException {
|
||||||
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = new TreeMap<>();
|
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = new TreeMap<>();
|
||||||
subscribeWalletNodes(wallet, nodes, nodeTransactionMap, 0);
|
|
||||||
|
Set<WalletNode> historyNodes = new HashSet<>(nodes);
|
||||||
|
//Add any nodes with mempool transactions in case these have been replaced
|
||||||
|
Set<WalletNode> mempoolNodes = wallet.getWalletTxos().entrySet().stream()
|
||||||
|
.filter(entry -> entry.getKey().getHeight() <= 0 || (entry.getKey().getSpentBy() != null && entry.getKey().getSpentBy().getHeight() <= 0))
|
||||||
|
.map(Map.Entry::getValue)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
historyNodes.addAll(mempoolNodes);
|
||||||
|
|
||||||
|
subscribeWalletNodes(wallet, historyNodes, nodeTransactionMap, 0);
|
||||||
getReferences(wallet, nodeTransactionMap.keySet(), nodeTransactionMap, 0);
|
getReferences(wallet, nodeTransactionMap.keySet(), nodeTransactionMap, 0);
|
||||||
Set<BlockTransactionHash> newReferences = nodeTransactionMap.values().stream().flatMap(Collection::stream).filter(ref -> !wallet.getTransactions().containsKey(ref.getHash())).collect(Collectors.toSet());
|
Set<BlockTransactionHash> newReferences = nodeTransactionMap.values().stream().flatMap(Collection::stream).filter(ref -> !wallet.getTransactions().containsKey(ref.getHash())).collect(Collectors.toSet());
|
||||||
getReferencedTransactions(wallet, nodeTransactionMap);
|
getReferencedTransactions(wallet, nodeTransactionMap);
|
||||||
|
@ -152,7 +161,7 @@ public class ElectrumServer {
|
||||||
BlockTransaction blockTransaction = wallet.getTransactions().get(reference.getHash());
|
BlockTransaction blockTransaction = wallet.getTransactions().get(reference.getHash());
|
||||||
for(TransactionOutput txOutput : blockTransaction.getTransaction().getOutputs()) {
|
for(TransactionOutput txOutput : blockTransaction.getTransaction().getOutputs()) {
|
||||||
WalletNode node = walletScriptHashes.get(getScriptHash(txOutput));
|
WalletNode node = walletScriptHashes.get(getScriptHash(txOutput));
|
||||||
if(node != null && !nodes.contains(node)) {
|
if(node != null && !historyNodes.contains(node)) {
|
||||||
additionalNodes.add(node);
|
additionalNodes.add(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -162,7 +171,7 @@ public class ElectrumServer {
|
||||||
if(inputBlockTransaction != null) {
|
if(inputBlockTransaction != null) {
|
||||||
TransactionOutput txOutput = inputBlockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
TransactionOutput txOutput = inputBlockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
||||||
WalletNode node = walletScriptHashes.get(getScriptHash(txOutput));
|
WalletNode node = walletScriptHashes.get(getScriptHash(txOutput));
|
||||||
if(node != null && !nodes.contains(node)) {
|
if(node != null && !historyNodes.contains(node)) {
|
||||||
additionalNodes.add(node);
|
additionalNodes.add(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.sparrowwallet.sparrow.net;
|
package com.sparrowwallet.sparrow.net;
|
||||||
|
|
||||||
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcMethod;
|
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.JsonRpcParam;
|
||||||
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService;
|
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
@ -23,7 +24,12 @@ public class SubscriptionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonRpcMethod("blockchain.scripthash.subscribe")
|
@JsonRpcMethod("blockchain.scripthash.subscribe")
|
||||||
public void scriptHashStatusUpdated(@JsonRpcParam("scripthash") final String scriptHash, @JsonRpcParam("status") final String status) {
|
public void scriptHashStatusUpdated(@JsonRpcParam("scripthash") final String scriptHash, @JsonRpcOptional @JsonRpcParam("status") final String status) {
|
||||||
|
if(status == null) {
|
||||||
|
//Mempool transaction was replaced returning change/consolidation script hash status to null, ignore this update
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Set<String> existingStatuses = ElectrumServer.getSubscribedScriptHashes().get(scriptHash);
|
Set<String> existingStatuses = ElectrumServer.getSubscribedScriptHashes().get(scriptHash);
|
||||||
if(existingStatuses == null) {
|
if(existingStatuses == null) {
|
||||||
log.warn("Received script hash status update for unsubscribed script hash: " + scriptHash);
|
log.warn("Received script hash status update for unsubscribed script hash: " + scriptHash);
|
||||||
|
|
|
@ -255,6 +255,10 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||||
|
|
||||||
public void setPayment(Payment payment) {
|
public void setPayment(Payment payment) {
|
||||||
if(getRecipientValueSats() == null || payment.getAmount() != getRecipientValueSats()) {
|
if(getRecipientValueSats() == null || payment.getAmount() != getRecipientValueSats()) {
|
||||||
|
address.setText(payment.getAddress().toString());
|
||||||
|
if(payment.getLabel() != null) {
|
||||||
|
label.setText(payment.getLabel());
|
||||||
|
}
|
||||||
setRecipientValueSats(payment.getAmount());
|
setRecipientValueSats(payment.getAmount());
|
||||||
setFiatAmount(AppController.getFiatCurrencyExchangeRate(), payment.getAmount());
|
setFiatAmount(AppController.getFiatCurrencyExchangeRate(), payment.getAmount());
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,6 +119,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
private final StringProperty utxoLabelSelectionProperty = new SimpleStringProperty("");
|
private final StringProperty utxoLabelSelectionProperty = new SimpleStringProperty("");
|
||||||
|
|
||||||
|
private final BooleanProperty includeMempoolInputsProperty = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
private final ChangeListener<String> feeListener = new ChangeListener<>() {
|
private final ChangeListener<String> feeListener = new ChangeListener<>() {
|
||||||
@Override
|
@Override
|
||||||
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
|
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
|
||||||
|
@ -147,7 +149,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
feeRate.setText("Unknown");
|
feeRate.setText("Unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
Tooltip tooltip = new Tooltip("Target confirmation within " + target + " blocks");
|
Tooltip tooltip = new Tooltip("Target inclusion within " + target + " blocks");
|
||||||
targetBlocks.setTooltip(tooltip);
|
targetBlocks.setTooltip(tooltip);
|
||||||
|
|
||||||
userFeeSet.set(false);
|
userFeeSet.set(false);
|
||||||
|
@ -271,6 +273,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
FeeRatesSelection feeRatesSelection = Config.get().getFeeRatesSelection();
|
FeeRatesSelection feeRatesSelection = Config.get().getFeeRatesSelection();
|
||||||
feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.BLOCK_TARGET : feeRatesSelection);
|
feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.BLOCK_TARGET : feeRatesSelection);
|
||||||
|
setDefaultFeeRate();
|
||||||
updateFeeRateSelection(feeRatesSelection);
|
updateFeeRateSelection(feeRatesSelection);
|
||||||
feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle);
|
feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle);
|
||||||
feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
feeSelectionToggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
@ -318,15 +321,12 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> {
|
walletTransactionProperty.addListener((observable, oldValue, walletTransaction) -> {
|
||||||
if(walletTransaction != null) {
|
if(walletTransaction != null) {
|
||||||
for(int i = 0; i < paymentTabs.getTabs().size(); i++) {
|
setPayments(walletTransaction.getPayments());
|
||||||
Payment payment = walletTransaction.getPayments().get(i);
|
|
||||||
PaymentController controller = (PaymentController)paymentTabs.getTabs().get(i).getUserData();
|
|
||||||
controller.setPayment(payment);
|
|
||||||
}
|
|
||||||
|
|
||||||
double feeRate = walletTransaction.getFeeRate();
|
double feeRate = walletTransaction.getFeeRate();
|
||||||
if(userFeeSet.get()) {
|
if(userFeeSet.get()) {
|
||||||
setTargetBlocks(getTargetBlocks(feeRate));
|
setTargetBlocks(getTargetBlocks(feeRate));
|
||||||
|
setFeeRangeRate(feeRate);
|
||||||
} else {
|
} else {
|
||||||
setFeeValueSats(walletTransaction.getFee());
|
setFeeValueSats(walletTransaction.getFee());
|
||||||
}
|
}
|
||||||
|
@ -386,7 +386,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
|
|
||||||
public Tab getPaymentTab() {
|
public Tab getPaymentTab() {
|
||||||
Tab tab = new Tab(Integer.toString(paymentTabs.getTabs().size() + 1));
|
OptionalInt highestTabNo = paymentTabs.getTabs().stream().mapToInt(tab -> Integer.parseInt(tab.getText())).max();
|
||||||
|
Tab tab = new Tab(Integer.toString(highestTabNo.isPresent() ? highestTabNo.getAsInt() + 1 : 1));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
FXMLLoader paymentLoader = new FXMLLoader(AppController.class.getResource("wallet/payment.fxml"));
|
FXMLLoader paymentLoader = new FXMLLoader(AppController.class.getResource("wallet/payment.fxml"));
|
||||||
|
@ -411,6 +412,22 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
return payments;
|
return payments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPayments(List<Payment> payments) {
|
||||||
|
while(paymentTabs.getTabs().size() < payments.size()) {
|
||||||
|
addPaymentTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
while(paymentTabs.getTabs().size() > payments.size()) {
|
||||||
|
paymentTabs.getTabs().remove(paymentTabs.getTabs().size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i = 0; i < paymentTabs.getTabs().size(); i++) {
|
||||||
|
Payment payment = payments.get(i);
|
||||||
|
PaymentController controller = (PaymentController)paymentTabs.getTabs().get(i).getUserData();
|
||||||
|
controller.setPayment(payment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void updateTransaction() {
|
public void updateTransaction() {
|
||||||
updateTransaction(null);
|
updateTransaction(null);
|
||||||
}
|
}
|
||||||
|
@ -437,7 +454,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
Integer currentBlockHeight = AppController.getCurrentBlockHeight();
|
Integer currentBlockHeight = AppController.getCurrentBlockHeight();
|
||||||
boolean groupByAddress = Config.get().isGroupByAddress();
|
boolean groupByAddress = Config.get().isGroupByAddress();
|
||||||
boolean includeMempoolChange = Config.get().isIncludeMempoolChange();
|
boolean includeMempoolChange = Config.get().isIncludeMempoolChange();
|
||||||
WalletTransaction walletTransaction = wallet.createWalletTransaction(getUtxoSelectors(), getUtxoFilters(), payments, getFeeRate(), getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolChange);
|
boolean includeMempoolInputs = includeMempoolInputsProperty.get();
|
||||||
|
WalletTransaction walletTransaction = wallet.createWalletTransaction(getUtxoSelectors(), getUtxoFilters(), payments, getFeeRate(), getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolChange, includeMempoolInputs);
|
||||||
walletTransactionProperty.setValue(walletTransaction);
|
walletTransactionProperty.setValue(walletTransaction);
|
||||||
insufficientInputsProperty.set(false);
|
insufficientInputsProperty.set(false);
|
||||||
|
|
||||||
|
@ -477,7 +495,11 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
boolean blockTargetSelection = (feeRatesSelection == FeeRatesSelection.BLOCK_TARGET);
|
boolean blockTargetSelection = (feeRatesSelection == FeeRatesSelection.BLOCK_TARGET);
|
||||||
targetBlocksField.setVisible(blockTargetSelection);
|
targetBlocksField.setVisible(blockTargetSelection);
|
||||||
blockTargetFeeRatesChart.setVisible(blockTargetSelection);
|
blockTargetFeeRatesChart.setVisible(blockTargetSelection);
|
||||||
setDefaultFeeRate();
|
if(blockTargetSelection) {
|
||||||
|
setTargetBlocks(getTargetBlocks(getFeeRangeRate()));
|
||||||
|
} else {
|
||||||
|
setFeeRangeRate(getTargetBlocksFeeRates().get(getTargetBlocks()));
|
||||||
|
}
|
||||||
updateTransaction();
|
updateTransaction();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -485,14 +507,10 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||||
int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget);
|
int index = TARGET_BLOCKS_RANGE.indexOf(defaultTarget);
|
||||||
Double defaultRate = getTargetBlocksFeeRates().get(defaultTarget);
|
Double defaultRate = getTargetBlocksFeeRates().get(defaultTarget);
|
||||||
if(targetBlocksField.isVisible()) {
|
targetBlocks.setValue(index);
|
||||||
targetBlocks.setValue(index);
|
blockTargetFeeRatesChart.select(defaultTarget);
|
||||||
blockTargetFeeRatesChart.select(defaultTarget);
|
setFeeRangeRate(defaultRate);
|
||||||
setFeeRate(defaultRate);
|
setFeeRate(getFeeRangeRate());
|
||||||
} else {
|
|
||||||
feeRange.setValue(Math.log(defaultRate) / Math.log(2));
|
|
||||||
setFeeRate(getFeeRangeRate());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long getFeeValueSats() {
|
private Long getFeeValueSats() {
|
||||||
|
@ -528,7 +546,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
for(Integer targetBlocks : targetBlocksFeeRates.keySet()) {
|
for(Integer targetBlocks : targetBlocksFeeRates.keySet()) {
|
||||||
maxTargetBlocks = Math.max(maxTargetBlocks, targetBlocks);
|
maxTargetBlocks = Math.max(maxTargetBlocks, targetBlocks);
|
||||||
Double candidate = targetBlocksFeeRates.get(targetBlocks);
|
Double candidate = targetBlocksFeeRates.get(targetBlocks);
|
||||||
if(feeRate > candidate) {
|
if(Math.round(feeRate) >= Math.round(candidate)) {
|
||||||
return targetBlocks;
|
return targetBlocks;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -541,6 +559,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
int index = TARGET_BLOCKS_RANGE.indexOf(target);
|
int index = TARGET_BLOCKS_RANGE.indexOf(target);
|
||||||
targetBlocks.setValue(index);
|
targetBlocks.setValue(index);
|
||||||
blockTargetFeeRatesChart.select(target);
|
blockTargetFeeRatesChart.select(target);
|
||||||
|
Tooltip tooltip = new Tooltip("Target inclusion within " + target + " blocks");
|
||||||
|
targetBlocks.setTooltip(tooltip);
|
||||||
targetBlocks.valueProperty().addListener(targetBlocksListener);
|
targetBlocks.valueProperty().addListener(targetBlocksListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -559,6 +579,12 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
return Math.pow(2.0, feeRange.getValue());
|
return Math.pow(2.0, feeRange.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setFeeRangeRate(Double feeRate) {
|
||||||
|
feeRange.valueProperty().removeListener(feeRangeListener);
|
||||||
|
feeRange.setValue(Math.log(feeRate) / Math.log(2));
|
||||||
|
feeRange.valueProperty().addListener(feeRangeListener);
|
||||||
|
}
|
||||||
|
|
||||||
public Double getFeeRate() {
|
public Double getFeeRate() {
|
||||||
if(targetBlocksField.isVisible()) {
|
if(targetBlocksField.isVisible()) {
|
||||||
return getTargetBlocksFeeRates().get(getTargetBlocks());
|
return getTargetBlocksFeeRates().get(getTargetBlocks());
|
||||||
|
@ -631,9 +657,10 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
fiatFeeAmount.setText("");
|
fiatFeeAmount.setText("");
|
||||||
|
|
||||||
userFeeSet.set(false);
|
userFeeSet.set(false);
|
||||||
targetBlocks.setValue(4);
|
setDefaultFeeRate();
|
||||||
utxoSelectorProperty.setValue(null);
|
utxoSelectorProperty.setValue(null);
|
||||||
utxoFilterProperty.setValue(null);
|
utxoFilterProperty.setValue(null);
|
||||||
|
includeMempoolInputsProperty.set(false);
|
||||||
walletTransactionProperty.setValue(null);
|
walletTransactionProperty.setValue(null);
|
||||||
createdWalletTransactionProperty.set(null);
|
createdWalletTransactionProperty.set(null);
|
||||||
|
|
||||||
|
@ -779,11 +806,23 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void spendUtxos(SpendUtxoEvent event) {
|
public void spendUtxos(SpendUtxoEvent event) {
|
||||||
if(!event.getUtxoEntries().isEmpty() && event.getUtxoEntries().get(0).getWallet().equals(getWalletForm().getWallet())) {
|
if(!event.getUtxos().isEmpty() && event.getWallet().equals(getWalletForm().getWallet())) {
|
||||||
List<BlockTransactionHashIndex> utxos = event.getUtxoEntries().stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList());
|
if(event.getPayments() != null) {
|
||||||
|
clear(null);
|
||||||
|
setPayments(event.getPayments());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(event.getFee() != null) {
|
||||||
|
setFeeValueSats(event.getFee());
|
||||||
|
userFeeSet.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
includeMempoolInputsProperty.set(event.isIncludeMempoolInputs());
|
||||||
|
|
||||||
|
List<BlockTransactionHashIndex> utxos = event.getUtxos();
|
||||||
utxoSelectorProperty.set(new PresetUtxoSelector(utxos));
|
utxoSelectorProperty.set(new PresetUtxoSelector(utxos));
|
||||||
utxoFilterProperty.set(null);
|
utxoFilterProperty.set(null);
|
||||||
updateTransaction(true);
|
updateTransaction(event.getPayments() == null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -138,9 +138,7 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
TransactionEntry that = (TransactionEntry) o;
|
TransactionEntry that = (TransactionEntry) o;
|
||||||
// Even though the txid identifies a transaction, receiving an incomplete set of script hash notifications can result in some inputs/outputs for a tx being missing.
|
return getWallet().equals(that.getWallet()) && blockTransaction.equals(that.blockTransaction);
|
||||||
// To resolve this we check the number of children, but not the children themselves (since we don't care here when they are spent)
|
|
||||||
return getWallet().equals(that.getWallet()) && blockTransaction.equals(that.blockTransaction) && getChildren().size() == that.getChildren().size();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.wallet;
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
|
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.control.CoinLabel;
|
import com.sparrowwallet.sparrow.control.CoinLabel;
|
||||||
import com.sparrowwallet.sparrow.control.UtxosChart;
|
import com.sparrowwallet.sparrow.control.UtxosChart;
|
||||||
|
@ -81,8 +82,9 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.isSpendable())
|
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.isSpendable())
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
EventManager.get().post(new SendActionEvent(utxoEntries));
|
final List<BlockTransactionHashIndex> spendingUtxos = utxoEntries.stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList());
|
||||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(utxoEntries)));
|
EventManager.get().post(new SendActionEvent(getWalletForm().getWallet(), spendingUtxos));
|
||||||
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(getWalletForm().getWallet(), spendingUtxos)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear(ActionEvent event) {
|
public void clear(ActionEvent event) {
|
||||||
|
|
|
@ -121,7 +121,7 @@ public class WalletController extends WalletFormController implements Initializa
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void sendAction(SendActionEvent event) {
|
public void sendAction(SendActionEvent event) {
|
||||||
if(!event.getUtxoEntries().isEmpty() && event.getUtxoEntries().get(0).getWallet().equals(walletForm.getWallet())) {
|
if(!event.getUtxos().isEmpty() && event.getWallet().equals(walletForm.getWallet())) {
|
||||||
selectFunction(Function.SEND);
|
selectFunction(Function.SEND);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,8 @@ public class WalletTransactionsEntry extends Entry {
|
||||||
entriesAdded.removeAll(entriesComplete);
|
entriesAdded.removeAll(entriesComplete);
|
||||||
for(Entry entry : entriesAdded) {
|
for(Entry entry : entriesAdded) {
|
||||||
TransactionEntry txEntry = (TransactionEntry)entry;
|
TransactionEntry txEntry = (TransactionEntry)entry;
|
||||||
log.warn("Not notifying for incomplete entry " + ((TransactionEntry)entry).getBlockTransaction().getHashAsString() + " value " + txEntry.getValue());
|
getChildren().remove(txEntry);
|
||||||
|
log.warn("Removing and not notifying incomplete entry " + ((TransactionEntry)entry).getBlockTransaction().getHashAsString() + " value " + txEntry.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Loading…
Reference in a new issue