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 <boilingfrog@gmx.com>
This commit is contained in:
HenrikJannsen 2022-11-07 14:47:09 -05:00
parent 78d3fd8d44
commit 402c4075d0
No known key found for this signature in database
GPG Key ID: 02AA2BAE387C8307
3 changed files with 83 additions and 7 deletions

View File

@ -591,7 +591,7 @@ public class BsqWalletService extends WalletService implements DaoStateListener
coinSelector.setUtxoCandidates(null); // We reuse the selectors. Reset the transactionOutputCandidates field coinSelector.setUtxoCandidates(null); // We reuse the selectors. Reset the transactionOutputCandidates field
return tx; return tx;
} catch (InsufficientMoneyException e) { } catch (InsufficientMoneyException e) {
log.error("getPreparedSendTx: tx={}", tx.toString()); log.error("getPreparedSendTx: tx={}", tx);
log.error(e.toString()); log.error(e.toString());
throw new InsufficientBsqException(e.missing); throw new InsufficientBsqException(e.missing);
} }

View File

@ -2494,6 +2494,10 @@ dao.wallet.send.receiverBtcAddress=Receiver's BTC address
dao.wallet.send.setDestinationAddress=Fill in your destination address dao.wallet.send.setDestinationAddress=Fill in your destination address
dao.wallet.send.send=Send BSQ funds dao.wallet.send.send=Send BSQ funds
dao.wallet.send.inputControl=Select inputs 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.sendBtc=Send BTC funds
dao.wallet.send.sendFunds.headline=Confirm withdrawal request 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? 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?

View File

@ -61,8 +61,11 @@ import bisq.core.util.validation.BtcAddressValidator;
import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PService;
import bisq.common.UserThread; import bisq.common.UserThread;
import bisq.common.crypto.Hash;
import bisq.common.handlers.ResultHandler; import bisq.common.handlers.ResultHandler;
import bisq.common.util.Hex;
import bisq.common.util.Tuple2; import bisq.common.util.Tuple2;
import bisq.common.util.Tuple3;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException; import org.bitcoinj.core.InsufficientMoneyException;
@ -72,8 +75,13 @@ import org.bitcoinj.core.TransactionOutput;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import com.google.common.base.Charsets;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
@ -87,6 +95,7 @@ import javax.annotation.Nullable;
import static bisq.desktop.util.FormBuilder.addInputTextField; import static bisq.desktop.util.FormBuilder.addInputTextField;
import static bisq.desktop.util.FormBuilder.addTitledGroupBg; import static bisq.desktop.util.FormBuilder.addTitledGroupBg;
import static bisq.desktop.util.FormBuilder.addTopLabelTextField;
@FxmlView @FxmlView
public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqBalanceListener { public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqBalanceListener {
@ -106,12 +115,15 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
private final WalletPasswordWindow walletPasswordWindow; private final WalletPasswordWindow walletPasswordWindow;
private int gridRow = 0; private int gridRow = 0;
private InputTextField amountInputTextField, btcAmountInputTextField; private InputTextField amountInputTextField, btcAmountInputTextField, preImageTextField;
private Button sendBsqButton, sendBtcButton, bsqInputControlButton, btcInputControlButton; private TextField opReturnDataAsHexTextField;
private VBox opReturnDataAsHexBox;
private Label opReturnDataAsHexLabel;
private Button sendBsqButton, sendBtcButton, bsqInputControlButton, btcInputControlButton, btcOpReturnButton;
private InputTextField receiversAddressInputTextField, receiversBtcAddressInputTextField; private InputTextField receiversAddressInputTextField, receiversBtcAddressInputTextField;
private ChangeListener<Boolean> focusOutListener; private ChangeListener<Boolean> focusOutListener;
private TitledGroupBg btcTitledGroupBg; private TitledGroupBg btcTitledGroupBg;
private ChangeListener<String> inputTextFieldListener; private ChangeListener<String> inputTextFieldListener, preImageInputTextFieldListener;
@Nullable @Nullable
private Set<TransactionOutput> bsqUtxoCandidates; private Set<TransactionOutput> bsqUtxoCandidates;
@Nullable @Nullable
@ -166,6 +178,8 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
}; };
inputTextFieldListener = (observable, oldValue, newValue) -> onUpdateBalances(); inputTextFieldListener = (observable, oldValue, newValue) -> onUpdateBalances();
preImageInputTextFieldListener = (observable, oldValue, newValue) -> opReturnDataAsHexTextField.setText(getOpReturnDataAsHexFromPreImage(newValue));
setSendBtcGroupVisibleState(false); setSendBtcGroupVisibleState(false);
} }
@ -183,6 +197,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
bsqInputControlButton.setOnAction((event) -> onBsqInputControl()); bsqInputControlButton.setOnAction((event) -> onBsqInputControl());
sendBtcButton.setOnAction((event) -> onSendBtc()); sendBtcButton.setOnAction((event) -> onSendBtc());
btcInputControlButton.setOnAction((event) -> onBtcInputControl()); btcInputControlButton.setOnAction((event) -> onBtcInputControl());
btcOpReturnButton.setOnAction((event) -> onShowPreImageField());
receiversAddressInputTextField.focusedProperty().addListener(focusOutListener); receiversAddressInputTextField.focusedProperty().addListener(focusOutListener);
amountInputTextField.focusedProperty().addListener(focusOutListener); amountInputTextField.focusedProperty().addListener(focusOutListener);
@ -193,6 +208,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
amountInputTextField.textProperty().addListener(inputTextFieldListener); amountInputTextField.textProperty().addListener(inputTextFieldListener);
receiversBtcAddressInputTextField.textProperty().addListener(inputTextFieldListener); receiversBtcAddressInputTextField.textProperty().addListener(inputTextFieldListener);
btcAmountInputTextField.textProperty().addListener(inputTextFieldListener); btcAmountInputTextField.textProperty().addListener(inputTextFieldListener);
preImageTextField.textProperty().addListener(preImageInputTextFieldListener);
bsqWalletService.addBsqBalanceListener(this); bsqWalletService.addBsqBalanceListener(this);
@ -228,11 +244,13 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
amountInputTextField.textProperty().removeListener(inputTextFieldListener); amountInputTextField.textProperty().removeListener(inputTextFieldListener);
receiversBtcAddressInputTextField.textProperty().removeListener(inputTextFieldListener); receiversBtcAddressInputTextField.textProperty().removeListener(inputTextFieldListener);
btcAmountInputTextField.textProperty().removeListener(inputTextFieldListener); btcAmountInputTextField.textProperty().removeListener(inputTextFieldListener);
preImageTextField.textProperty().removeListener(preImageInputTextFieldListener);
bsqWalletService.removeBsqBalanceListener(this); bsqWalletService.removeBsqBalanceListener(this);
sendBsqButton.setOnAction(null); sendBsqButton.setOnAction(null);
btcInputControlButton.setOnAction(null); btcInputControlButton.setOnAction(null);
btcOpReturnButton.setOnAction(null);
sendBtcButton.setOnAction(null); sendBtcButton.setOnAction(null);
bsqInputControlButton.setOnAction(null); bsqInputControlButton.setOnAction(null);
} }
@ -367,12 +385,14 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
btcAmountInputTextField.setVisible(visible); btcAmountInputTextField.setVisible(visible);
sendBtcButton.setVisible(visible); sendBtcButton.setVisible(visible);
btcInputControlButton.setVisible(visible); btcInputControlButton.setVisible(visible);
btcOpReturnButton.setVisible(visible);
btcTitledGroupBg.setManaged(visible); btcTitledGroupBg.setManaged(visible);
receiversBtcAddressInputTextField.setManaged(visible); receiversBtcAddressInputTextField.setManaged(visible);
btcAmountInputTextField.setManaged(visible); btcAmountInputTextField.setManaged(visible);
sendBtcButton.setManaged(visible); sendBtcButton.setManaged(visible);
btcInputControlButton.setManaged(visible); btcInputControlButton.setManaged(visible);
btcOpReturnButton.setManaged(visible);
} }
private void addSendBtcGroup() { private void addSendBtcGroup() {
@ -387,10 +407,24 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
btcAmountInputTextField.setValidator(btcValidator); btcAmountInputTextField.setValidator(btcValidator);
GridPane.setColumnSpan(btcAmountInputTextField, 3); GridPane.setColumnSpan(btcAmountInputTextField, 3);
Tuple2<Button, Button> tuple = FormBuilder.add2ButtonsAfterGroup(root, ++gridRow, preImageTextField = addInputTextField(root, ++gridRow, Res.get("dao.wallet.send.preImage"));
Res.get("dao.wallet.send.sendBtc"), Res.get("dao.wallet.send.inputControl")); GridPane.setColumnSpan(preImageTextField, 3);
preImageTextField.setVisible(false);
preImageTextField.setManaged(false);
Tuple3<Label, TextField, VBox> 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<Button, Button, Button> 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; sendBtcButton = tuple.first;
btcInputControlButton = tuple.second; btcInputControlButton = tuple.second;
btcOpReturnButton = tuple.third;
} }
private void onBtcInputControl() { private void onBtcInputControl() {
@ -410,6 +444,15 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
show(); 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<TransactionOutput> candidates) { private void setBtcUtxoCandidates(Set<TransactionOutput> candidates) {
this.btcUtxoCandidates = candidates; this.btcUtxoCandidates = candidates;
updateBtcValidator(getSpendableBtcBalance()); updateBtcValidator(getSpendableBtcBalance());
@ -430,8 +473,12 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
String receiversAddressString = receiversBtcAddressInputTextField.getText(); String receiversAddressString = receiversBtcAddressInputTextField.getText();
Coin receiverAmount = bsqFormatter.parseToBTC(btcAmountInputTextField.getText()); Coin receiverAmount = bsqFormatter.parseToBTC(btcAmountInputTextField.getText());
try { try {
byte[] opReturnData = null;
if (preImageTextField.isVisible() && !preImageTextField.getText().trim().isEmpty()) {
opReturnData = getOpReturnData(preImageTextField.getText());
}
Transaction preparedSendTx = bsqWalletService.getPreparedSendBtcTx(receiversAddressString, receiverAmount, btcUtxoCandidates); Transaction preparedSendTx = bsqWalletService.getPreparedSendBtcTx(receiversAddressString, receiverAmount, btcUtxoCandidates);
Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx); Transaction txWithBtcFee = btcWalletService.completePreparedBsqTx(preparedSendTx, opReturnData);
Transaction signedTx = bsqWalletService.signTxAndVerifyNoDustOutputs(txWithBtcFee); Transaction signedTx = bsqWalletService.signTxAndVerifyNoDustOutputs(txWithBtcFee);
Coin miningFee = signedTx.getFee(); Coin miningFee = signedTx.getFee();
@ -449,6 +496,7 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
() -> { () -> {
receiversBtcAddressInputTextField.setText(""); receiversBtcAddressInputTextField.setText("");
btcAmountInputTextField.setText(""); btcAmountInputTextField.setText("");
preImageTextField.clear();
receiversBtcAddressInputTextField.resetValidation(); receiversBtcAddressInputTextField.resetValidation();
btcAmountInputTextField.resetValidation(); btcAmountInputTextField.resetValidation();
@ -463,6 +511,30 @@ public class BsqSendView extends ActivatableView<GridPane, Void> implements BsqB
} }
} }
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) { private void handleError(Throwable t) {
if (t instanceof InsufficientMoneyException) { if (t instanceof InsufficientMoneyException) {
final Coin missingCoin = ((InsufficientMoneyException) t).missing; final Coin missingCoin = ((InsufficientMoneyException) t).missing;