haveno/desktop/src/main/java/bisq/desktop/main/overlays/windows/OfferDetailsWindow.java
2021-12-16 13:33:12 -05:00

436 lines
20 KiB
Java

/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.overlays.windows;
import bisq.desktop.Navigation;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.BusyAnimation;
import bisq.desktop.components.TitledGroupBg;
import bisq.desktop.components.TxIdTextField;
import bisq.desktop.main.overlays.Overlay;
import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.GUIUtil;
import bisq.desktop.util.Layout;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.locale.BankUtil;
import bisq.core.locale.CountryUtil;
import bisq.core.locale.Res;
import bisq.core.monetary.Price;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferUtil;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.user.User;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter;
import bisq.common.crypto.KeyRing;
import bisq.common.util.Tuple2;
import bisq.common.util.Tuple4;
import org.bitcoinj.core.Coin;
import javax.inject.Inject;
import javax.inject.Named;
import com.google.common.base.Joiner;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import java.util.List;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static bisq.desktop.util.FormBuilder.*;
public class OfferDetailsWindow extends Overlay<OfferDetailsWindow> {
protected static final Logger log = LoggerFactory.getLogger(OfferDetailsWindow.class);
private final CoinFormatter formatter;
private final User user;
private final KeyRing keyRing;
private final Navigation navigation;
private final BtcWalletService btcWalletService;
private Offer offer;
private Coin tradeAmount;
private Price tradePrice;
private Optional<Runnable> placeOfferHandlerOptional = Optional.empty();
private Optional<Runnable> takeOfferHandlerOptional = Optional.empty();
private BusyAnimation busyAnimation;
///////////////////////////////////////////////////////////////////////////////////////////
// Public API
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public OfferDetailsWindow(@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
User user,
KeyRing keyRing,
Navigation navigation,
BtcWalletService btcWalletService) {
this.formatter = formatter;
this.user = user;
this.keyRing = keyRing;
this.navigation = navigation;
this.btcWalletService = btcWalletService;
type = Type.Confirmation;
}
public void show(Offer offer, Coin tradeAmount, Price tradePrice) {
this.offer = offer;
this.tradeAmount = tradeAmount;
this.tradePrice = tradePrice;
rowIndex = -1;
width = 1118;
createGridPane();
addContent();
display();
}
public void show(Offer offer) {
this.offer = offer;
rowIndex = -1;
width = 1118;
createGridPane();
addContent();
display();
}
public OfferDetailsWindow onPlaceOffer(Runnable placeOfferHandler) {
this.placeOfferHandlerOptional = Optional.of(placeOfferHandler);
return this;
}
public OfferDetailsWindow onTakeOffer(Runnable takeOfferHandler) {
this.takeOfferHandlerOptional = Optional.of(takeOfferHandler);
return this;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Protected
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected void onHidden() {
if (busyAnimation != null)
busyAnimation.stop();
}
@Override
protected void createGridPane() {
super.createGridPane();
gridPane.setPadding(new Insets(35, 40, 30, 40));
gridPane.getStyleClass().add("grid-pane");
}
private void addContent() {
gridPane.getColumnConstraints().get(0).setMinWidth(224);
int rows = 5;
List<String> acceptedBanks = offer.getAcceptedBankIds();
boolean showAcceptedBanks = acceptedBanks != null && !acceptedBanks.isEmpty();
List<String> acceptedCountryCodes = offer.getAcceptedCountryCodes();
boolean showAcceptedCountryCodes = acceptedCountryCodes != null && !acceptedCountryCodes.isEmpty();
boolean isF2F = offer.getPaymentMethod().equals(PaymentMethod.F2F);
boolean showExtraInfo = offer.getPaymentMethod().equals(PaymentMethod.F2F) || offer.getPaymentMethod().equals(PaymentMethod.CASH_BY_MAIL);
if (!takeOfferHandlerOptional.isPresent())
rows++;
if (showAcceptedBanks)
rows++;
if (showAcceptedCountryCodes)
rows++;
if (showExtraInfo)
rows++;
if (isF2F)
rows++;
boolean showXmrAutoConf = offer.isXmr() && offer.getDirection() == OfferPayload.Direction.SELL;
if (showXmrAutoConf) {
rows++;
}
addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.Offer"));
String fiatDirectionInfo = "";
String btcDirectionInfo = "";
OfferPayload.Direction direction = offer.getDirection();
String currencyCode = offer.getCurrencyCode();
String offerTypeLabel = Res.get("shared.offerType");
String toReceive = " " + Res.get("shared.toReceive");
String toSpend = " " + Res.get("shared.toSpend");
double firstRowDistance = Layout.TWICE_FIRST_ROW_DISTANCE;
if (takeOfferHandlerOptional.isPresent()) {
addConfirmationLabelLabel(gridPane, rowIndex, offerTypeLabel,
DisplayUtils.getDirectionForTakeOffer(direction, currencyCode), firstRowDistance);
fiatDirectionInfo = direction == OfferPayload.Direction.BUY ? toReceive : toSpend;
btcDirectionInfo = direction == OfferPayload.Direction.SELL ? toReceive : toSpend;
} else if (placeOfferHandlerOptional.isPresent()) {
addConfirmationLabelLabel(gridPane, rowIndex, offerTypeLabel,
DisplayUtils.getOfferDirectionForCreateOffer(direction, currencyCode), firstRowDistance);
fiatDirectionInfo = direction == OfferPayload.Direction.SELL ? toReceive : toSpend;
btcDirectionInfo = direction == OfferPayload.Direction.BUY ? toReceive : toSpend;
} else {
addConfirmationLabelLabel(gridPane, rowIndex, offerTypeLabel,
DisplayUtils.getDirectionBothSides(direction, currencyCode), firstRowDistance);
}
String btcAmount = Res.get("shared.btcAmount");
if (takeOfferHandlerOptional.isPresent()) {
addConfirmationLabelLabel(gridPane, ++rowIndex, btcAmount + btcDirectionInfo,
formatter.formatCoinWithCode(tradeAmount));
addConfirmationLabelLabel(gridPane, ++rowIndex, DisplayUtils.formatVolumeLabel(currencyCode) + fiatDirectionInfo,
DisplayUtils.formatVolumeWithCode(offer.getVolumeByAmount(tradeAmount)));
} else {
addConfirmationLabelLabel(gridPane, ++rowIndex, btcAmount + btcDirectionInfo,
formatter.formatCoinWithCode(offer.getAmount()));
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.minBtcAmount"),
formatter.formatCoinWithCode(offer.getMinAmount()));
String volume = DisplayUtils.formatVolumeWithCode(offer.getVolume());
String minVolume = "";
if (offer.getVolume() != null && offer.getMinVolume() != null &&
!offer.getVolume().equals(offer.getMinVolume()))
minVolume = " " + Res.get("offerDetailsWindow.min", DisplayUtils.formatVolumeWithCode(offer.getMinVolume()));
addConfirmationLabelLabel(gridPane, ++rowIndex,
DisplayUtils.formatVolumeLabel(currencyCode) + fiatDirectionInfo, volume + minVolume);
}
String priceLabel = Res.get("shared.price");
if (takeOfferHandlerOptional.isPresent()) {
addConfirmationLabelLabel(gridPane, ++rowIndex, priceLabel, FormattingUtils.formatPrice(tradePrice));
} else {
Price price = offer.getPrice();
if (offer.isUseMarketBasedPrice()) {
addConfirmationLabelLabel(gridPane, ++rowIndex, priceLabel, FormattingUtils.formatPrice(price) +
" " + Res.get("offerDetailsWindow.distance",
FormattingUtils.formatPercentagePrice(offer.getMarketPriceMargin())));
} else {
addConfirmationLabelLabel(gridPane, ++rowIndex, priceLabel, FormattingUtils.formatPrice(price));
}
}
final PaymentMethod paymentMethod = offer.getPaymentMethod();
String bankId = offer.getBankId();
if (bankId == null || bankId.equals("null"))
bankId = "";
else
bankId = " (" + bankId + ")";
final boolean isSpecificBanks = paymentMethod.equals(PaymentMethod.SPECIFIC_BANKS);
final boolean isNationalBanks = paymentMethod.equals(PaymentMethod.NATIONAL_BANK);
final boolean isSepa = paymentMethod.equals(PaymentMethod.SEPA);
final String makerPaymentAccountId = offer.getMakerPaymentAccountId();
final PaymentAccount myPaymentAccount = user.getPaymentAccount(makerPaymentAccountId);
String countryCode = offer.getCountryCode();
if (offer.isMyOffer(keyRing) && makerPaymentAccountId != null && myPaymentAccount != null) {
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.myTradingAccount"), myPaymentAccount.getAccountName());
} else {
final String method = Res.get(paymentMethod.getId());
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.paymentMethod"), method);
}
if (showXmrAutoConf) {
String isAutoConf = offer.isXmrAutoConf() ?
Res.get("shared.yes") :
Res.get("shared.no");
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerbook.xmrAutoConf"), isAutoConf);
}
if (showAcceptedBanks) {
if (paymentMethod.equals(PaymentMethod.SAME_BANK)) {
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.bankId"), acceptedBanks.get(0));
} else if (isSpecificBanks) {
String value = Joiner.on(", ").join(acceptedBanks);
String acceptedBanksLabel = Res.get("shared.acceptedBanks");
Tooltip tooltip = new Tooltip(acceptedBanksLabel + " " + value);
Label acceptedBanksTextField = addConfirmationLabelLabel(gridPane, ++rowIndex, acceptedBanksLabel, value).second;
acceptedBanksTextField.setMouseTransparent(false);
acceptedBanksTextField.setTooltip(tooltip);
}
}
if (showAcceptedCountryCodes) {
String countries;
Tooltip tooltip = null;
if (CountryUtil.containsAllSepaEuroCountries(acceptedCountryCodes)) {
countries = Res.get("shared.allEuroCountries");
} else {
if (acceptedCountryCodes.size() == 1) {
countries = CountryUtil.getNameAndCode(acceptedCountryCodes.get(0));
tooltip = new Tooltip(countries);
} else {
countries = CountryUtil.getCodesString(acceptedCountryCodes);
tooltip = new Tooltip(CountryUtil.getNamesByCodesString(acceptedCountryCodes));
}
}
Label acceptedCountries = addConfirmationLabelLabel(gridPane, ++rowIndex,
Res.get("shared.acceptedTakerCountries"), countries).second;
if (tooltip != null) {
acceptedCountries.setMouseTransparent(false);
acceptedCountries.setTooltip(tooltip);
}
}
if (isF2F) {
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("payment.f2f.city"), offer.getF2FCity());
}
if (showExtraInfo) {
TextArea textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("payment.shared.extraInfo"), "", 0).second;
textArea.setText(offer.getExtraInfo());
textArea.setMinHeight(33);
textArea.setMaxHeight(textArea.getMinHeight());
textArea.setEditable(false);
}
rows = 3;
if (countryCode != null)
rows++;
if (!isF2F)
rows++;
// At create offer we do not show the makerFeeTxId
if (!placeOfferHandlerOptional.isPresent()) {
rows++;
}
TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, ++rowIndex, rows, Res.get("shared.details"), Layout.GROUP_DISTANCE);
addConfirmationLabelTextFieldWithCopyIcon(gridPane, rowIndex, Res.get("shared.offerId"), offer.getId(),
Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE);
addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("offerDetailsWindow.makersOnion"),
offer.getMakerNodeAddress().getFullAddress());
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.creationDate"),
DisplayUtils.formatDateTime(offer.getDate()));
String value = Res.getWithColAndCap("shared.buyer") +
" " +
formatter.formatCoinWithCode(offer.getBuyerSecurityDeposit()) +
" / " +
Res.getWithColAndCap("shared.seller") +
" " +
formatter.formatCoinWithCode(offer.getSellerSecurityDeposit());
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), value);
// At create offer we do not show the makerFeeTxId
if (!placeOfferHandlerOptional.isPresent()) {
TxIdTextField makerFeeTxIdTextField = addLabelTxIdTextField(gridPane, ++rowIndex,
Res.get("shared.makerFeeTxId"), offer.getOfferFeePaymentTxId()).second;
int finalRows = rows;
OfferUtil.getInvalidMakerFeeTxErrorMessage(offer, btcWalletService)
.ifPresent(errorMsg -> {
makerFeeTxIdTextField.getTextField().setId("address-text-field-error");
GridPane.setRowSpan(titledGroupBg, finalRows + 1);
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.errorMessage"),
errorMsg.replace("\n\n", "\n"));
});
}
if (countryCode != null && !isF2F)
addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.countryBank"),
CountryUtil.getNameAndCode(countryCode));
if (placeOfferHandlerOptional.isPresent()) {
addTitledGroupBg(gridPane, ++rowIndex, 1, Res.get("offerDetailsWindow.commitment"), Layout.GROUP_DISTANCE);
final Tuple2<Label, Label> labelLabelTuple2 = addConfirmationLabelLabel(gridPane, rowIndex, Res.get("offerDetailsWindow.agree"), Res.get("createOffer.tac"),
Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE);
labelLabelTuple2.second.setWrapText(true);
addConfirmAndCancelButtons(true);
} else if (takeOfferHandlerOptional.isPresent()) {
addTitledGroupBg(gridPane, ++rowIndex, 1, Res.get("shared.contract"), Layout.GROUP_DISTANCE);
final Tuple2<Label, Label> labelLabelTuple2 = addConfirmationLabelLabel(gridPane, rowIndex, Res.get("offerDetailsWindow.tac"), Res.get("takeOffer.tac"),
Layout.TWICE_FIRST_ROW_AND_GROUP_DISTANCE);
labelLabelTuple2.second.setWrapText(true);
addConfirmAndCancelButtons(false);
} else {
Button closeButton = addButtonAfterGroup(gridPane, ++rowIndex, Res.get("shared.close"));
GridPane.setColumnIndex(closeButton, 1);
GridPane.setHalignment(closeButton, HPos.RIGHT);
closeButton.setOnAction(e -> {
closeHandlerOptional.ifPresent(Runnable::run);
hide();
});
}
}
private void addConfirmAndCancelButtons(boolean isPlaceOffer) {
boolean isBuyOffer = offer.isBuyOffer();
boolean isBuyerRole = isPlaceOffer == isBuyOffer;
String placeOfferButtonText = isBuyerRole ?
Res.get("offerDetailsWindow.confirm.maker", Res.get("shared.buy")) :
Res.get("offerDetailsWindow.confirm.maker", Res.get("shared.sell"));
String takeOfferButtonText = isBuyerRole ?
Res.get("offerDetailsWindow.confirm.taker", Res.get("shared.buy")) :
Res.get("offerDetailsWindow.confirm.taker", Res.get("shared.sell"));
ImageView iconView = new ImageView();
iconView.setId(isBuyerRole ? "image-buy-white" : "image-sell-white");
Tuple4<Button, BusyAnimation, Label, HBox> placeOfferTuple = addButtonBusyAnimationLabelAfterGroup(gridPane,
++rowIndex, 1,
isPlaceOffer ? placeOfferButtonText : takeOfferButtonText);
AutoTooltipButton button = (AutoTooltipButton) placeOfferTuple.first;
button.setMinHeight(40);
button.setPadding(new Insets(0, 20, 0, 20));
button.setGraphic(iconView);
button.setGraphicTextGap(10);
button.setId(isBuyerRole ? "buy-button-big" : "sell-button-big");
button.updateText(isPlaceOffer ? placeOfferButtonText : takeOfferButtonText);
busyAnimation = placeOfferTuple.second;
Label spinnerInfoLabel = placeOfferTuple.third;
Button cancelButton = new AutoTooltipButton(Res.get("shared.cancel"));
cancelButton.setDefaultButton(false);
cancelButton.setOnAction(e -> {
closeHandlerOptional.ifPresent(Runnable::run);
hide();
});
placeOfferTuple.fourth.getChildren().add(cancelButton);
button.setOnAction(e -> {
if (GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation)) {
button.setDisable(true);
cancelButton.setDisable(true);
// temporarily disabled due to high CPU usage (per issue #4649)
// busyAnimation.play();
if (isPlaceOffer) {
spinnerInfoLabel.setText(Res.get("createOffer.fundsBox.placeOfferSpinnerInfo"));
placeOfferHandlerOptional.ifPresent(Runnable::run);
} else {
spinnerInfoLabel.setText(Res.get("takeOffer.fundsBox.takeOfferSpinnerInfo"));
takeOfferHandlerOptional.ifPresent(Runnable::run);
}
}
});
}
}