Delay payout if buyers account got flagged as risky by filter data

Signed-off-by: HenrikJannsen <boilingfrog@gmx.com>
This commit is contained in:
HenrikJannsen 2023-10-08 18:43:35 +07:00 committed by Alejandro García
parent 3a25eef64a
commit 01bd8bb86e
No known key found for this signature in database
GPG Key ID: F806F422E222AA02
3 changed files with 109 additions and 8 deletions

View File

@ -912,6 +912,7 @@ portfolio.pending.step3_seller.xmrTxHash=Transaction ID
portfolio.pending.step3_seller.xmrTxKey=Transaction key portfolio.pending.step3_seller.xmrTxKey=Transaction key
portfolio.pending.step3_seller.buyersAccount=Buyers account data portfolio.pending.step3_seller.buyersAccount=Buyers account data
portfolio.pending.step3_seller.confirmReceipt=Confirm payment receipt portfolio.pending.step3_seller.confirmReceipt=Confirm payment receipt
portfolio.pending.step3_seller.releaseBitcoin=Release Bitcoin
portfolio.pending.step3_seller.showBsqWallet=Show payment in BSQ wallet portfolio.pending.step3_seller.showBsqWallet=Show payment in BSQ wallet
portfolio.pending.step3_seller.buyerStartedPayment=The BTC buyer has started the {0} payment.\n{1} portfolio.pending.step3_seller.buyerStartedPayment=The BTC buyer has started the {0} payment.\n{1}
portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Check for blockchain confirmations at your altcoin wallet or block explorer and confirm the payment when you have sufficient blockchain confirmations. portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Check for blockchain confirmations at your altcoin wallet or block explorer and confirm the payment when you have sufficient blockchain confirmations.
@ -922,6 +923,12 @@ portfolio.pending.step3_seller.warn.part2=You still have not confirmed the recei
Please check {0} if you have received the payment. Please check {0} if you have received the payment.
portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt of the payment!\n\ portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt of the payment!\n\
The max. period for the trade has elapsed.\nPlease confirm or request assistance from the mediator. The max. period for the trade has elapsed.\nPlease confirm or request assistance from the mediator.
portfolio.pending.step3_seller.delayedPayout=Trade with ID ''{0}'' has been flagged as high risk.\n\n\
You need to wait releasing the Bitcoin to the buyer until {1} to reduce chargeback risks.\n\n\
Please note, that this does not mean that the buyer''s account got flagged but maybe their bank or the payment method was used for chargeback fraud in the past.\n\n\
You can also request mediation and tell the mediator that this trade got flagged as high risk. The mediator can try to verify the account ownership of the buyer and if nothing is suspicious suggest an earlier payout.
# suppress inspection "TrailingSpacesInProperty" # suppress inspection "TrailingSpacesInProperty"
portfolio.pending.step3_seller.onPaymentReceived.part1=Have you received the {0} payment from your trading partner?\n\n portfolio.pending.step3_seller.onPaymentReceived.part1=Have you received the {0} payment from your trading partner?\n\n
# suppress inspection "TrailingSpacesInProperty" # suppress inspection "TrailingSpacesInProperty"
@ -3033,6 +3040,7 @@ filterWindow.offers=Filtered offers (comma sep.)
filterWindow.onions=Banned from trading addresses (comma sep.) filterWindow.onions=Banned from trading addresses (comma sep.)
filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.) filterWindow.bannedFromNetwork=Banned from network addresses (comma sep.)
filterWindow.accounts=Filtered trading account data:\nFormat: comma sep. list of [payment method id | data field | value] filterWindow.accounts=Filtered trading account data:\nFormat: comma sep. list of [payment method id | data field | value]
filterWindow.delayedPayout=Require delayed payout:\nFormat: comma sep. list of [payment method id | data field | value]
filterWindow.bannedCurrencies=Filtered currency codes (comma sep.) filterWindow.bannedCurrencies=Filtered currency codes (comma sep.)
filterWindow.bannedPaymentMethods=Filtered payment method IDs (comma sep.) filterWindow.bannedPaymentMethods=Filtered payment method IDs (comma sep.)
filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys) filterWindow.bannedAccountWitnessSignerPubKeys=Filtered account witness signer pub keys (comma sep. hex of pub keys)

View File

@ -32,6 +32,7 @@ import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.dao.DaoFacade; import bisq.core.dao.DaoFacade;
import bisq.core.filter.FilterManager;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.offer.OfferDirection; import bisq.core.offer.OfferDirection;
@ -62,6 +63,7 @@ import bisq.core.util.coin.CoinFormatter;
import bisq.network.p2p.P2PService; import bisq.network.p2p.P2PService;
import bisq.common.app.DevEnv;
import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.PubKeyRing;
import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.FaultHandler; import bisq.common.handlers.FaultHandler;
@ -88,6 +90,8 @@ import javafx.collections.ObservableList;
import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.KeyParameter;
import java.util.Date; import java.util.Date;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -113,6 +117,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
public final WalletPasswordWindow walletPasswordWindow; public final WalletPasswordWindow walletPasswordWindow;
private final NotificationCenter notificationCenter; private final NotificationCenter notificationCenter;
private final OfferUtil offerUtil; private final OfferUtil offerUtil;
private final FilterManager filterManager;
private final CoinFormatter btcFormatter; private final CoinFormatter btcFormatter;
final ObservableList<PendingTradesListItem> list = FXCollections.observableArrayList(); final ObservableList<PendingTradesListItem> list = FXCollections.observableArrayList();
@ -151,6 +156,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
WalletPasswordWindow walletPasswordWindow, WalletPasswordWindow walletPasswordWindow,
NotificationCenter notificationCenter, NotificationCenter notificationCenter,
OfferUtil offerUtil, OfferUtil offerUtil,
FilterManager filterManager,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter) { @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter) {
this.tradeManager = tradeManager; this.tradeManager = tradeManager;
this.btcWalletService = btcWalletService; this.btcWalletService = btcWalletService;
@ -167,6 +173,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
this.walletPasswordWindow = walletPasswordWindow; this.walletPasswordWindow = walletPasswordWindow;
this.notificationCenter = notificationCenter; this.notificationCenter = notificationCenter;
this.offerUtil = offerUtil; this.offerUtil = offerUtil;
this.filterManager = filterManager;
this.btcFormatter = formatter; this.btcFormatter = formatter;
tradesListChangeListener = change -> onListChanged(); tradesListChangeListener = change -> onListChanged();
@ -695,5 +702,33 @@ public class PendingTradesDataModel extends ActivatableDataModel {
} }
}); });
} }
public boolean requiresPayoutDelay() {
return filterManager.isDelayedPayoutPaymentAccount(Objects.requireNonNull(getTrade()).getProcessModel().getTradePeer().getPaymentAccountPayload());
}
public boolean requiredPayoutDelayHasPassed() {
return getSellerConfirmedPaymentReceiptDate() > 0 && new Date().after(getDelayedPayoutDate());
}
public void setSellerConfirmedPaymentReceiptDate() {
if (getSellerConfirmedPaymentReceiptDate() == 0) {
Objects.requireNonNull(getTrade()).setSellerConfirmedPaymentReceiptDate(new Date().getTime());
tradeManager.requestPersistence();
}
}
public long getSellerConfirmedPaymentReceiptDate() {
return Objects.requireNonNull(getTrade()).getSellerConfirmedPaymentReceiptDate();
}
public Date getDelayedPayoutDate() {
return new Date(Objects.requireNonNull(getTrade()).getSellerConfirmedPaymentReceiptDate() + getPayoutDelay());
}
private long getPayoutDelay() {
return DevEnv.isDevMode() ? TimeUnit.SECONDS.toMillis(10) :
TimeUnit.DAYS.toMillis(14);
}
} }

View File

@ -29,6 +29,7 @@ import bisq.desktop.main.dao.wallet.tx.BsqTxView;
import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.portfolio.pendingtrades.PendingTradesViewModel; import bisq.desktop.main.portfolio.pendingtrades.PendingTradesViewModel;
import bisq.desktop.main.portfolio.pendingtrades.steps.TradeStepView; import bisq.desktop.main.portfolio.pendingtrades.steps.TradeStepView;
import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.GUIUtil; import bisq.desktop.util.GUIUtil;
import bisq.core.locale.Res; import bisq.core.locale.Res;
@ -74,7 +75,9 @@ import org.fxmisc.easybind.Subscription;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -98,6 +101,8 @@ public class SellerStep3View extends TradeStepView {
private TxConfidenceIndicator assetTxConfidenceIndicator; private TxConfidenceIndicator assetTxConfidenceIndicator;
@Nullable @Nullable
private ChangeListener<Number> proofResultListener; private ChangeListener<Number> proofResultListener;
@Nullable
private Timer payoutDelayTimer;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -169,6 +174,8 @@ public class SellerStep3View extends TradeStepView {
applyAssetTxProofResult(trade.getAssetTxProofResult()); applyAssetTxProofResult(trade.getAssetTxProofResult());
} }
updateConfirmButton();
} }
@Override @Override
@ -189,6 +196,11 @@ public class SellerStep3View extends TradeStepView {
if (isXmrTrade()) { if (isXmrTrade()) {
trade.getAssetTxProofResultUpdateProperty().removeListener(proofResultListener); trade.getAssetTxProofResultUpdateProperty().removeListener(proofResultListener);
} }
if (payoutDelayTimer != null) {
payoutDelayTimer.stop();
payoutDelayTimer = null;
}
} }
@ -363,7 +375,7 @@ public class SellerStep3View extends TradeStepView {
protected void updateDisputeState(Trade.DisputeState disputeState) { protected void updateDisputeState(Trade.DisputeState disputeState) {
super.updateDisputeState(disputeState); super.updateDisputeState(disputeState);
confirmButton.setDisable(!trade.confirmPermitted()); updateConfirmButtonDisableState();
} }
@Override @Override
@ -475,14 +487,60 @@ public class SellerStep3View extends TradeStepView {
private void confirmPaymentReceived() { private void confirmPaymentReceived() {
log.info("User pressed the [Confirm payment receipt] button for Trade {}", trade.getShortId()); log.info("User pressed the [Confirm payment receipt] button for Trade {}", trade.getShortId());
busyAnimation.play();
statusLabel.setText(Res.get("shared.sendingConfirmation"));
model.dataModel.onFiatPaymentReceived(() -> { model.dataModel.setSellerConfirmedPaymentReceiptDate();
}, errorMessage -> { updateConfirmButton();
busyAnimation.stop(); if (!model.dataModel.requiresPayoutDelay() || model.dataModel.requiredPayoutDelayHasPassed()) {
new Popup().warning(Res.get("popup.warning.sendMsgFailed")).show(); onReleaseBitcoin();
}); }
}
private void updateConfirmButton() {
if (model.dataModel.requiresPayoutDelay() &&
model.dataModel.getSellerConfirmedPaymentReceiptDate() > 0) {
if (payoutDelayTimer == null) {
confirmButton.setText(Res.get("portfolio.pending.step3_seller.releaseBitcoin"));
confirmButton.setOnAction(e -> onReleaseBitcoin());
payoutDelayTimer = UserThread.runPeriodically(this::updateConfirmButtonDisableState, 1, TimeUnit.SECONDS);
}
if (!model.dataModel.requiredPayoutDelayHasPassed()) {
new Popup().warning(Res.get("portfolio.pending.step3_seller.delayedPayout",
Objects.requireNonNull(model.dataModel.getTrade()).getShortId(),
DisplayUtils.formatDateTime(model.dataModel.getDelayedPayoutDate())))
.closeButtonText(Res.get("shared.iUnderstand"))
.show();
}
} else {
if (payoutDelayTimer != null) {
payoutDelayTimer.stop();
payoutDelayTimer = null;
}
confirmButton.setText(Res.get("portfolio.pending.step3_seller.confirmReceipt"));
confirmButton.setOnAction(e -> onPaymentReceived());
}
updateConfirmButtonDisableState();
}
private void updateConfirmButtonDisableState() {
boolean confirmedReceiptAndRequiredDelayNotPassed = model.dataModel.getSellerConfirmedPaymentReceiptDate() > 0 &&
model.dataModel.requiresPayoutDelay() &&
!model.dataModel.requiredPayoutDelayHasPassed();
confirmButton.setDisable(!trade.confirmPermitted() || confirmedReceiptAndRequiredDelayNotPassed);
}
private void onReleaseBitcoin() {
if (!model.dataModel.requiresPayoutDelay() || model.dataModel.requiredPayoutDelayHasPassed()) {
busyAnimation.play();
statusLabel.setText(Res.get("shared.sendingConfirmation"));
model.dataModel.onFiatPaymentReceived(() -> {
}, errorMessage -> {
busyAnimation.stop();
new Popup().warning(Res.get("popup.warning.sendMsgFailed")).show();
});
} else {
log.error("Release Bitcoin is only permitted if no delayed payout is required or delay has passed");
}
} }
private Optional<String> getOptionalHolderName() { private Optional<String> getOptionalHolderName() {