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..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 @@ -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,514 @@ 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 Coin getInputFieldAsCoin(InputTextField inputTextField) { + try { + return Coin.parseCoin(inputTextField.getText()); + } catch (RuntimeException ignore) { + } + return Coin.ZERO; + } - @Override - public void onFailure(TxBroadcastException exception) { - log.error(exception.toString()); - UserThread.execute(() -> new Popup().warning(exception.toString()).show()); + private void calculateTxFee() { + if (buyerPayoutAmount.getText().length() > 0 && + sellerPayoutAmount.getText().length() > 0 && + amountInMultisig.getText().length() > 0) { + Coin txFeeValue = getInputFieldAsCoin(amountInMultisig) + .subtract(getInputFieldAsCoin(buyerPayoutAmount)) + .subtract(getInputFieldAsCoin(sellerPayoutAmount)); + 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); + } + } + + 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(), + getInputFieldAsCoin(buyerPayoutAmount), + getInputFieldAsCoin(sellerPayoutAmount), + getInputFieldAsCoin(txFee), + buyerAddressString.getText(), + sellerAddressString.getText(), + buyerPubKeyAsHex.getText(), + sellerPubKeyAsHex.getText(), + depositTxLegacy.isSelected()); + String redeemScriptHex = combined.first; + String unsignedTxHex = combined.second; + retVal = tradeWalletService.emergencyGenerateSignature( + unsignedTxHex, + redeemScriptHex, + getInputFieldAsCoin(amountInMultisig), + 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(), + getInputFieldAsCoin(buyerPayoutAmount), + getInputFieldAsCoin(sellerPayoutAmount), + getInputFieldAsCoin(txFee), + 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; + } + }