From b1d22af1ae357c926ef2cd0b8692c880d6263de1 Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Fri, 4 Dec 2020 13:13:13 -0600 Subject: [PATCH 1/2] Privacy improvements for manual payout Redesign the UI Add import/export of payout settings Add ability to import from mediation ticket Mediator does not need private key User can sign using own wallet or private key Validation of input fields Calculate the tx fee based on inputs Display of the generated txid & hex so it can be checked --- .../core/btc/wallet/TradeWalletService.java | 74 +- .../windows/ManualPayoutTxWindow.java | 689 ++++++++++++++++-- 2 files changed, 669 insertions(+), 94 deletions(-) diff --git a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java index 19049e863cb..04188aee71b 100644 --- a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java @@ -30,6 +30,7 @@ import bisq.core.user.Preferences; import bisq.common.config.Config; +import bisq.common.util.Tuple2; import org.bitcoinj.core.Address; import org.bitcoinj.core.AddressFormatException; @@ -1094,24 +1095,21 @@ public Transaction traderSignAndFinalizeDisputedPayoutTx(byte[] depositTxSeriali // Emergency payoutTx /////////////////////////////////////////////////////////////////////////////////////////// - public void emergencySignAndPublishPayoutTxFrom2of2MultiSig(String depositTxHex, - Coin buyerPayoutAmount, - Coin sellerPayoutAmount, - Coin txFee, - String buyerAddressString, - String sellerAddressString, - String buyerPrivateKeyAsHex, - String sellerPrivateKeyAsHex, - String buyerPubKeyAsHex, - String sellerPubKeyAsHex, - boolean hashedMultiSigOutputIsLegacy, - TxBroadcaster.Callback callback) - throws AddressFormatException, TransactionVerificationException, WalletException { + public Tuple2 emergencyBuildPayoutTxFrom2of2MultiSig(String depositTxHex, + Coin buyerPayoutAmount, + Coin sellerPayoutAmount, + Coin txFee, + String buyerAddressString, + String sellerAddressString, + String buyerPubKeyAsHex, + String sellerPubKeyAsHex, + boolean hashedMultiSigOutputIsLegacy) { byte[] buyerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(buyerPubKeyAsHex)).getPubKey(); byte[] sellerPubKey = ECKey.fromPublicOnly(Utils.HEX.decode(sellerPubKeyAsHex)).getPubKey(); + Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); Script hashedMultiSigOutputScript = get2of2MultiSigOutputScript(buyerPubKey, sellerPubKey, - hashedMultiSigOutputIsLegacy); + hashedMultiSigOutputIsLegacy); Coin msOutputValue = buyerPayoutAmount.add(sellerPayoutAmount).add(txFee); TransactionOutput hashedMultiSigOutput = new TransactionOutput(params, null, msOutputValue, hashedMultiSigOutputScript.getProgram()); @@ -1129,27 +1127,44 @@ public void emergencySignAndPublishPayoutTxFrom2of2MultiSig(String depositTxHex, payoutTx.addOutput(sellerPayoutAmount, Address.fromString(params, sellerAddressString)); } - // take care of sorting! - Script redeemScript = get2of2MultiSigRedeemScript(buyerPubKey, sellerPubKey); + String redeemScriptHex = Utils.HEX.encode(redeemScript.getProgram()); + String unsignedTxHex = Utils.HEX.encode(payoutTx.bitcoinSerialize(!hashedMultiSigOutputIsLegacy)); + return new Tuple2<>(redeemScriptHex, unsignedTxHex); + } + + public String emergencyGenerateSignature(String rawTxHex, String redeemScriptHex, Coin inputValue, String myPrivKeyAsHex) + throws IllegalArgumentException { + boolean hashedMultiSigOutputIsLegacy = true; + if (rawTxHex.startsWith("010000000001")) + hashedMultiSigOutputIsLegacy = false; + byte[] payload = Utils.HEX.decode(rawTxHex); + Transaction payoutTx = new Transaction(params, payload, null, params.getDefaultSerializer(), payload.length); + Script redeemScript = new Script(Utils.HEX.decode(redeemScriptHex)); Sha256Hash sigHash; if (hashedMultiSigOutputIsLegacy) { sigHash = payoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); } else { - Coin inputValue = msOutputValue; sigHash = payoutTx.hashForWitnessSignature(0, redeemScript, inputValue, Transaction.SigHash.ALL, false); } - ECKey buyerPrivateKey = ECKey.fromPrivate(Utils.HEX.decode(buyerPrivateKeyAsHex)); - checkNotNull(buyerPrivateKey, "key must not be null"); - ECKey.ECDSASignature buyerECDSASignature = buyerPrivateKey.sign(sigHash, aesKey).toCanonicalised(); - - ECKey sellerPrivateKey = ECKey.fromPrivate(Utils.HEX.decode(sellerPrivateKeyAsHex)); - checkNotNull(sellerPrivateKey, "key must not be null"); - ECKey.ECDSASignature sellerECDSASignature = sellerPrivateKey.sign(sigHash, aesKey).toCanonicalised(); + ECKey myPrivateKey = ECKey.fromPrivate(Utils.HEX.decode(myPrivKeyAsHex)); + checkNotNull(myPrivateKey, "key must not be null"); + ECKey.ECDSASignature myECDSASignature = myPrivateKey.sign(sigHash, aesKey).toCanonicalised(); + TransactionSignature myTxSig = new TransactionSignature(myECDSASignature, Transaction.SigHash.ALL, false); + return Utils.HEX.encode(myTxSig.encodeToBitcoin()); + } - TransactionSignature buyerTxSig = new TransactionSignature(buyerECDSASignature, Transaction.SigHash.ALL, false); - TransactionSignature sellerTxSig = new TransactionSignature(sellerECDSASignature, Transaction.SigHash.ALL, false); + public Tuple2 emergencyApplySignatureToPayoutTxFrom2of2MultiSig(String unsignedTxHex, + String redeemScriptHex, + String buyerSignatureAsHex, + String sellerSignatureAsHex, + boolean hashedMultiSigOutputIsLegacy) + throws AddressFormatException, SignatureDecodeException { + Transaction payoutTx = new Transaction(params, Utils.HEX.decode(unsignedTxHex)); + TransactionSignature buyerTxSig = TransactionSignature.decodeFromBitcoin(Utils.HEX.decode(buyerSignatureAsHex), true, true); + TransactionSignature sellerTxSig = TransactionSignature.decodeFromBitcoin(Utils.HEX.decode(sellerSignatureAsHex), true, true); + Script redeemScript = new Script(Utils.HEX.decode(redeemScriptHex)); TransactionInput input = payoutTx.getInput(0); if (hashedMultiSigOutputIsLegacy) { @@ -1161,7 +1176,14 @@ public void emergencySignAndPublishPayoutTxFrom2of2MultiSig(String depositTxHex, TransactionWitness witness = TransactionWitness.redeemP2WSH(redeemScript, sellerTxSig, buyerTxSig); input.setWitness(witness); } + String txId = payoutTx.getTxId().toString(); + String signedTxHex = Utils.HEX.encode(payoutTx.bitcoinSerialize(!hashedMultiSigOutputIsLegacy)); + return new Tuple2<>(txId, signedTxHex); + } + public void emergencyPublishPayoutTxFrom2of2MultiSig(String signedTxHex, TxBroadcaster.Callback callback) + throws AddressFormatException, TransactionVerificationException, WalletException { + Transaction payoutTx = new Transaction(params, Utils.HEX.decode(signedTxHex)); WalletService.printTx("payoutTx", payoutTx); WalletService.verifyTransaction(payoutTx); WalletService.checkWalletConsistency(wallet); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java index 609c526fdec..5e40b22ea24 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java @@ -17,10 +17,13 @@ package bisq.desktop.main.overlays.windows; +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.BisqTextArea; import bisq.desktop.components.InputTextField; import bisq.desktop.main.overlays.Overlay; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.validation.LengthValidator; import bisq.core.btc.exceptions.TransactionVerificationException; import bisq.core.btc.exceptions.TxBroadcastException; @@ -28,46 +31,125 @@ import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.locale.Res; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.user.BlockChainExplorer; +import bisq.core.user.Preferences; import bisq.network.p2p.P2PService; import bisq.common.UserThread; +import bisq.common.util.Base64; +import bisq.common.util.Tuple2; +import bisq.common.util.Utilities; import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.Coin; +import org.bitcoinj.core.SignatureDecodeException; import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.Utils; +import org.bitcoinj.core.VerificationException; import javax.inject.Inject; +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; + import javafx.scene.Scene; +import javafx.scene.control.Button; import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.control.TextArea; +import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCode; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; + +import javafx.beans.value.ChangeListener; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.time.Instant; + +import java.nio.charset.Charset; + +import java.util.ArrayList; +import java.util.Optional; +import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; -import static bisq.desktop.util.FormBuilder.addCheckBox; -import static bisq.desktop.util.FormBuilder.addInputTextField; +import static bisq.desktop.util.FormBuilder.*; // We don't translate here as it is for dev only purpose public class ManualPayoutTxWindow extends Overlay { + private static final int HEX_HASH_LENGTH = 32 * 2; + private static final int HEX_PUBKEY_LENGTH = 33 * 2; private static final Logger log = LoggerFactory.getLogger(ManualPayoutTxWindow.class); private final TradeWalletService tradeWalletService; private final P2PService p2PService; + private final MediationManager mediationManager; + private final Preferences preferences; private final WalletsSetup walletsSetup; - + private final WalletsManager walletsManager; + GridPane inputsGridPane; + GridPane importTxGridPane; + GridPane exportTxGridPane; + GridPane signTxGridPane; + GridPane buildTxGridPane; + CheckBox depositTxLegacy, recentTickets; + ComboBox mediationDropDown; + ObservableList disputeObservableList; + Label blockExplorerIcon, copyIcon; + InputTextField depositTxHex; + InputTextField amountInMultisig; + InputTextField buyerPayoutAmount; + InputTextField sellerPayoutAmount; + InputTextField txFee; + InputTextField buyerAddressString; + InputTextField sellerAddressString; + InputTextField buyerPubKeyAsHex; + InputTextField sellerPubKeyAsHex; + InputTextField buyerSignatureAsHex; + InputTextField sellerSignatureAsHex; + InputTextField privateKeyHex; + InputTextField signatureHex; + TextArea importHex; + TextArea exportHex; + TextArea finalSignedTxHex; + private ChangeListener txFeeListener, amountInMultisigListener, buyerPayoutAmountListener, sellerPayoutAmountListener; /////////////////////////////////////////////////////////////////////////////////////////// // Public API /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public ManualPayoutTxWindow(TradeWalletService tradeWalletService, P2PService p2PService, WalletsSetup walletsSetup) { + public ManualPayoutTxWindow(TradeWalletService tradeWalletService, + P2PService p2PService, + MediationManager mediationManager, + Preferences preferences, + WalletsSetup walletsSetup, + WalletsManager walletsManager) { this.tradeWalletService = tradeWalletService; this.p2PService = p2PService; + this.mediationManager = mediationManager; + this.preferences = preferences; this.walletsSetup = walletsSetup; + this.walletsManager = walletsManager; type = Type.Attention; } @@ -81,6 +163,22 @@ public void show() { addContent(); addButtons(); applyStyles(); + txFeeListener = (observable, oldValue, newValue) -> { + calculateTxFee(); + }; + buyerPayoutAmountListener = (observable, oldValue, newValue) -> { + calculateTxFee(); + }; + sellerPayoutAmountListener = (observable, oldValue, newValue) -> { + calculateTxFee(); + }; + amountInMultisigListener = (observable, oldValue, newValue) -> { + calculateTxFee(); + }; + txFee.focusedProperty().addListener(txFeeListener); + buyerPayoutAmount.focusedProperty().addListener(buyerPayoutAmountListener); + sellerPayoutAmount.focusedProperty().addListener(sellerPayoutAmountListener); + amountInMultisig.focusedProperty().addListener(amountInMultisigListener); display(); } @@ -100,25 +198,42 @@ protected void setupKeyHandler(Scene scene) { } } - private void addContent() { - gridPane.getColumnConstraints().remove(1); - // We dont translate here as it is for dev only purpose - InputTextField depositTxHex = addInputTextField(gridPane, ++rowIndex, "depositTxHex"); - - InputTextField buyerPayoutAmount = addInputTextField(gridPane, ++rowIndex, "buyerPayoutAmount"); - InputTextField sellerPayoutAmount = addInputTextField(gridPane, ++rowIndex, "sellerPayoutAmount"); - InputTextField txFee = addInputTextField(gridPane, ++rowIndex, "Tx fee"); - - InputTextField buyerAddressString = addInputTextField(gridPane, ++rowIndex, "buyerAddressString"); - InputTextField sellerAddressString = addInputTextField(gridPane, ++rowIndex, "sellerAddressString"); - - InputTextField buyerPrivateKeyAsHex = addInputTextField(gridPane, ++rowIndex, "buyerPrivateKeyAsHex"); - InputTextField sellerPrivateKeyAsHex = addInputTextField(gridPane, ++rowIndex, "sellerPrivateKeyAsHex"); + @Override + protected void createGridPane() { + gridPane = new GridPane(); + gridPane.setHgap(15); + gridPane.setVgap(15); + gridPane.setPadding(new Insets(64, 64, 64, 64)); + gridPane.setPrefWidth(width); + ColumnConstraints columnConstraints1 = new ColumnConstraints(); + ColumnConstraints columnConstraints2 = new ColumnConstraints(); + columnConstraints1.setPercentWidth(25); + columnConstraints2.setPercentWidth(75); + gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); + } - InputTextField buyerPubKeyAsHex = addInputTextField(gridPane, ++rowIndex, "buyerPubKeyAsHex"); - InputTextField sellerPubKeyAsHex = addInputTextField(gridPane, ++rowIndex, "sellerPubKeyAsHex"); + @Override + protected void cleanup() { + blockExplorerIcon.setOnMouseClicked(null); + copyIcon.setOnMouseClicked(null); + txFee.focusedProperty().removeListener(txFeeListener); + buyerPayoutAmount.focusedProperty().removeListener(buyerPayoutAmountListener); + sellerPayoutAmount.focusedProperty().removeListener(sellerPayoutAmountListener); + amountInMultisig.focusedProperty().removeListener(amountInMultisigListener); + super.cleanup(); + } - CheckBox depositTxLegacy = addCheckBox(gridPane, ++rowIndex, "depositTxLegacy"); + private void addContent() { + rowIndex = 1; + this.disableActionButton = true; + addLeftPanelButtons(); + addInputsPane(); + addImportPane(); + addExportPane(); + addSignPane(); + addBuildPane(); + hideAllPanes(); + inputsGridPane.setVisible(true); // Notes: // Open with alt+g @@ -126,68 +241,506 @@ private void addContent() { // Take missing buyerPubKeyAsHex and sellerPubKeyAsHex from contract data! // Lookup sellerPrivateKeyAsHex associated with sellerPubKeyAsHex (or buyers) in wallet details data // sellerPubKeys/buyerPubKeys are auto generated if used the fields below + } - depositTxHex.setText(""); + private void addLeftPanelButtons() { + Button buttonInputs = new AutoTooltipButton("Inputs"); + Button buttonImport = new AutoTooltipButton("Import"); + Button buttonExport = new AutoTooltipButton("Export"); + Button buttonSign = new AutoTooltipButton("Sign"); + Button buttonBuild = new AutoTooltipButton("Build"); + VBox vBox = new VBox(12, buttonInputs, buttonImport, buttonExport, buttonSign, buttonBuild); + vBox.getChildren().forEach(button -> ((Button) button).setPrefWidth(500)); + gridPane.add(vBox, 0, rowIndex); + buttonInputs.getStyleClass().add("action-button"); + buttonInputs.setOnAction(e -> { // just show the inputs pane + hideAllPanes(); + vBox.getChildren().forEach(button -> button.getStyleClass().remove("action-button")); + buttonInputs.getStyleClass().add("action-button"); + inputsGridPane.setVisible(true); + }); + buttonImport.setOnAction(e -> { // just show the import pane + hideAllPanes(); + vBox.getChildren().forEach(button -> button.getStyleClass().remove("action-button")); + buttonImport.getStyleClass().add("action-button"); + importTxGridPane.setVisible(true); + importHex.setText(""); + }); + buttonExport.setOnAction(e -> { // show export pane and fill in the data + hideAllPanes(); + vBox.getChildren().forEach(button -> button.getStyleClass().remove("action-button")); + buttonExport.getStyleClass().add("action-button"); + exportTxGridPane.setVisible(true); + exportHex.setText(generateExportText()); + }); + buttonSign.setOnAction(e -> { // just show the sign pane + hideAllPanes(); + vBox.getChildren().forEach(button -> button.getStyleClass().remove("action-button")); + buttonSign.getStyleClass().add("action-button"); + signTxGridPane.setVisible(true); + privateKeyHex.setText(""); + signatureHex.setText(""); + }); + buttonBuild.setOnAction(e -> { // just show the build pane + hideAllPanes(); + vBox.getChildren().forEach(button -> button.getStyleClass().remove("action-button")); + buttonBuild.getStyleClass().add("action-button"); + buildTxGridPane.setVisible(true); + finalSignedTxHex.setText(""); + }); + } + private void addInputsPane() { + inputsGridPane = new GridPane(); + gridPane.add(inputsGridPane, 1, rowIndex); + int rowIndexA = 0; + + depositTxLegacy = addCheckBox(inputsGridPane, rowIndexA, "depositTxLegacy"); + + Tooltip tooltip = new Tooltip(Res.get("txIdTextField.blockExplorerIcon.tooltip")); + blockExplorerIcon = new Label(); + blockExplorerIcon.getStyleClass().addAll("icon", "highlight"); + blockExplorerIcon.setTooltip(tooltip); + AwesomeDude.setIcon(blockExplorerIcon, AwesomeIcon.EXTERNAL_LINK); + blockExplorerIcon.setMinWidth(20); + blockExplorerIcon.setOnMouseClicked(mouseEvent -> openBlockExplorer(depositTxHex.getText())); + depositTxHex = addInputTextField(inputsGridPane, rowIndexA, "depositTxId"); + HBox hBoxTx = new HBox(12, depositTxHex, blockExplorerIcon); + hBoxTx.setAlignment(Pos.BASELINE_LEFT); + hBoxTx.setPrefWidth(800); + inputsGridPane.add(new Label(""), 0, ++rowIndexA); // spacer + inputsGridPane.add(hBoxTx, 0, ++rowIndexA); + + amountInMultisig = addInputTextField(inputsGridPane, ++rowIndexA, "amountInMultisig"); + inputsGridPane.add(new Label(""), 0, ++rowIndexA); // spacer + buyerPayoutAmount = addInputTextField(inputsGridPane, rowIndexA, "buyerPayoutAmount"); + sellerPayoutAmount = addInputTextField(inputsGridPane, rowIndexA, "sellerPayoutAmount"); + txFee = addInputTextField(inputsGridPane, rowIndexA, "Tx fee"); + txFee.setEditable(false); + HBox hBox = new HBox(12, buyerPayoutAmount, sellerPayoutAmount, txFee); + hBox.setAlignment(Pos.BASELINE_LEFT); + hBox.setPrefWidth(800); + inputsGridPane.add(hBox, 0, ++rowIndexA); + buyerAddressString = addInputTextField(inputsGridPane, ++rowIndexA, "buyerPayoutAddress"); + sellerAddressString = addInputTextField(inputsGridPane, ++rowIndexA, "sellerPayoutAddress"); + buyerPubKeyAsHex = addInputTextField(inputsGridPane, ++rowIndexA, "buyerPubKeyAsHex"); + sellerPubKeyAsHex = addInputTextField(inputsGridPane, ++rowIndexA, "sellerPubKeyAsHex"); + depositTxHex.setPrefWidth(800); + depositTxLegacy.setAllowIndeterminate(false); + depositTxLegacy.setSelected(false); + depositTxHex.setValidator(new LengthValidator(HEX_HASH_LENGTH, HEX_HASH_LENGTH)); + buyerAddressString.setValidator(new LengthValidator(20, 80)); + sellerAddressString.setValidator(new LengthValidator(20, 80)); + buyerPubKeyAsHex.setValidator(new LengthValidator(HEX_PUBKEY_LENGTH, HEX_PUBKEY_LENGTH)); + sellerPubKeyAsHex.setValidator(new LengthValidator(HEX_PUBKEY_LENGTH, HEX_PUBKEY_LENGTH)); + } + + private void addImportPane() { + int rowIndexB = 0; + importTxGridPane = new GridPane(); + gridPane.add(importTxGridPane, 1, rowIndex); + importHex = new BisqTextArea(); + importHex.setEditable(true); + importHex.setWrapText(true); + importHex.setPrefSize(800, 150); + importTxGridPane.add(importHex, 0, ++rowIndexB); + importTxGridPane.add(new Label(""), 0, ++rowIndexB); // spacer + Button buttonImport = new AutoTooltipButton("Import From String"); + buttonImport.setOnAction(e -> { + // here we need to populate the "inputs" fields from the data contained in the TextArea + if (doImport(importHex.getText())) { + // switch back to the inputs pane + hideAllPanes(); + inputsGridPane.setVisible(true); + } + }); + HBox hBox = new HBox(12, buttonImport); + hBox.setAlignment(Pos.BASELINE_CENTER); + hBox.setPrefWidth(800); + importTxGridPane.add(hBox, 0, ++rowIndexB); + importTxGridPane.add(new Label(""), 0, ++rowIndexB); // spacer + + final Separator separator = new Separator(Orientation.HORIZONTAL); + separator.setPadding(new Insets(10, 10, 10, 10)); + importTxGridPane.add(separator, 0, ++rowIndexB); + + importTxGridPane.add(new Label(""), 0, ++rowIndexB); // spacer + final Tuple2> xTuple = addTopLabelComboBox(importTxGridPane, rowIndexB, "Mediation Ticket", "", 0); + mediationDropDown = xTuple.second; + recentTickets = addCheckBox(importTxGridPane, rowIndexB, "Recent Tickets"); + recentTickets.setSelected(true); + HBox hBox2 = new HBox(12, mediationDropDown, recentTickets); + hBox2.setAlignment(Pos.BASELINE_CENTER); + hBox2.setPrefWidth(800); + importTxGridPane.add(hBox2, 0, ++rowIndexB); + populateMediationTicketCombo(recentTickets.isSelected()); + recentTickets.setOnAction(e -> { + populateMediationTicketCombo(recentTickets.isSelected()); + }); + importTxGridPane.add(new Label(""), 0, ++rowIndexB); // spacer + Button buttonImportTicket = new AutoTooltipButton("Import From Mediation Ticket"); + buttonImportTicket.setOnAction(e -> { + // here we need to populate the "inputs" fields from the chosen mediator ticket + importFromMediationTicket(mediationDropDown.getValue()); + }); + HBox hBox3 = new HBox(12, buttonImportTicket); + hBox3.setAlignment(Pos.BASELINE_CENTER); + hBox3.setPrefWidth(800); + importTxGridPane.add(hBox3, 0, ++rowIndexB); + } + + private void addExportPane() { + exportTxGridPane = new GridPane(); + gridPane.add(exportTxGridPane, 1, rowIndex); + exportHex = new BisqTextArea(); + exportHex.setEditable(false); + exportHex.setWrapText(true); + exportHex.setPrefSize(800, 250); + exportTxGridPane.add(exportHex, 0, 1); + } + + private void addSignPane() { + int rowIndexB = 0; + signTxGridPane = new GridPane(); + gridPane.add(signTxGridPane, 1, rowIndex); + privateKeyHex = addInputTextField(inputsGridPane, ++rowIndexB, "privateKeyHex"); + signTxGridPane.add(privateKeyHex, 0, ++rowIndexB); + signatureHex = addInputTextField(signTxGridPane, ++rowIndexB, "signatureHex"); + signatureHex.setPrefWidth(800); + signatureHex.setEditable(false); + copyIcon = new Label(); + copyIcon.setTooltip(new Tooltip(Res.get("txIdTextField.copyIcon.tooltip"))); + AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY); + copyIcon.getStyleClass().addAll("icon", "highlight"); + copyIcon.setMinWidth(20); + copyIcon.setOnMouseClicked(mouseEvent -> Utilities.copyToClipboard(signatureHex.getText())); + HBox hBoxSig = new HBox(12, signatureHex, copyIcon); + hBoxSig.setAlignment(Pos.BASELINE_LEFT); + hBoxSig.setPrefWidth(800); + signTxGridPane.add(new Label(""), 0, ++rowIndexB); // spacer + signTxGridPane.add(hBoxSig, 0, ++rowIndexB); + signTxGridPane.add(new Label(""), 0, ++rowIndexB); // spacer + Button buttonLocate = new AutoTooltipButton("Locate key in wallet"); + Button buttonSign = new AutoTooltipButton("Generate Signature"); + HBox hBox = new HBox(12, buttonLocate, buttonSign); + hBox.setAlignment(Pos.BASELINE_CENTER); + hBox.setPrefWidth(800); + signTxGridPane.add(hBox, 0, ++rowIndexB); + buttonLocate.setOnAction(e -> { + if (!validateInputFields()) { + signatureHex.setText("You need to fill in the inputs tab first"); + return; + } + String walletInfo = walletsManager.getWalletsAsString(true); + String privateKeyText = findPrivForPub(walletInfo, buyerPubKeyAsHex.getText()); + if (privateKeyText == null) { + privateKeyText = findPrivForPub(walletInfo, sellerPubKeyAsHex.getText()); + } + if (privateKeyText == null) { + privateKeyText = "Not found in wallet"; + } + privateKeyHex.setText(privateKeyText); + }); + buttonSign.setOnAction(e -> { + signatureHex.setText(generateSignature()); + }); + } + + private void addBuildPane() { + buildTxGridPane = new GridPane(); + gridPane.add(buildTxGridPane, 1, rowIndex); + int rowIndexA = 0; + buyerSignatureAsHex = addInputTextField(buildTxGridPane, ++rowIndexA, "buyerSignatureAsHex"); + sellerSignatureAsHex = addInputTextField(buildTxGridPane, ++rowIndexA, "sellerSignatureAsHex"); + buildTxGridPane.add(new Label(""), 0, ++rowIndexA); // spacer + finalSignedTxHex = new BisqTextArea(); + finalSignedTxHex.setEditable(false); + finalSignedTxHex.setWrapText(true); + finalSignedTxHex.setPrefSize(800, 250); + buildTxGridPane.add(finalSignedTxHex, 0, ++rowIndexA); + buildTxGridPane.add(new Label(""), 0, ++rowIndexA); // spacer + Button buttonBuild = new AutoTooltipButton("Build"); + Button buttonBroadcast = new AutoTooltipButton("Broadcast"); + HBox hBox = new HBox(12, buttonBuild, buttonBroadcast); + hBox.setAlignment(Pos.BASELINE_CENTER); + hBox.setPrefWidth(800); + buildTxGridPane.add(hBox, 0, ++rowIndexA); + buttonBuild.setOnAction(e -> { + finalSignedTxHex.setText(buildFinalTx(false)); + }); + buttonBroadcast.setOnAction(e -> { + finalSignedTxHex.setText(buildFinalTx(true)); + }); + } + + private void hideAllPanes() { + inputsGridPane.setVisible(false); + importTxGridPane.setVisible(false); + exportTxGridPane.setVisible(false); + signTxGridPane.setVisible(false); + buildTxGridPane.setVisible(false); + } + + private void populateMediationTicketCombo(boolean recentTicketsOnly) { + Instant twoWeeksAgo = Instant.ofEpochSecond(Instant.now().getEpochSecond() - TimeUnit.DAYS.toSeconds(14)); + disputeObservableList = mediationManager.getDisputesAsObservableList(); + ObservableList disputeIds = FXCollections.observableArrayList(); + for (Dispute dispute :disputeObservableList) { + if (dispute.getDisputePayoutTxId() != null) // only show disputes not paid out + continue; + if (!dispute.isClosed()) // only show closed disputes + continue; + if (recentTicketsOnly && dispute.getOpeningDate().toInstant().isBefore(twoWeeksAgo)) + continue; + if (!disputeIds.contains(dispute.getTradeId())) + disputeIds.add(dispute.getTradeId()); + } + disputeIds.sort((a, b) -> a.compareTo(b)); + mediationDropDown.setItems(disputeIds); + } + + private void clearInputFields() { + depositTxHex.setText(""); + amountInMultisig.setText(""); buyerPayoutAmount.setText(""); sellerPayoutAmount.setText(""); - buyerAddressString.setText(""); - buyerPubKeyAsHex.setText(""); - buyerPrivateKeyAsHex.setText(""); - sellerAddressString.setText(""); + buyerPubKeyAsHex.setText(""); sellerPubKeyAsHex.setText(""); - sellerPrivateKeyAsHex.setText(""); + } - depositTxLegacy.setAllowIndeterminate(false); - depositTxLegacy.setSelected(false); + private boolean validateInputFields() { + return (depositTxHex.getText().length() == HEX_HASH_LENGTH && + amountInMultisig.getText().length() > 0 && + buyerPayoutAmount.getText().length() > 0 && + sellerPayoutAmount.getText().length() > 0 && + txFee.getText().length() > 0 && + buyerAddressString.getText().length() > 0 && + sellerAddressString.getText().length() > 0 && + buyerPubKeyAsHex.getText().length() == HEX_PUBKEY_LENGTH && + sellerPubKeyAsHex.getText().length() == HEX_PUBKEY_LENGTH); + } - actionButtonText("Sign and publish transaction"); + private boolean validateInputFieldsAndSignatures() { + return (validateInputFields() && + buyerSignatureAsHex.getText().length() > 0 && + sellerSignatureAsHex.getText().length() > 0); + } - TxBroadcaster.Callback callback = new TxBroadcaster.Callback() { - @Override - public void onSuccess(@Nullable Transaction result) { - log.error("onSuccess"); - UserThread.execute(() -> { - String txId = result != null ? result.getTxId().toString() : "null"; - new Popup().information("Transaction successful published. Transaction ID: " + txId).show(); - }); - } + private void calculateTxFee() { + if (buyerPayoutAmount.getText().length() > 0 && + sellerPayoutAmount.getText().length() > 0 && + amountInMultisig.getText().length() > 0) { + Coin txFeeValue = Coin.parseCoin(amountInMultisig.getText()) + .subtract(Coin.parseCoin(buyerPayoutAmount.getText())) + .subtract(Coin.parseCoin(sellerPayoutAmount.getText())); + txFee.setText(txFeeValue.toPlainString()); + } + } + + private void openBlockExplorer(String txId) { + if (txId.length() != HEX_HASH_LENGTH) + return; + if (preferences != null) { + BlockChainExplorer blockChainExplorer = preferences.getBlockChainExplorer(); + GUIUtil.openWebPage(blockChainExplorer.txUrl + txId, false); + } + } - @Override - public void onFailure(TxBroadcastException exception) { - log.error(exception.toString()); - UserThread.execute(() -> new Popup().warning(exception.toString()).show()); + private String findPrivForPub(String walletInfo, String publicKey) { + // split the walletInfo into lines, strip whitespace + // look for lines beginning "DeterministicKey{pub HEX=" .... ", priv HEX=" + int lineIndex = 0; + while (lineIndex < walletInfo.length() && lineIndex != -1) { + lineIndex = walletInfo.indexOf("DeterministicKey{pub HEX=", lineIndex); + if (lineIndex == -1) { + return null; } - }; - onAction(() -> { - if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { - try { - tradeWalletService.emergencySignAndPublishPayoutTxFrom2of2MultiSig(depositTxHex.getText(), - Coin.parseCoin(buyerPayoutAmount.getText()), - Coin.parseCoin(sellerPayoutAmount.getText()), - Coin.parseCoin(txFee.getText()), - buyerAddressString.getText(), - sellerAddressString.getText(), - buyerPrivateKeyAsHex.getText(), - sellerPrivateKeyAsHex.getText(), - buyerPubKeyAsHex.getText(), - sellerPubKeyAsHex.getText(), - depositTxLegacy.isSelected(), - callback); - } catch (AddressFormatException | WalletException | TransactionVerificationException e) { - log.error(e.toString()); - e.printStackTrace(); - UserThread.execute(() -> new Popup().warning(e.toString()).show()); + int toIndex = walletInfo.indexOf("}", lineIndex); + if (toIndex == -1) { + return null; + } + String candidate1 = walletInfo.substring(lineIndex, toIndex); + lineIndex = toIndex; + // do we have the public key? + if (candidate1.indexOf(publicKey, 0) > -1) { + int startOfPriv = candidate1.indexOf("priv HEX=", 0); + if (startOfPriv > -1) { + return candidate1.substring(startOfPriv + 9, startOfPriv + 9 + HEX_HASH_LENGTH); } } - }); + } + return null; } - @Override - protected void addButtons() { - super.addButtons(); - actionButton.setOnAction(event -> actionHandlerOptional.ifPresent(Runnable::run)); + private String generateExportText() { + // check that all input fields have been entered, except signatures + ArrayList fieldList = new ArrayList<>(); + fieldList.add(depositTxLegacy.isSelected() ? "legacy" : "segwit"); + fieldList.add(depositTxHex.getText()); + fieldList.add(amountInMultisig.getText()); + fieldList.add(buyerPayoutAmount.getText()); + fieldList.add(sellerPayoutAmount.getText()); + fieldList.add(buyerAddressString.getText()); + fieldList.add(sellerAddressString.getText()); + fieldList.add(buyerPubKeyAsHex.getText()); + fieldList.add(sellerPubKeyAsHex.getText()); + for (String item : fieldList) { + if (item.length() < 1) { + return "You need to fill in the inputs first"; + } + } + String listString = String.join(":", fieldList); + String base64encoded = Base64.encode(listString.getBytes()); + return base64encoded; + } + + private boolean doImport(String importedText) { + try { + clearInputFields(); + String decoded = new String(Base64.decode(importedText.replaceAll("\\s+", "")), Charset.forName("UTF-8")); + String splitArray[] = decoded.split(":"); + if (splitArray.length < 9) { + importHex.setText("Import failed - data format incorrect"); + return false; + } + int fieldIndex = 0; + depositTxLegacy.setSelected(splitArray[fieldIndex++].equalsIgnoreCase("legacy")); + depositTxHex.setText(splitArray[fieldIndex++]); + amountInMultisig.setText(splitArray[fieldIndex++]); + buyerPayoutAmount.setText(splitArray[fieldIndex++]); + sellerPayoutAmount.setText(splitArray[fieldIndex++]); + buyerAddressString.setText(splitArray[fieldIndex++]); + sellerAddressString.setText(splitArray[fieldIndex++]); + buyerPubKeyAsHex.setText(splitArray[fieldIndex++]); + sellerPubKeyAsHex.setText(splitArray[fieldIndex++]); + calculateTxFee(); + } catch (IllegalArgumentException e) { + importHex.setText("Import failed - base64 string incorrect"); + return false; + } + return true; + } + + private void importFromMediationTicket(String tradeId) { + clearInputFields(); + Optional optionalDispute = mediationManager.findDispute(tradeId); + if (optionalDispute.isPresent()) { + Dispute dispute = optionalDispute.get(); + depositTxHex.setText(dispute.getDepositTxId()); + if (dispute.disputeResultProperty().get() != null) { + buyerPayoutAmount.setText(dispute.disputeResultProperty().get().getBuyerPayoutAmount().toPlainString()); + sellerPayoutAmount.setText(dispute.disputeResultProperty().get().getSellerPayoutAmount().toPlainString()); + } + buyerAddressString.setText(dispute.getContract().getBuyerPayoutAddressString()); + sellerAddressString.setText(dispute.getContract().getSellerPayoutAddressString()); + buyerPubKeyAsHex.setText(Utils.HEX.encode(dispute.getContract().getBuyerMultiSigPubKey())); + sellerPubKeyAsHex.setText(Utils.HEX.encode(dispute.getContract().getSellerMultiSigPubKey())); + // switch back to the inputs pane + hideAllPanes(); + inputsGridPane.setVisible(true); + UserThread.execute(() -> new Popup().warning("Ticket imported. You still need to enter the multisig amount and specify if it is a legacy Tx").show()); + } + } + + private String generateSignature() { + calculateTxFee(); + // check that all input fields have been entered, except signatures + if (!validateInputFields() || privateKeyHex.getText().length() < 1) { + return "You need to fill in the inputs first"; + } + + String retVal = ""; + try { + Tuple2 combined = tradeWalletService.emergencyBuildPayoutTxFrom2of2MultiSig(depositTxHex.getText(), + Coin.parseCoin(buyerPayoutAmount.getText()), + Coin.parseCoin(sellerPayoutAmount.getText()), + Coin.parseCoin(txFee.getText()), + buyerAddressString.getText(), + sellerAddressString.getText(), + buyerPubKeyAsHex.getText(), + sellerPubKeyAsHex.getText(), + depositTxLegacy.isSelected()); + String redeemScriptHex = combined.first; + String unsignedTxHex = combined.second; + retVal = tradeWalletService.emergencyGenerateSignature( + unsignedTxHex, + redeemScriptHex, + Coin.parseCoin(amountInMultisig.getText()), + privateKeyHex.getText()); + } catch (IllegalArgumentException ee) { + log.error(ee.toString()); + ee.printStackTrace(); + UserThread.execute(() -> new Popup().warning(ee.toString()).show()); + } + return retVal; } + + private String buildFinalTx(boolean broadcastIt) { + String retVal = ""; + calculateTxFee(); + // check that all input fields have been entered, including signatures + if (!validateInputFieldsAndSignatures()) { + retVal = "You need to fill in the inputs first"; + } else { + try { + // grab data from the inputs pane, build an unsigned tx and write it to the TextArea + Tuple2 combined = tradeWalletService.emergencyBuildPayoutTxFrom2of2MultiSig(depositTxHex.getText(), + Coin.parseCoin(buyerPayoutAmount.getText()), + Coin.parseCoin(sellerPayoutAmount.getText()), + Coin.parseCoin(txFee.getText()), + buyerAddressString.getText(), + sellerAddressString.getText(), + buyerPubKeyAsHex.getText(), + sellerPubKeyAsHex.getText(), + depositTxLegacy.isSelected()); + String redeemScriptHex = combined.first; + String unsignedTxHex = combined.second; + Tuple2 txIdAndHex = tradeWalletService.emergencyApplySignatureToPayoutTxFrom2of2MultiSig( + unsignedTxHex, + redeemScriptHex, + buyerSignatureAsHex.getText(), + sellerSignatureAsHex.getText(), + depositTxLegacy.isSelected()); + retVal = "txId:{" + txIdAndHex.first + "}\r\ntxHex:{" + txIdAndHex.second + "}"; + + if (broadcastIt) { + TxBroadcaster.Callback callback = new TxBroadcaster.Callback() { + @Override + public void onSuccess(@Nullable Transaction result) { + log.error("onSuccess"); + UserThread.execute(() -> { + String txId = result != null ? result.getTxId().toString() : "null"; + new Popup().information("Transaction successfully published. Transaction ID: " + txId).show(); + }); + } + @Override + public void onFailure(TxBroadcastException exception) { + log.error(exception.toString()); + UserThread.execute(() -> new Popup().warning(exception.toString()).show()); + } + }; + + if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { + try { + tradeWalletService.emergencyPublishPayoutTxFrom2of2MultiSig( + txIdAndHex.second, + callback); + } catch (AddressFormatException | WalletException | TransactionVerificationException ee) { + log.error(ee.toString()); + ee.printStackTrace(); + UserThread.execute(() -> new Popup().warning(ee.toString()).show()); + } + } + } + } catch (IllegalArgumentException | SignatureDecodeException | VerificationException ee) { + log.error(ee.toString()); + ee.printStackTrace(); + retVal = ee.toString(); + } + } + return retVal; + } + } From e888be319256ef1fd5a429a367274df35fed21b2 Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Wed, 16 Dec 2020 22:15:16 -0600 Subject: [PATCH 2/2] review fix: try..catch around all parseCoin() --- .../windows/ManualPayoutTxWindow.java | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java index 5e40b22ea24..72874554570 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java @@ -528,13 +528,21 @@ private boolean validateInputFieldsAndSignatures() { sellerSignatureAsHex.getText().length() > 0); } + private Coin getInputFieldAsCoin(InputTextField inputTextField) { + try { + return Coin.parseCoin(inputTextField.getText()); + } catch (RuntimeException ignore) { + } + return Coin.ZERO; + } + private void calculateTxFee() { if (buyerPayoutAmount.getText().length() > 0 && sellerPayoutAmount.getText().length() > 0 && amountInMultisig.getText().length() > 0) { - Coin txFeeValue = Coin.parseCoin(amountInMultisig.getText()) - .subtract(Coin.parseCoin(buyerPayoutAmount.getText())) - .subtract(Coin.parseCoin(sellerPayoutAmount.getText())); + Coin txFeeValue = getInputFieldAsCoin(amountInMultisig) + .subtract(getInputFieldAsCoin(buyerPayoutAmount)) + .subtract(getInputFieldAsCoin(sellerPayoutAmount)); txFee.setText(txFeeValue.toPlainString()); } } @@ -654,9 +662,9 @@ private String generateSignature() { String retVal = ""; try { Tuple2 combined = tradeWalletService.emergencyBuildPayoutTxFrom2of2MultiSig(depositTxHex.getText(), - Coin.parseCoin(buyerPayoutAmount.getText()), - Coin.parseCoin(sellerPayoutAmount.getText()), - Coin.parseCoin(txFee.getText()), + getInputFieldAsCoin(buyerPayoutAmount), + getInputFieldAsCoin(sellerPayoutAmount), + getInputFieldAsCoin(txFee), buyerAddressString.getText(), sellerAddressString.getText(), buyerPubKeyAsHex.getText(), @@ -667,7 +675,7 @@ private String generateSignature() { retVal = tradeWalletService.emergencyGenerateSignature( unsignedTxHex, redeemScriptHex, - Coin.parseCoin(amountInMultisig.getText()), + getInputFieldAsCoin(amountInMultisig), privateKeyHex.getText()); } catch (IllegalArgumentException ee) { log.error(ee.toString()); @@ -687,9 +695,9 @@ private String buildFinalTx(boolean broadcastIt) { try { // grab data from the inputs pane, build an unsigned tx and write it to the TextArea Tuple2 combined = tradeWalletService.emergencyBuildPayoutTxFrom2of2MultiSig(depositTxHex.getText(), - Coin.parseCoin(buyerPayoutAmount.getText()), - Coin.parseCoin(sellerPayoutAmount.getText()), - Coin.parseCoin(txFee.getText()), + getInputFieldAsCoin(buyerPayoutAmount), + getInputFieldAsCoin(sellerPayoutAmount), + getInputFieldAsCoin(txFee), buyerAddressString.getText(), sellerAddressString.getText(), buyerPubKeyAsHex.getText(),