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.buyersAccount=Buyers account data
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.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.
@ -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.
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.
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"
portfolio.pending.step3_seller.onPaymentReceived.part1=Have you received the {0} payment from your trading partner?\n\n
# suppress inspection "TrailingSpacesInProperty"
@ -3033,6 +3040,7 @@ filterWindow.offers=Filtered offers (comma sep.)
filterWindow.onions=Banned from trading 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.delayedPayout=Require delayed payout:\nFormat: comma sep. list of [payment method id | data field | value]
filterWindow.bannedCurrencies=Filtered currency codes (comma sep.)
filterWindow.bannedPaymentMethods=Filtered payment method IDs (comma sep.)
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.wallet.BtcWalletService;
import bisq.core.dao.DaoFacade;
import bisq.core.filter.FilterManager;
import bisq.core.locale.Res;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferDirection;
@ -62,6 +63,7 @@ import bisq.core.util.coin.CoinFormatter;
import bisq.network.p2p.P2PService;
import bisq.common.app.DevEnv;
import bisq.common.crypto.PubKeyRing;
import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.FaultHandler;
@ -88,6 +90,8 @@ import javafx.collections.ObservableList;
import org.bouncycastle.crypto.params.KeyParameter;
import java.util.Date;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
@ -113,6 +117,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
public final WalletPasswordWindow walletPasswordWindow;
private final NotificationCenter notificationCenter;
private final OfferUtil offerUtil;
private final FilterManager filterManager;
private final CoinFormatter btcFormatter;
final ObservableList<PendingTradesListItem> list = FXCollections.observableArrayList();
@ -151,6 +156,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
WalletPasswordWindow walletPasswordWindow,
NotificationCenter notificationCenter,
OfferUtil offerUtil,
FilterManager filterManager,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter) {
this.tradeManager = tradeManager;
this.btcWalletService = btcWalletService;
@ -167,6 +173,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
this.walletPasswordWindow = walletPasswordWindow;
this.notificationCenter = notificationCenter;
this.offerUtil = offerUtil;
this.filterManager = filterManager;
this.btcFormatter = formatter;
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.portfolio.pendingtrades.PendingTradesViewModel;
import bisq.desktop.main.portfolio.pendingtrades.steps.TradeStepView;
import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.GUIUtil;
import bisq.core.locale.Res;
@ -74,7 +75,9 @@ import org.fxmisc.easybind.Subscription;
import javafx.beans.value.ChangeListener;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
@ -98,6 +101,8 @@ public class SellerStep3View extends TradeStepView {
private TxConfidenceIndicator assetTxConfidenceIndicator;
@Nullable
private ChangeListener<Number> proofResultListener;
@Nullable
private Timer payoutDelayTimer;
///////////////////////////////////////////////////////////////////////////////////////////
@ -169,6 +174,8 @@ public class SellerStep3View extends TradeStepView {
applyAssetTxProofResult(trade.getAssetTxProofResult());
}
updateConfirmButton();
}
@Override
@ -189,6 +196,11 @@ public class SellerStep3View extends TradeStepView {
if (isXmrTrade()) {
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) {
super.updateDisputeState(disputeState);
confirmButton.setDisable(!trade.confirmPermitted());
updateConfirmButtonDisableState();
}
@Override
@ -475,6 +487,49 @@ public class SellerStep3View extends TradeStepView {
private void confirmPaymentReceived() {
log.info("User pressed the [Confirm payment receipt] button for Trade {}", trade.getShortId());
model.dataModel.setSellerConfirmedPaymentReceiptDate();
updateConfirmButton();
if (!model.dataModel.requiresPayoutDelay() || model.dataModel.requiredPayoutDelayHasPassed()) {
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"));
@ -483,6 +538,9 @@ public class SellerStep3View extends TradeStepView {
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() {