diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 9b7b25bc8f2..bc5f8a4b200 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -45,6 +45,8 @@ import lombok.extern.slf4j.Slf4j; +import javax.annotation.Nullable; + /** * Provides high level interface to functionality of core Bisq features. * E.g. useful for different APIs to access data of different domains of Bisq. @@ -208,8 +210,8 @@ public void keepFunds(String tradeId) { coreTradesService.keepFunds(tradeId); } - public void withdrawFunds(String tradeId, String address) { - coreTradesService.withdrawFunds(tradeId, address); + public void withdrawFunds(String tradeId, String address, @Nullable String memo) { + coreTradesService.withdrawFunds(tradeId, address, memo); } public Trade getTrade(String tradeId) { diff --git a/core/src/main/java/bisq/core/api/CoreTradesService.java b/core/src/main/java/bisq/core/api/CoreTradesService.java index 4bc678d9263..2f75f241407 100644 --- a/core/src/main/java/bisq/core/api/CoreTradesService.java +++ b/core/src/main/java/bisq/core/api/CoreTradesService.java @@ -41,6 +41,8 @@ import lombok.extern.slf4j.Slf4j; +import javax.annotation.Nullable; + import static bisq.core.btc.model.AddressEntry.Context.TRADE_PAYOUT; import static java.lang.String.format; @@ -154,7 +156,7 @@ void keepFunds(String tradeId) { tradeManager.onTradeCompleted(trade); } - void withdrawFunds(String tradeId, String toAddress) { + void withdrawFunds(String tradeId, String toAddress, @Nullable String memo) { // An encrypted wallet must be unlocked for this operation. verifyTradeIsNotClosed(tradeId); var trade = getOpenTrade(tradeId).orElseThrow(() -> @@ -184,6 +186,7 @@ void withdrawFunds(String tradeId, String toAddress) { fee, coreWalletsService.getKey(), trade, + memo, () -> { }, (errorMessage, throwable) -> { diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java index 3d45c491878..06c04a56dd2 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -254,8 +254,8 @@ private Transaction completePreparedProposalTx(Transaction feeTx, byte[] opRetur sendRequest.signInputs = false; sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs + - sigSizePerInput * numLegacyInputs + - sigSizePerInput * numSegwitInputs / 4); + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4); sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; @@ -274,8 +274,8 @@ private Transaction completePreparedProposalTx(Transaction feeTx, byte[] opRetur numSegwitInputs = numInputs.second; txVsizeWithUnsignedInputs = resultTx.getVsize(); long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs + - sigSizePerInput * numLegacyInputs + - sigSizePerInput * numSegwitInputs / 4).value; + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4).value; // calculated fee must be inside of a tolerance range with tx fee isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000; @@ -374,8 +374,8 @@ private Transaction addInputsForMinerFee(Transaction preparedTx, sendRequest.signInputs = false; sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs + - sigSizePerInput * numLegacyInputs + - sigSizePerInput * numSegwitInputs / 4); + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4); sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; @@ -393,8 +393,8 @@ private Transaction addInputsForMinerFee(Transaction preparedTx, numSegwitInputs = numInputs.second; txVsizeWithUnsignedInputs = resultTx.getVsize(); final long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs + - sigSizePerInput * numLegacyInputs + - sigSizePerInput * numSegwitInputs / 4).value; + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4).value; // calculated fee must be inside of a tolerance range with tx fee isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000; } @@ -532,8 +532,8 @@ public Transaction completePreparedBsqTx(Transaction preparedBsqTx, sendRequest.signInputs = false; sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs + - sigSizePerInput * numLegacyInputs + - sigSizePerInput * numSegwitInputs / 4); + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4); sendRequest.feePerKb = Coin.ZERO; sendRequest.ensureMinRequiredFee = false; @@ -558,8 +558,8 @@ public Transaction completePreparedBsqTx(Transaction preparedBsqTx, numSegwitInputs = numInputs.second; txVsizeWithUnsignedInputs = resultTx.getVsize(); final long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs + - sigSizePerInput * numLegacyInputs + - sigSizePerInput * numSegwitInputs / 4).value; + sigSizePerInput * numLegacyInputs + + sigSizePerInput * numSegwitInputs / 4).value; // calculated fee must be inside of a tolerance range with tx fee isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000; } @@ -583,7 +583,7 @@ private Tuple2 getNumInputs(Transaction tx) { for (TransactionInput input : tx.getInputs()) { TransactionOutput connectedOutput = input.getConnectedOutput(); if (connectedOutput == null || ScriptPattern.isP2PKH(connectedOutput.getScriptPubKey()) || - ScriptPattern.isP2PK(connectedOutput.getScriptPubKey())) { + ScriptPattern.isP2PK(connectedOutput.getScriptPubKey())) { // If connectedOutput is null, we don't know here the input type. To avoid underpaying fees, // we treat it as a legacy input which will result in a higher fee estimation. numLegacyInputs++; @@ -1100,12 +1100,15 @@ public String sendFunds(String fromAddress, Coin fee, @Nullable KeyParameter aesKey, @SuppressWarnings("SameParameterValue") AddressEntry.Context context, + @Nullable String memo, FutureCallback callback) throws AddressFormatException, AddressEntryException, InsufficientMoneyException { SendRequest sendRequest = getSendRequest(fromAddress, toAddress, receiverAmount, fee, aesKey, context); Wallet.SendResult sendResult = wallet.sendCoins(sendRequest); Futures.addCallback(sendResult.broadcastComplete, callback, MoreExecutors.directExecutor()); - + if (memo != null) { + sendResult.tx.setMemo(memo); + } printTx("sendFunds", sendResult.tx); return sendResult.tx.getTxId().toString(); } @@ -1116,13 +1119,16 @@ public Transaction sendFundsForMultipleAddresses(Set fromAddresses, Coin fee, @Nullable String changeAddress, @Nullable KeyParameter aesKey, + @Nullable String memo, FutureCallback callback) throws AddressFormatException, AddressEntryException, InsufficientMoneyException { SendRequest request = getSendRequestForMultipleAddresses(fromAddresses, toAddress, receiverAmount, fee, changeAddress, aesKey); Wallet.SendResult sendResult = wallet.sendCoins(request); Futures.addCallback(sendResult.broadcastComplete, callback, MoreExecutors.directExecutor()); - + if (memo != null) { + sendResult.tx.setMemo(memo); + } printTx("sendFunds", sendResult.tx); return sendResult.tx; } diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 5bbd88c4cd5..30cd7cfbf29 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -462,8 +462,14 @@ private OfferAvailabilityModel getOfferAvailabilityModel(Offer offer) { // Complete trade /////////////////////////////////////////////////////////////////////////////////////////// - public void onWithdrawRequest(String toAddress, Coin amount, Coin fee, KeyParameter aesKey, - Trade trade, ResultHandler resultHandler, FaultHandler faultHandler) { + public void onWithdrawRequest(String toAddress, + Coin amount, + Coin fee, + KeyParameter aesKey, + Trade trade, + @Nullable String memo, + ResultHandler resultHandler, + FaultHandler faultHandler) { String fromAddress = btcWalletService.getOrCreateAddressEntry(trade.getId(), AddressEntry.Context.TRADE_PAYOUT).getAddressString(); FutureCallback callback = new FutureCallback<>() { @@ -487,7 +493,8 @@ public void onFailure(@NotNull Throwable t) { } }; try { - btcWalletService.sendFunds(fromAddress, toAddress, amount, fee, aesKey, AddressEntry.Context.TRADE_PAYOUT, callback); + btcWalletService.sendFunds(fromAddress, toAddress, amount, fee, aesKey, + AddressEntry.Context.TRADE_PAYOUT, memo, callback); } catch (AddressFormatException | InsufficientMoneyException | AddressEntryException e) { e.printStackTrace(); log.error(e.getMessage()); diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java index 449859a9f63..1dbb453a22a 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcTradesService.java @@ -144,7 +144,8 @@ public void keepFunds(KeepFundsRequest req, public void withdrawFunds(WithdrawFundsRequest req, StreamObserver responseObserver) { try { - coreApi.withdrawFunds(req.getTradeId(), req.getAddress()); + //TODO @ghubstan Feel free to add a memo param for withdrawal requests (was just added in UI) + coreApi.withdrawFunds(req.getTradeId(), req.getAddress(), null); var reply = WithdrawFundsReply.newBuilder().build(); responseObserver.onNext(reply); responseObserver.onCompleted(); diff --git a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java index 76340bb38e8..26adadf8e3f 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java @@ -494,14 +494,19 @@ private void doWithdraw(Coin amount, Coin fee, FutureCallback callb private void sendFunds(Coin amount, Coin fee, KeyParameter aesKey, FutureCallback callback) { try { + String memo = withdrawMemoTextField.getText(); + if (memo.isEmpty()) { + memo = null; + } Transaction transaction = btcWalletService.sendFundsForMultipleAddresses(fromAddresses, withdrawToTextField.getText(), amount, fee, null, aesKey, + memo, callback); - transaction.setMemo(withdrawMemoTextField.getText()); + reset(); updateList(); } catch (AddressFormatException e) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index 59f33ef27c9..b3e75bf15fe 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -209,6 +209,7 @@ public void onWithdrawRequest(String toAddress, Coin amount, Coin fee, KeyParameter aesKey, + @Nullable String memo, ResultHandler resultHandler, FaultHandler faultHandler) { checkNotNull(getTrade(), "trade must not be null"); @@ -220,6 +221,7 @@ public void onWithdrawRequest(String toAddress, fee, aesKey, getTrade(), + memo, () -> { resultHandler.handleResult(); selectBestItem(); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java index a8a1bf026ec..b6dbf081bdc 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java @@ -74,7 +74,7 @@ public class BuyerStep4View extends TradeStepView { // private final ChangeListener focusedPropertyListener; - private InputTextField withdrawAddressTextField; + private InputTextField withdrawAddressTextField, withdrawMemoTextField; private Button withdrawToExternalWalletButton, useSavingsWalletButton; private TitledGroupBg withdrawTitledGroupBg; @@ -131,10 +131,17 @@ protected void addContent() { withdrawTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 1, Res.get("portfolio.pending.step5_buyer.withdrawBTC"), Layout.COMPACT_GROUP_DISTANCE); withdrawTitledGroupBg.getStyleClass().add("last"); addCompactTopLabelTextField(gridPane, gridRow, Res.get("portfolio.pending.step5_buyer.amount"), model.getPayoutAmount(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + withdrawAddressTextField = addInputTextField(gridPane, ++gridRow, Res.get("portfolio.pending.step5_buyer.withdrawToAddress")); withdrawAddressTextField.setManaged(false); withdrawAddressTextField.setVisible(false); + withdrawMemoTextField = addInputTextField(gridPane, ++gridRow, + Res.get("funds.withdrawal.memoLabel", Res.getBaseCurrencyCode())); + withdrawMemoTextField.setPromptText(Res.get("funds.withdrawal.memo")); + withdrawMemoTextField.setManaged(false); + withdrawMemoTextField.setVisible(false); + HBox hBox = new HBox(); hBox.setSpacing(10); useSavingsWalletButton = new AutoTooltipButton(Res.get("portfolio.pending.step5_buyer.moveToBisqWallet")); @@ -170,7 +177,9 @@ protected void addContent() { private void onWithdrawal() { withdrawAddressTextField.setManaged(true); withdrawAddressTextField.setVisible(true); - GridPane.setRowSpan(withdrawTitledGroupBg, 2); + withdrawMemoTextField.setManaged(true); + withdrawMemoTextField.setVisible(true); + GridPane.setRowSpan(withdrawTitledGroupBg, 3); withdrawToExternalWalletButton.setDefaultButton(true); useSavingsWalletButton.setDefaultButton(false); withdrawToExternalWalletButton.getStyleClass().add("action-button"); @@ -271,10 +280,15 @@ private void doWithdrawRequest(String toAddress, FaultHandler faultHandler) { useSavingsWalletButton.setDisable(true); withdrawToExternalWalletButton.setDisable(true); + String memo = withdrawMemoTextField.getText(); + if (memo.isEmpty()) { + memo = null; + } model.dataModel.onWithdrawRequest(toAddress, amount, fee, aesKey, + memo, resultHandler, faultHandler); }