mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
add create cpfp transaction functionality
This commit is contained in:
parent
3ff626e2aa
commit
cd00535212
4 changed files with 90 additions and 4 deletions
|
@ -92,6 +92,17 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
actionBox.getChildren().add(increaseFeeButton);
|
actionBox.getChildren().add(increaseFeeButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(blockTransaction.getHeight() <= 0 && containsWalletOutputs(transactionEntry)) {
|
||||||
|
Button cpfpButton = new Button("");
|
||||||
|
Glyph cpfpGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SIGN_OUT_ALT);
|
||||||
|
cpfpGlyph.setFontSize(12);
|
||||||
|
cpfpButton.setGraphic(cpfpGlyph);
|
||||||
|
cpfpButton.setOnAction(event -> {
|
||||||
|
createCpfp(transactionEntry);
|
||||||
|
});
|
||||||
|
actionBox.getChildren().add(cpfpButton);
|
||||||
|
}
|
||||||
|
|
||||||
setGraphic(actionBox);
|
setGraphic(actionBox);
|
||||||
} else if(entry instanceof NodeEntry) {
|
} else if(entry instanceof NodeEntry) {
|
||||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||||
|
@ -245,6 +256,37 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
return AppServices.getTargetBlockFeeRates().values().iterator().next();
|
return AppServices.getTargetBlockFeeRates().values().iterator().next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void createCpfp(TransactionEntry transactionEntry) {
|
||||||
|
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||||
|
List<BlockTransactionHashIndex> ourOutputs = transactionEntry.getChildren().stream()
|
||||||
|
.filter(e -> e instanceof HashIndexEntry)
|
||||||
|
.map(e -> (HashIndexEntry)e)
|
||||||
|
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT))
|
||||||
|
.map(HashIndexEntry::getHashIndex)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if(ourOutputs.isEmpty()) {
|
||||||
|
throw new IllegalStateException("Cannot create CPFP without any wallet outputs to spend");
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockTransactionHashIndex utxo = ourOutputs.get(0);
|
||||||
|
|
||||||
|
WalletNode freshNode = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE);
|
||||||
|
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
|
||||||
|
label += (label.isEmpty() ? "" : " ") + "(CPFP)";
|
||||||
|
Payment payment = new Payment(transactionEntry.getWallet().getAddress(freshNode), label, utxo.getValue(), true);
|
||||||
|
|
||||||
|
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), List.of(utxo)));
|
||||||
|
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
|
||||||
|
return transactionEntry.getChildren().stream()
|
||||||
|
.filter(e -> e instanceof HashIndexEntry)
|
||||||
|
.map(e -> (HashIndexEntry)e)
|
||||||
|
.anyMatch(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT));
|
||||||
|
}
|
||||||
|
|
||||||
private static void sendSelectedUtxos(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
private static void sendSelectedUtxos(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
||||||
List<HashIndexEntry> utxoEntries = treeTableView.getSelectionModel().getSelectedCells().stream()
|
List<HashIndexEntry> utxoEntries = treeTableView.getSelectionModel().getSelectedCells().stream()
|
||||||
.map(tp -> tp.getTreeItem().getValue())
|
.map(tp -> tp.getTreeItem().getValue())
|
||||||
|
@ -286,7 +328,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
getItems().add(copyTxid);
|
getItems().add(copyTxid);
|
||||||
|
|
||||||
if(blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
if(blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||||
MenuItem increaseFee = new MenuItem("Increase Fee");
|
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
||||||
increaseFee.setOnAction(AE -> {
|
increaseFee.setOnAction(AE -> {
|
||||||
hide();
|
hide();
|
||||||
increaseFee(transactionEntry);
|
increaseFee(transactionEntry);
|
||||||
|
@ -294,6 +336,16 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||||
|
|
||||||
getItems().add(increaseFee);
|
getItems().add(increaseFee);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(containsWalletOutputs(transactionEntry)) {
|
||||||
|
MenuItem createCpfp = new MenuItem("Increase Effective Fee (CPFP)");
|
||||||
|
createCpfp.setOnAction(AE -> {
|
||||||
|
hide();
|
||||||
|
createCpfp(transactionEntry);
|
||||||
|
});
|
||||||
|
|
||||||
|
getItems().add(createCpfp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -48,12 +48,14 @@ public class FontAwesome5 extends GlyphFont {
|
||||||
SATELLITE_DISH('\uf7c0'),
|
SATELLITE_DISH('\uf7c0'),
|
||||||
SD_CARD('\uf7c2'),
|
SD_CARD('\uf7c2'),
|
||||||
SEARCH('\uf002'),
|
SEARCH('\uf002'),
|
||||||
|
SIGN_OUT_ALT('\uf2f5'),
|
||||||
SQUARE('\uf0c8'),
|
SQUARE('\uf0c8'),
|
||||||
TIMES_CIRCLE('\uf057'),
|
TIMES_CIRCLE('\uf057'),
|
||||||
TOGGLE_OFF('\uf204'),
|
TOGGLE_OFF('\uf204'),
|
||||||
TOGGLE_ON('\uf205'),
|
TOGGLE_ON('\uf205'),
|
||||||
TOOLS('\uf7d9'),
|
TOOLS('\uf7d9'),
|
||||||
UNDO('\uf0e2'),
|
UNDO('\uf0e2'),
|
||||||
|
USER_FRIENDS('\uf500'),
|
||||||
WALLET('\uf555');
|
WALLET('\uf555');
|
||||||
|
|
||||||
private final char ch;
|
private final char ch;
|
||||||
|
|
|
@ -81,6 +81,9 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
@FXML
|
@FXML
|
||||||
private CopyableLabel feeRate;
|
private CopyableLabel feeRate;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label cpfpFeeRate;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Label feeRatePriority;
|
private Label feeRatePriority;
|
||||||
|
|
||||||
|
@ -281,6 +284,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
FeeRatesSelection feeRatesSelection = Config.get().getFeeRatesSelection();
|
FeeRatesSelection feeRatesSelection = Config.get().getFeeRatesSelection();
|
||||||
feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.MEMPOOL_SIZE : feeRatesSelection);
|
feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.MEMPOOL_SIZE : feeRatesSelection);
|
||||||
|
cpfpFeeRate.managedProperty().bind(cpfpFeeRate.visibleProperty());
|
||||||
|
cpfpFeeRate.setVisible(false);
|
||||||
setDefaultFeeRate();
|
setDefaultFeeRate();
|
||||||
updateFeeRateSelection(feeRatesSelection);
|
updateFeeRateSelection(feeRatesSelection);
|
||||||
feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle);
|
feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle);
|
||||||
|
@ -346,6 +351,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
|
|
||||||
setFeeRate(feeRate);
|
setFeeRate(feeRate);
|
||||||
|
setEffectiveFeeRate(walletTransaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionDiagram.update(walletTransaction);
|
transactionDiagram.update(walletTransaction);
|
||||||
|
@ -354,7 +360,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
|
|
||||||
transactionDiagram.sceneProperty().addListener((observable, oldScene, newScene) -> {
|
transactionDiagram.sceneProperty().addListener((observable, oldScene, newScene) -> {
|
||||||
if(oldScene == null && newScene != null) {
|
if(oldScene == null && newScene != null) {
|
||||||
transactionDiagram.update(null);
|
transactionDiagram.update(walletTransactionProperty.get());
|
||||||
newScene.getWindow().heightProperty().addListener((observable1, oldValue, newValue) -> {
|
newScene.getWindow().heightProperty().addListener((observable1, oldValue, newValue) -> {
|
||||||
transactionDiagram.update(walletTransactionProperty.get());
|
transactionDiagram.update(walletTransactionProperty.get());
|
||||||
});
|
});
|
||||||
|
@ -636,10 +642,28 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setFeeRate(Double feeRateAmt) {
|
private void setFeeRate(Double feeRateAmt) {
|
||||||
feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vByte");
|
feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vB");
|
||||||
setFeeRatePriority(feeRateAmt);
|
setFeeRatePriority(feeRateAmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setEffectiveFeeRate(WalletTransaction walletTransaction) {
|
||||||
|
List<BlockTransaction> unconfirmedUtxoTxs = walletTransaction.getSelectedUtxos().keySet().stream().filter(ref -> ref.getHeight() <= 0)
|
||||||
|
.map(ref -> getWalletForm().getWallet().getTransactions().get(ref.getHash())).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||||
|
if(!unconfirmedUtxoTxs.isEmpty()) {
|
||||||
|
cpfpFeeRate.setVisible(true);
|
||||||
|
long utxoTxFee = unconfirmedUtxoTxs.stream().mapToLong(BlockTransaction::getFee).sum();
|
||||||
|
double utxoTxSize = unconfirmedUtxoTxs.stream().mapToDouble(blkTx -> blkTx.getTransaction().getVirtualSize()).sum();
|
||||||
|
long thisFee = walletTransaction.getFee();
|
||||||
|
double thisSize = walletTransaction.getTransaction().getVirtualSize();
|
||||||
|
double effectiveRate = (utxoTxFee + thisFee) / (utxoTxSize + thisSize);
|
||||||
|
Tooltip tooltip = new Tooltip(String.format("%.2f", effectiveRate) + " sats/vB effective rate");
|
||||||
|
cpfpFeeRate.setTooltip(tooltip);
|
||||||
|
cpfpFeeRate.setVisible(true);
|
||||||
|
} else {
|
||||||
|
cpfpFeeRate.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void setFeeRatePriority(Double feeRateAmt) {
|
private void setFeeRatePriority(Double feeRateAmt) {
|
||||||
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
|
Map<Integer, Double> targetBlocksFeeRates = getTargetBlocksFeeRates();
|
||||||
Integer targetBlocks = getTargetBlocks(feeRateAmt);
|
Integer targetBlocks = getTargetBlocks(feeRateAmt);
|
||||||
|
@ -961,7 +985,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||||
List<BlockTransactionHashIndex> utxos = event.getUtxos();
|
List<BlockTransactionHashIndex> utxos = event.getUtxos();
|
||||||
utxoSelectorProperty.set(new PresetUtxoSelector(utxos));
|
utxoSelectorProperty.set(new PresetUtxoSelector(utxos));
|
||||||
utxoFilterProperty.set(null);
|
utxoFilterProperty.set(null);
|
||||||
updateTransaction(event.getPayments() == null);
|
updateTransaction(event.getPayments() == null || event.getPayments().stream().anyMatch(Payment::isSendMax));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,6 +90,14 @@
|
||||||
</Field>
|
</Field>
|
||||||
<Field fx:id="feeRateField" text="Rate:">
|
<Field fx:id="feeRateField" text="Rate:">
|
||||||
<CopyableLabel fx:id="feeRate" />
|
<CopyableLabel fx:id="feeRate" />
|
||||||
|
<Label fx:id="cpfpFeeRate" text="CPFP">
|
||||||
|
<graphic>
|
||||||
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="SIGN_OUT_ALT" />
|
||||||
|
</graphic>
|
||||||
|
<padding>
|
||||||
|
<Insets left="10"/>
|
||||||
|
</padding>
|
||||||
|
</Label>
|
||||||
<Region HBox.hgrow="ALWAYS" />
|
<Region HBox.hgrow="ALWAYS" />
|
||||||
<Label fx:id="feeRatePriority" graphicTextGap="5" contentDisplay="RIGHT">
|
<Label fx:id="feeRatePriority" graphicTextGap="5" contentDisplay="RIGHT">
|
||||||
<graphic>
|
<graphic>
|
||||||
|
|
Loading…
Reference in a new issue