From 057db80c1dcfd2716490d629208c7955b1ebf948 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Mon, 7 Nov 2022 14:47:09 -0500 Subject: [PATCH] Add option to add opReturn data at BSQ wallet send screen for non-BSQ balance. This can be used for providing proof of ownership of a genesis output when attaching a custom BM receiver address. See: https://github.com/bisq-network/proposals/issues/390#issuecomment-1306075295 Signed-off-by: HenrikJannsen --- .../core/btc/wallet/BsqWalletService.java | 2 +- .../resources/i18n/displayStrings.properties | 4 + .../main/dao/wallet/send/BsqSendView.java | 84 +++++++++++++++++-- 3 files changed, 83 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java index 273def00b0b..f07ea8cf97d 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java @@ -591,7 +591,7 @@ private Transaction getPreparedSendTx(String receiverAddress, coinSelector.setUtxoCandidates(null); // We reuse the selectors. Reset the transactionOutputCandidates field return tx; } catch (InsufficientMoneyException e) { - log.error("getPreparedSendTx: tx={}", tx.toString()); + log.error("getPreparedSendTx: tx={}", tx); log.error(e.toString()); throw new InsufficientBsqException(e.missing); } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 36b99750148..5f37e7cce16 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2493,6 +2493,10 @@ dao.wallet.send.receiverBtcAddress=Receiver's BTC address dao.wallet.send.setDestinationAddress=Fill in your destination address dao.wallet.send.send=Send BSQ funds dao.wallet.send.inputControl=Select inputs +dao.wallet.send.addOpReturn=Add data +dao.wallet.send.preImage=Pre-image +dao.wallet.send.opReturnAsHex=Op-Return data Hex encoded +dao.wallet.send.opReturnAsHash=Hash of Op-Return data dao.wallet.send.sendBtc=Send BTC funds dao.wallet.send.sendFunds.headline=Confirm withdrawal request dao.wallet.send.sendFunds.details=Sending: {0}\nTo receiving address: {1}.\nRequired mining fee is: {2} ({3} satoshis/vbyte)\nTransaction vsize: {4} vKb\n\nThe recipient will receive: {5}\n\nAre you sure you want to withdraw that amount? diff --git a/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java index 010b58ea96a..c9caa5e0bec 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java @@ -61,8 +61,11 @@ import bisq.network.p2p.P2PService; import bisq.common.UserThread; +import bisq.common.crypto.Hash; import bisq.common.handlers.ResultHandler; +import bisq.common.util.Hex; import bisq.common.util.Tuple2; +import bisq.common.util.Tuple3; import org.bitcoinj.core.Coin; import org.bitcoinj.core.InsufficientMoneyException; @@ -72,8 +75,13 @@ import javax.inject.Inject; import javax.inject.Named; +import com.google.common.base.Charsets; + import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; +import javafx.scene.layout.VBox; import javafx.beans.value.ChangeListener; @@ -87,6 +95,7 @@ import static bisq.desktop.util.FormBuilder.addInputTextField; import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; @FxmlView public class BsqSendView extends ActivatableView implements BsqBalanceListener { @@ -106,12 +115,15 @@ public class BsqSendView extends ActivatableView implements BsqB private final WalletPasswordWindow walletPasswordWindow; private int gridRow = 0; - private InputTextField amountInputTextField, btcAmountInputTextField; - private Button sendBsqButton, sendBtcButton, bsqInputControlButton, btcInputControlButton; + private InputTextField amountInputTextField, btcAmountInputTextField, preImageTextField; + private TextField opReturnDataAsHexTextField; + private VBox opReturnDataAsHexBox; + private Label opReturnDataAsHexLabel; + private Button sendBsqButton, sendBtcButton, bsqInputControlButton, btcInputControlButton, btcOpReturnButton; private InputTextField receiversAddressInputTextField, receiversBtcAddressInputTextField; private ChangeListener focusOutListener; private TitledGroupBg btcTitledGroupBg; - private ChangeListener inputTextFieldListener; + private ChangeListener inputTextFieldListener, preImageInputTextFieldListener; @Nullable private Set bsqUtxoCandidates; @Nullable @@ -166,6 +178,8 @@ public void initialize() { }; inputTextFieldListener = (observable, oldValue, newValue) -> onUpdateBalances(); + preImageInputTextFieldListener = (observable, oldValue, newValue) -> opReturnDataAsHexTextField.setText(getOpReturnDataAsHexFromPreImage(newValue)); + setSendBtcGroupVisibleState(false); } @@ -183,6 +197,7 @@ protected void activate() { bsqInputControlButton.setOnAction((event) -> onBsqInputControl()); sendBtcButton.setOnAction((event) -> onSendBtc()); btcInputControlButton.setOnAction((event) -> onBtcInputControl()); + btcOpReturnButton.setOnAction((event) -> onShowPreImageField()); receiversAddressInputTextField.focusedProperty().addListener(focusOutListener); amountInputTextField.focusedProperty().addListener(focusOutListener); @@ -193,6 +208,7 @@ protected void activate() { amountInputTextField.textProperty().addListener(inputTextFieldListener); receiversBtcAddressInputTextField.textProperty().addListener(inputTextFieldListener); btcAmountInputTextField.textProperty().addListener(inputTextFieldListener); + preImageTextField.textProperty().addListener(preImageInputTextFieldListener); bsqWalletService.addBsqBalanceListener(this); @@ -228,11 +244,13 @@ protected void deactivate() { amountInputTextField.textProperty().removeListener(inputTextFieldListener); receiversBtcAddressInputTextField.textProperty().removeListener(inputTextFieldListener); btcAmountInputTextField.textProperty().removeListener(inputTextFieldListener); + preImageTextField.textProperty().removeListener(preImageInputTextFieldListener); bsqWalletService.removeBsqBalanceListener(this); sendBsqButton.setOnAction(null); btcInputControlButton.setOnAction(null); + btcOpReturnButton.setOnAction(null); sendBtcButton.setOnAction(null); bsqInputControlButton.setOnAction(null); } @@ -367,12 +385,14 @@ private void setSendBtcGroupVisibleState(boolean visible) { btcAmountInputTextField.setVisible(visible); sendBtcButton.setVisible(visible); btcInputControlButton.setVisible(visible); + btcOpReturnButton.setVisible(visible); btcTitledGroupBg.setManaged(visible); receiversBtcAddressInputTextField.setManaged(visible); btcAmountInputTextField.setManaged(visible); sendBtcButton.setManaged(visible); btcInputControlButton.setManaged(visible); + btcOpReturnButton.setManaged(visible); } private void addSendBtcGroup() { @@ -387,10 +407,24 @@ private void addSendBtcGroup() { btcAmountInputTextField.setValidator(btcValidator); GridPane.setColumnSpan(btcAmountInputTextField, 3); - Tuple2 tuple = FormBuilder.add2ButtonsAfterGroup(root, ++gridRow, - Res.get("dao.wallet.send.sendBtc"), Res.get("dao.wallet.send.inputControl")); + preImageTextField = addInputTextField(root, ++gridRow, Res.get("dao.wallet.send.preImage")); + GridPane.setColumnSpan(preImageTextField, 3); + preImageTextField.setVisible(false); + preImageTextField.setManaged(false); + + Tuple3 opReturnDataAsHexTuple = addTopLabelTextField(root, ++gridRow, Res.get("dao.wallet.send.opReturnAsHex"), -10); + opReturnDataAsHexLabel = opReturnDataAsHexTuple.first; + opReturnDataAsHexTextField = opReturnDataAsHexTuple.second; + opReturnDataAsHexBox = opReturnDataAsHexTuple.third; + GridPane.setColumnSpan(opReturnDataAsHexBox, 3); + opReturnDataAsHexBox.setVisible(false); + opReturnDataAsHexBox.setManaged(false); + + Tuple3 tuple = FormBuilder.add3ButtonsAfterGroup(root, ++gridRow, + Res.get("dao.wallet.send.sendBtc"), Res.get("dao.wallet.send.inputControl"), Res.get("dao.wallet.send.addOpReturn")); sendBtcButton = tuple.first; btcInputControlButton = tuple.second; + btcOpReturnButton = tuple.third; } private void onBtcInputControl() { @@ -410,6 +444,15 @@ private void onBtcInputControl() { show(); } + private void onShowPreImageField() { + btcOpReturnButton.setDisable(true); + preImageTextField.setManaged(true); + preImageTextField.setVisible(true); + opReturnDataAsHexBox.setManaged(true); + opReturnDataAsHexBox.setVisible(true); + GridPane.setRowSpan(btcTitledGroupBg, 4); + } + private void setBtcUtxoCandidates(Set candidates) { this.btcUtxoCandidates = candidates; updateBtcValidator(getSpendableBtcBalance()); @@ -430,8 +473,12 @@ private void onSendBtc() { String receiversAddressString = receiversBtcAddressInputTextField.getText(); Coin receiverAmount = bsqFormatter.parseToBTC(btcAmountInputTextField.getText()); try { + byte[] opReturnData = null; + if (preImageTextField.isVisible() && !preImageTextField.getText().trim().isEmpty()) { + opReturnData = getOpReturnData(preImageTextField.getText()); + } Transaction preparedSendTx = bsqWalletService.getPreparedSendBtcTx(receiversAddressString, receiverAmount, btcUtxoCandidates); - Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx); + Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedSendTx, opReturnData); Transaction signedTx = bsqWalletService.signTxAndVerifyNoDustOutputs(txWithBtcFee); Coin miningFee = signedTx.getFee(); @@ -449,6 +496,7 @@ private void onSendBtc() { () -> { receiversBtcAddressInputTextField.setText(""); btcAmountInputTextField.setText(""); + preImageTextField.clear(); receiversBtcAddressInputTextField.resetValidation(); btcAmountInputTextField.resetValidation(); @@ -463,6 +511,30 @@ private void onSendBtc() { } } + private byte[] getOpReturnData(String preImageAsString) { + byte[] opReturnData; + try { + // If preImage is hex encoded we use it directly + opReturnData = Hex.decode(preImageAsString); + } catch (Throwable ignore) { + opReturnData = preImageAsString.getBytes(Charsets.UTF_8); + } + + // If too long for OpReturn we hash it + if (opReturnData.length > 80) { + opReturnData = Hash.getSha256Ripemd160hash(opReturnData); + opReturnDataAsHexLabel.setText(Res.get("dao.wallet.send.opReturnAsHash")); + } else { + opReturnDataAsHexLabel.setText(Res.get("dao.wallet.send.opReturnAsHex")); + } + + return opReturnData; + } + + private String getOpReturnDataAsHexFromPreImage(String preImage) { + return Hex.encode(getOpReturnData(preImage)); + } + private void handleError(Throwable t) { if (t instanceof InsufficientMoneyException) { final Coin missingCoin = ((InsufficientMoneyException) t).missing;