From cd005352128d5900b3894065653379d9105a306e Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Fri, 23 Apr 2021 12:05:30 +0200 Subject: [PATCH] add create cpfp transaction functionality --- .../sparrow/control/EntryCell.java | 54 ++++++++++++++++++- .../sparrow/glyphfont/FontAwesome5.java | 2 + .../sparrow/wallet/SendController.java | 30 +++++++++-- .../sparrowwallet/sparrow/wallet/send.fxml | 8 +++ 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java index 9dd2e461..4a29111d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java @@ -92,6 +92,17 @@ public class EntryCell extends TreeTableCell { 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); } else if(entry instanceof NodeEntry) { NodeEntry nodeEntry = (NodeEntry)entry; @@ -245,6 +256,37 @@ public class EntryCell extends TreeTableCell { return AppServices.getTargetBlockFeeRates().values().iterator().next(); } + private static void createCpfp(TransactionEntry transactionEntry) { + BlockTransaction blockTransaction = transactionEntry.getBlockTransaction(); + List 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 treeTableView, HashIndexEntry hashIndexEntry) { List utxoEntries = treeTableView.getSelectionModel().getSelectedCells().stream() .map(tp -> tp.getTreeItem().getValue()) @@ -286,7 +328,7 @@ public class EntryCell extends TreeTableCell { getItems().add(copyTxid); 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 -> { hide(); increaseFee(transactionEntry); @@ -294,6 +336,16 @@ public class EntryCell extends TreeTableCell { getItems().add(increaseFee); } + + if(containsWalletOutputs(transactionEntry)) { + MenuItem createCpfp = new MenuItem("Increase Effective Fee (CPFP)"); + createCpfp.setOnAction(AE -> { + hide(); + createCpfp(transactionEntry); + }); + + getItems().add(createCpfp); + } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index b1a8bd75..a83742c4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -48,12 +48,14 @@ public class FontAwesome5 extends GlyphFont { SATELLITE_DISH('\uf7c0'), SD_CARD('\uf7c2'), SEARCH('\uf002'), + SIGN_OUT_ALT('\uf2f5'), SQUARE('\uf0c8'), TIMES_CIRCLE('\uf057'), TOGGLE_OFF('\uf204'), TOGGLE_ON('\uf205'), TOOLS('\uf7d9'), UNDO('\uf0e2'), + USER_FRIENDS('\uf500'), WALLET('\uf555'); private final char ch; diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 2c8f3eca..c6722369 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -81,6 +81,9 @@ public class SendController extends WalletFormController implements Initializabl @FXML private CopyableLabel feeRate; + @FXML + private Label cpfpFeeRate; + @FXML private Label feeRatePriority; @@ -281,6 +284,8 @@ public class SendController extends WalletFormController implements Initializabl FeeRatesSelection feeRatesSelection = Config.get().getFeeRatesSelection(); feeRatesSelection = (feeRatesSelection == null ? FeeRatesSelection.MEMPOOL_SIZE : feeRatesSelection); + cpfpFeeRate.managedProperty().bind(cpfpFeeRate.visibleProperty()); + cpfpFeeRate.setVisible(false); setDefaultFeeRate(); updateFeeRateSelection(feeRatesSelection); feeSelectionToggleGroup.selectToggle(feeRatesSelection == FeeRatesSelection.BLOCK_TARGET ? targetBlocksToggle : mempoolSizeToggle); @@ -346,6 +351,7 @@ public class SendController extends WalletFormController implements Initializabl } setFeeRate(feeRate); + setEffectiveFeeRate(walletTransaction); } transactionDiagram.update(walletTransaction); @@ -354,7 +360,7 @@ public class SendController extends WalletFormController implements Initializabl transactionDiagram.sceneProperty().addListener((observable, oldScene, newScene) -> { if(oldScene == null && newScene != null) { - transactionDiagram.update(null); + transactionDiagram.update(walletTransactionProperty.get()); newScene.getWindow().heightProperty().addListener((observable1, oldValue, newValue) -> { transactionDiagram.update(walletTransactionProperty.get()); }); @@ -636,10 +642,28 @@ public class SendController extends WalletFormController implements Initializabl } private void setFeeRate(Double feeRateAmt) { - feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vByte"); + feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vB"); setFeeRatePriority(feeRateAmt); } + private void setEffectiveFeeRate(WalletTransaction walletTransaction) { + List 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) { Map targetBlocksFeeRates = getTargetBlocksFeeRates(); Integer targetBlocks = getTargetBlocks(feeRateAmt); @@ -961,7 +985,7 @@ public class SendController extends WalletFormController implements Initializabl List utxos = event.getUtxos(); utxoSelectorProperty.set(new PresetUtxoSelector(utxos)); utxoFilterProperty.set(null); - updateTransaction(event.getPayments() == null); + updateTransaction(event.getPayments() == null || event.getPayments().stream().anyMatch(Payment::isSendMax)); } } diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml index 7e7a7765..89bb01ae 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/send.fxml @@ -90,6 +90,14 @@ +