Skip to content

Commit

Permalink
Add preliminary code to publish warning tx if mediation fails
Browse files Browse the repository at this point in the history
Adapt the existing workflow of starting a second-round arbitration
process, upon mediation failure, to the v5 trade protocol, by giving the
trader an option to broadcast his warning tx. This replaces the current
(tertiary) action of broadcasting the (v4 protocol) delayed payout tx to
start arbitration, on the mediation result popup. Instead, the trader
must now wait for the peer to see the warning tx and actually start
arbitration by broadcasting his redirect tx. (This second part is not
yet implemented.)

Also clean up 'DisputeValidation' slightly and prevent the errant
display of a duplicate-DPT-detected message in the event that a dispute
has a missing delayed payout txId (as is currently the case for v5
protocol trades). Fix the logic similarly for missing trade IDs &
deposit txIds.

TODO: Allow peer to start arbitration by broadcasting his redirect tx,
 upon detection (via a suitable listener) of a warning tx broadcast.
  • Loading branch information
stejbac committed Sep 14, 2024
1 parent cf4016e commit ce5087c
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 48 deletions.
33 changes: 14 additions & 19 deletions core/src/main/java/bisq/core/support/dispute/DisputeValidation.java
Original file line number Diff line number Diff line change
Expand Up @@ -218,22 +218,16 @@ private static Tuple3<Map<String, Set<String>>, Map<String, Set<String>>, Map<St
String uid = dispute.getUid();

String tradeId = dispute.getTradeId();
disputesPerTradeId.putIfAbsent(tradeId, new HashSet<>());
Set<String> set = disputesPerTradeId.get(tradeId);
set.add(uid);
disputesPerTradeId.computeIfAbsent(tradeId, id -> new HashSet<>()).add(uid);

String delayedPayoutTxId = dispute.getDelayedPayoutTxId();
if (delayedPayoutTxId != null) {
disputesPerDelayedPayoutTxId.putIfAbsent(delayedPayoutTxId, new HashSet<>());
set = disputesPerDelayedPayoutTxId.get(delayedPayoutTxId);
set.add(uid);
disputesPerDelayedPayoutTxId.computeIfAbsent(delayedPayoutTxId, id -> new HashSet<>()).add(uid);
}

String depositTxId = dispute.getDepositTxId();
if (depositTxId != null) {
disputesPerDepositTxId.putIfAbsent(depositTxId, new HashSet<>());
set = disputesPerDepositTxId.get(depositTxId);
set.add(uid);
disputesPerDepositTxId.computeIfAbsent(depositTxId, id -> new HashSet<>()).add(uid);
}
});

Expand All @@ -255,6 +249,7 @@ private static void testIfDisputeTriesReplay(Dispute disputeToTest,
// So until all users have updated to 1.4.0 we only check in refund agent case. With 1.4.0 we send the
// delayed payout tx also in mediation cases and that if check can be removed.
if (disputeToTest.getSupportType() == SupportType.REFUND) {
// TODO: Handle v5 protocol trades, which have no delayed payout tx.
checkNotNull(disputeToTestDelayedPayoutTxId,
"Delayed payout transaction ID is null. " +
"Trade ID: " + disputeToTestTradeId);
Expand All @@ -265,20 +260,20 @@ private static void testIfDisputeTriesReplay(Dispute disputeToTest,
"agentsUid must not be null. Trade ID: " + disputeToTestTradeId);

Set<String> disputesPerTradeIdItems = disputesPerTradeId.get(disputeToTestTradeId);
checkArgument(disputesPerTradeIdItems != null && disputesPerTradeIdItems.size() <= 2,
"We found more then 2 disputes with the same trade ID. " +
"Trade ID: " + disputeToTestTradeId);
checkArgument(disputesPerTradeIdItems == null || disputesPerTradeIdItems.size() <= 2,
"We found more than 2 disputes with the same trade ID. " +
"Trade ID: %s", disputeToTestTradeId);
if (!disputesPerDelayedPayoutTxId.isEmpty()) {
Set<String> disputesPerDelayedPayoutTxIdItems = disputesPerDelayedPayoutTxId.get(disputeToTestDelayedPayoutTxId);
checkArgument(disputesPerDelayedPayoutTxIdItems != null && disputesPerDelayedPayoutTxIdItems.size() <= 2,
"We found more then 2 disputes with the same delayedPayoutTxId. " +
"Trade ID: " + disputeToTestTradeId);
checkArgument(disputesPerDelayedPayoutTxIdItems == null || disputesPerDelayedPayoutTxIdItems.size() <= 2,
"We found more than 2 disputes with the same delayedPayoutTxId. " +
"Trade ID: %s", disputeToTestTradeId);
}
if (!disputesPerDepositTxId.isEmpty()) {
Set<String> disputesPerDepositTxIdItems = disputesPerDepositTxId.get(disputeToTestDepositTxId);
checkArgument(disputesPerDepositTxIdItems != null && disputesPerDepositTxIdItems.size() <= 2,
"We found more then 2 disputes with the same depositTxId. " +
"Trade ID: " + disputeToTestTradeId);
checkArgument(disputesPerDepositTxIdItems == null || disputesPerDepositTxIdItems.size() <= 2,
"We found more than 2 disputes with the same depositTxId. " +
"Trade ID: %s", disputeToTestTradeId);
}
} catch (IllegalArgumentException e) {
throw new DisputeReplayException(disputeToTest, e.getMessage());
Expand All @@ -287,7 +282,7 @@ private static void testIfDisputeTriesReplay(Dispute disputeToTest,
"disputeToTest={}, disputesPerTradeId={}, disputesPerDelayedPayoutTxId={}, " +
"disputesPerDepositTxId={}",
disputeToTest, disputesPerTradeId, disputesPerDelayedPayoutTxId, disputesPerDepositTxId);
throw new DisputeReplayException(disputeToTest, e.toString() + " at dispute " + disputeToTest.toString());
throw new DisputeReplayException(disputeToTest, e + " at dispute " + disputeToTest);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
import bisq.core.trade.protocol.bisq_v1.tasks.mediation.SendMediatedPayoutTxPublishedMessage;
import bisq.core.trade.protocol.bisq_v1.tasks.mediation.SetupMediatedPayoutTxListener;
import bisq.core.trade.protocol.bisq_v1.tasks.mediation.SignMediatedPayoutTx;
import bisq.core.trade.protocol.bisq_v5.messages.DepositTxAndSellerPaymentAccountMessage;
import bisq.core.trade.protocol.bisq_v5.tasks.arbitration.PublishWarningTx;

import bisq.network.p2p.AckMessage;
import bisq.network.p2p.NodeAddress;
Expand All @@ -51,13 +53,13 @@

@Slf4j
public class DisputeProtocol extends TradeProtocol {

protected Trade trade;
protected final ProcessModel processModel;

enum DisputeEvent implements FluentProtocol.Event {
MEDIATION_RESULT_ACCEPTED,
MEDIATION_RESULT_REJECTED,
WARNING_SENT,
ARBITRATION_REQUESTED
}

Expand All @@ -84,7 +86,8 @@ protected void onAckMessage(AckMessage ackMessage, NodeAddress peer) {
// as we support automatic re-send of the msg in case it was not ACKed after a certain time
if (ackMessage.getSourceMsgClassName().equals(CounterCurrencyTransferStartedMessage.class.getSimpleName())) {
processModel.setPaymentStartedAckMessage(ackMessage);
} else if (ackMessage.getSourceMsgClassName().equals(DepositTxAndDelayedPayoutTxMessage.class.getSimpleName())) {
} else if (ackMessage.getSourceMsgClassName().equals(DepositTxAndDelayedPayoutTxMessage.class.getSimpleName()) ||
ackMessage.getSourceMsgClassName().equals(DepositTxAndSellerPaymentAccountMessage.class.getSimpleName())) {
processModel.setDepositTxSentAckMessage(ackMessage);
}

Expand Down Expand Up @@ -206,6 +209,22 @@ public void onPublishDelayedPayoutTx(ResultHandler resultHandler, ErrorMessageHa
}


///////////////////////////////////////////////////////////////////////////////////////////
// Warning tx
///////////////////////////////////////////////////////////////////////////////////////////

public void onPublishWarningTx(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
DisputeEvent event = DisputeEvent.WARNING_SENT;
expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED,
Trade.Phase.FIAT_SENT,
Trade.Phase.FIAT_RECEIVED)
.with(event)
.preCondition(trade.hasV5Protocol()))
.setup(tasks(PublishWarningTx.class))
.executeTasks();
}


///////////////////////////////////////////////////////////////////////////////////////////
// Peer has published the delayed payout tx
///////////////////////////////////////////////////////////////////////////////////////////
Expand Down
31 changes: 29 additions & 2 deletions core/src/main/resources/i18n/displayStrings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -1037,10 +1037,27 @@ portfolio.pending.mediationResult.popup.info=The mediator has suggested the foll
Please inform the mediator if the trade is not paid out in the next 48h.\n\n\
If agreement is not possible, you will have to wait until {2} (block {3}) to Send to Arbitration,\
which will open a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their findings.\n\n\
If the trade goes to arbitration the arbitrator will pay out the trade amount plus one peer's security deposit. \
If the trade goes to arbitration the arbitrator will pay out the trade amount plus one peer''s security deposit. \
This means the total arbitration payout will be less than the mediation payout. \
Requesting arbitration is meant for exceptional circumstances. such as; \
one peer not responding, or disputing the mediator made a fair payout suggestion. \n\n\
one peer not responding, or disputing the mediator made a fair payout suggestion.\n\n\
More details about the arbitration model: [HYPERLINK:https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration]
portfolio.pending.mediationResult.popup.info.v5=The mediator has suggested the following payout:\n\
You receive: {0}\n\
Your trading peer receives: {1}\n\n\
You can accept or reject this suggested payout.\n\n\
By accepting, you sign the proposed payout transaction. \
Mediation is expected to be the optimal resolution for both traders. \
If your trading peer also accepts and signs, the payout will be completed, and the trade will be closed. \
Please inform the mediator if the trade is not paid out in the next 48h.\n\n\
If agreement is not possible, you will have to wait until {2} (block {3}) to Send a Warning to your trading peer, \
by broadcasting your warning transaction. The peer will then have a further 10 days (1440 blocks) to respond, by \
opening a second-round dispute with an arbitrator who will investigate the case again and do a payout based on their \
findings. If the peer does not respond in time, you can automatically claim the entire trade collateral.\n\n\
If the trade goes to arbitration the arbitrator will pay out the trade amount plus one peer''s security deposit. \
This means the total arbitration payout will be less than the mediation payout. \
Sending a warning or requesting arbitration is meant for exceptional circumstances. such as; \
one peer not responding, or disputing the mediator made a fair payout suggestion.\n\n\
More details about the arbitration model: [HYPERLINK:https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration]
portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout \
but it seems that your trading peer has not yet accepted it. \
Expand All @@ -1049,8 +1066,18 @@ portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accep
investigate the case again and do a payout based on their findings.\n\n\
You can find more details about the arbitration model at: \
[HYPERLINK:https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration]
portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver.v5=You have accepted the mediator''s suggested payout \
but it seems that your trading peer has not yet accepted it. \
Inform your mediator that you have accepted mediation if your peer has not accepted the mediation suggestion in 48h.\n\n\
Once the lock time is over on {0} (block {1}), you can Send a Warning to your trading peer, by broadcasting your \
warning transaction. The peer will then have a further 10 days (1440 blocks) to respond, by opening a second-round \
dispute with an arbitrator who will investigate the case again and do a payout based on their findings. If the peer \
does not respond in time, you can automatically claim the entire trade collateral.\n\n\
You can find more details about the arbitration model at: \
[HYPERLINK:https://bisq.wiki/Dispute_resolution#Level_3:_Arbitration]
portfolio.pending.mediationResult.popup.reject=Reject
portfolio.pending.mediationResult.popup.openArbitration=Send to arbitration
portfolio.pending.mediationResult.popup.sendWarning=Send warning to peer
portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted

portfolio.pending.failedTrade.taker.missingTakerFeeTx=The taker fee transaction is missing.\n\n\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,19 @@ public void onOpenDispute() {
tryOpenDispute(false);
}

public void onSendWarning() {
Trade trade = getTrade();
if (trade == null) {
log.error("Trade is null");
return;
}
// TODO: What if the peer has already broadcast his warning tx? We need to detect that.
trade.setDisputeState(Trade.DisputeState.WARNING_SENT);
((DisputeProtocol) tradeManager.getTradeProtocol(trade)).onPublishWarningTx(
() -> log.info("Warning tx published"),
errorMessage -> new Popup().error(errorMessage).show());
}

public void onOpenSupportTicket() {
tryOpenDispute(true);
}
Expand Down
Loading

0 comments on commit ce5087c

Please sign in to comment.