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
This commit is contained in:
jmacxx 2020-12-04 13:13:13 -06:00
parent 9b774d1515
commit b1d22af1ae
No known key found for this signature in database
GPG key ID: 155297BABFE94A1B
2 changed files with 682 additions and 107 deletions

View file

@ -30,6 +30,7 @@ import bisq.core.locale.Res;
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 class TradeWalletService {
// 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<String, String> 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 class TradeWalletService {
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 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());
}
ECKey sellerPrivateKey = ECKey.fromPrivate(Utils.HEX.decode(sellerPrivateKeyAsHex));
checkNotNull(sellerPrivateKey, "key must not be null");
ECKey.ECDSASignature sellerECDSASignature = sellerPrivateKey.sign(sigHash, aesKey).toCanonicalised();
TransactionSignature buyerTxSig = new TransactionSignature(buyerECDSASignature, Transaction.SigHash.ALL, false);
TransactionSignature sellerTxSig = new TransactionSignature(sellerECDSASignature, Transaction.SigHash.ALL, false);
public Tuple2<String, String> 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 class TradeWalletService {
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);

View file

@ -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.exceptions.WalletException;
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<ManualPayoutTxWindow> {
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<String> mediationDropDown;
ObservableList<Dispute> 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<Boolean> 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 class ManualPayoutTxWindow extends Overlay<ManualPayoutTxWindow> {
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 @@ public class ManualPayoutTxWindow extends Overlay<ManualPayoutTxWindow> {
}
}
@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);
}
@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();
}
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");
InputTextField buyerPubKeyAsHex = addInputTextField(gridPane, ++rowIndex, "buyerPubKeyAsHex");
InputTextField sellerPubKeyAsHex = addInputTextField(gridPane, ++rowIndex, "sellerPubKeyAsHex");
CheckBox depositTxLegacy = addCheckBox(gridPane, ++rowIndex, "depositTxLegacy");
rowIndex = 1;
this.disableActionButton = true;
addLeftPanelButtons();
addInputsPane();
addImportPane();
addExportPane();
addSignPane();
addBuildPane();
hideAllPanes();
inputsGridPane.setVisible(true);
// Notes:
// Open with alt+g
@ -126,68 +241,506 @@ public class ManualPayoutTxWindow extends Overlay<ManualPayoutTxWindow> {
// 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("");
buyerPayoutAmount.setText("");
sellerPayoutAmount.setText("");
buyerAddressString.setText("");
buyerPubKeyAsHex.setText("");
buyerPrivateKeyAsHex.setText("");
sellerAddressString.setText("");
sellerPubKeyAsHex.setText("");
sellerPrivateKeyAsHex.setText("");
depositTxLegacy.setAllowIndeterminate(false);
depositTxLegacy.setSelected(false);
actionButtonText("Sign and publish transaction");
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();
});
}
@Override
public void onFailure(TxBroadcastException exception) {
log.error(exception.toString());
UserThread.execute(() -> new Popup().warning(exception.toString()).show());
}
};
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());
}
}
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("");
});
}
@Override
protected void addButtons() {
super.addButtons();
actionButton.setOnAction(event -> actionHandlerOptional.ifPresent(Runnable::run));
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<Label, ComboBox<String>> 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<String> 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("");
sellerAddressString.setText("");
buyerPubKeyAsHex.setText("");
sellerPubKeyAsHex.setText("");
}
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);
}
private boolean validateInputFieldsAndSignatures() {
return (validateInputFields() &&
buyerSignatureAsHex.getText().length() > 0 &&
sellerSignatureAsHex.getText().length() > 0);
}
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);
}
}
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;
}
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;
}
private String generateExportText() {
// check that all input fields have been entered, except signatures
ArrayList<String> 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<Dispute> 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<String, String> 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<String, String> 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<String, String> 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;
}
}