refactor payout protocol

send payment key & multisig hex on deposit confirm for resilience
support payout published, confirmed, unlocked states
keep trade wallets open throughout trade
close and delete trade wallets when payout unlocks
arbitrator idles trade wallets after deposits confirm (1/hour)
This commit is contained in:
woodser 2022-10-26 01:05:09 -04:00
parent 45bac8c264
commit f36dde2857
84 changed files with 1486 additions and 2272 deletions

View file

@ -28,7 +28,7 @@ import bisq.core.offer.placeoffer.tasks.MakerReserveOfferFunds;
import bisq.core.offer.placeoffer.tasks.ValidateOffer;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage;
import bisq.core.trade.protocol.tasks.BuyerProcessPaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.ProcessPaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage;
import bisq.core.trade.protocol.tasks.MakerSetLockTime;
import bisq.core.trade.protocol.tasks.RemoveOffer;
@ -36,8 +36,7 @@ import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.SellerProcessPaymentSentMessage;
import bisq.core.trade.protocol.tasks.SellerPublishDepositTx;
import bisq.core.trade.protocol.tasks.SellerPublishTradeStatistics;
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.SetupPayoutTxListener;
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer;
import bisq.core.trade.protocol.tasks.TakerVerifyMakerFeePayment;
import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
import bisq.common.taskrunner.Task;
@ -109,7 +108,7 @@ public class DebugView extends InitializableView<GridPane, Void> {
TakerVerifyMakerFeePayment.class,
SellerPreparePaymentReceivedMessage.class,
//SellerBroadcastPayoutTx.class, // TODO (woodser): removed from main pipeline; debug view?
SellerSendPaymentReceivedMessage.class
SellerSendPaymentReceivedMessageToBuyer.class
)
));
@ -123,10 +122,9 @@ public class DebugView extends InitializableView<GridPane, Void> {
ApplyFilter.class,
BuyerPreparePaymentSentMessage.class,
SetupPayoutTxListener.class,
BuyerSendPaymentSentMessage.class,
BuyerProcessPaymentReceivedMessage.class
ProcessPaymentReceivedMessage.class
)
));
@ -142,10 +140,9 @@ public class DebugView extends InitializableView<GridPane, Void> {
ApplyFilter.class,
TakerVerifyMakerFeePayment.class,
BuyerPreparePaymentSentMessage.class,
SetupPayoutTxListener.class,
BuyerSendPaymentSentMessage.class,
BuyerProcessPaymentReceivedMessage.class)
ProcessPaymentReceivedMessage.class)
));
addGroup("SellerAsMakerProtocol",
FXCollections.observableArrayList(Arrays.asList(
@ -166,7 +163,7 @@ public class DebugView extends InitializableView<GridPane, Void> {
ApplyFilter.class,
SellerPreparePaymentReceivedMessage.class,
//SellerBroadcastPayoutTx.class, // TODO (woodser): removed from main pipeline; debug view?
SellerSendPaymentReceivedMessage.class
SellerSendPaymentReceivedMessageToBuyer.class
)
));
}

View file

@ -42,7 +42,7 @@ import bisq.core.offer.Offer;
import bisq.core.offer.OfferDirection;
import bisq.core.offer.OpenOfferManager;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.HavenoUtils;
import bisq.core.user.Preferences;
import bisq.core.util.VolumeUtil;

View file

@ -424,8 +424,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
case PAYMENT_RECEIVED:
appendMsg = Res.get("takeOffer.error.depositPublished");
break;
case PAYOUT_PUBLISHED:
case WITHDRAWN:
case COMPLETED:
appendMsg = Res.get("takeOffer.error.payoutPublished");
break;
default:
@ -444,7 +443,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
}
private void applyTradeState() {
if (trade.isTakerFeePublished()) {
if (trade.isDepositRequested()) {
if (takeOfferResultHandler != null)
takeOfferResultHandler.run();

View file

@ -183,12 +183,11 @@ public class NotificationCenter {
private void onTradePhaseChanged(Trade trade, Trade.Phase phase) {
String message = null;
if (trade.isPayoutPublished() && !trade.isWithdrawn()) {
if (trade.isPayoutPublished() && !trade.isCompleted()) {
message = Res.get("notification.trade.completed");
} else {
if (trade instanceof MakerTrade &&
phase.ordinal() == Trade.Phase.DEPOSITS_PUBLISHED.ordinal() ||
phase.ordinal() == Trade.Phase.DEPOSITS_CONFIRMED.ordinal()) {
phase.ordinal() == Trade.Phase.DEPOSITS_PUBLISHED.ordinal()) {
final String role = trade instanceof BuyerTrade ? Res.get("shared.seller") : Res.get("shared.buyer");
message = Res.get("notification.trade.accepted", role);
}

View file

@ -590,7 +590,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
Button cancelButton = tuple.second;
closeTicketButton.setOnAction(e -> {
disputesService.resolveDisputePayout(dispute, disputeResult, contract);
disputesService.applyDisputePayout(dispute, disputeResult, contract);
doClose(closeTicketButton);
// if (dispute.getDepositTxSerialized() == null) {

View file

@ -200,7 +200,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
((BuyerProtocol) tradeManager.getTradeProtocol(trade)).onPaymentStarted(resultHandler, errorMessageHandler);
}
public void onFiatPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
Trade trade = getTrade();
checkNotNull(trade, "trade must not be null");
checkArgument(trade instanceof SellerTrade, "Trade must be instance of SellerTrade");
@ -466,7 +466,6 @@ public class PendingTradesDataModel extends ActivatableDataModel {
String payoutTxHashAsString = null;
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
String updatedMultisigHex = multisigWallet.exportMultisigHex();
xmrWalletService.closeMultisigWallet(trade.getId()); // close multisig wallet
if (trade.getPayoutTxId() != null) {
// payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr
// payoutTxHashAsString = payoutTx.getHashAsString();

View file

@ -30,8 +30,11 @@ import bisq.core.offer.Offer;
import bisq.core.offer.OfferUtil;
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.mempool.MempoolService;
import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.BuyerTrade;
import bisq.core.trade.ClosedTradableManager;
import bisq.core.trade.Contract;
import bisq.core.trade.SellerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeUtil;
import bisq.core.user.User;
@ -115,6 +118,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
@Getter
private final ObjectProperty<MessageState> messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
private Subscription tradeStateSubscription;
private Subscription payoutStateSubscription;
private Subscription messageStateSubscription;
@Getter
protected final IntegerProperty mempoolStatus = new SimpleIntegerProperty();
@ -160,6 +164,11 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
tradeStateSubscription = null;
}
if (payoutStateSubscription != null) {
payoutStateSubscription.unsubscribe();
payoutStateSubscription = null;
}
if (messageStateSubscription != null) {
messageStateSubscription.unsubscribe();
messageStateSubscription = null;
@ -174,6 +183,12 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
buyerState.set(BuyerState.UNDEFINED);
}
if (payoutStateSubscription != null) {
payoutStateSubscription.unsubscribe();
sellerState.set(SellerState.UNDEFINED);
buyerState.set(BuyerState.UNDEFINED);
}
if (messageStateSubscription != null) {
messageStateSubscription.unsubscribe();
messageStateProperty.set(MessageState.UNDEFINED);
@ -184,6 +199,9 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), state -> {
UserThread.execute(() -> onTradeStateChanged(state));
});
payoutStateSubscription = EasyBind.subscribe(trade.payoutStateProperty(), state -> {
UserThread.execute(() -> onPayoutStateChanged(state));
});
messageStateSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentStartedMessageStateProperty(), this::onMessageStateChanged);
}
}
@ -399,6 +417,13 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
tradeState,
trade != null ? trade.getShortId() : "trade is null");
// arbitrator trade view only shows tx status
if (trade instanceof ArbitratorTrade) {
buyerState.set(BuyerState.STEP1);
sellerState.set(SellerState.STEP1);
return;
}
switch (tradeState) {
// preparation
case PREPARATION:
@ -414,9 +439,8 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
// deposit requested
case SENT_PUBLISH_DEPOSIT_TX_REQUEST:
case SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST:
case STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST:
case SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST:
case SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST:
// deposit published
case ARBITRATOR_PUBLISHED_DEPOSIT_TXS:
@ -456,29 +480,16 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
// seller step 4
case SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT: // UI action
case SELLER_SENT_PAYMENT_RECEIVED_MSG:
case SELLER_PUBLISHED_PAYOUT_TX: // payout tx broadcasted
case SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG: // PAYOUT_TX_PUBLISHED_MSG sent
sellerState.set(SellerState.STEP3);
if (trade instanceof BuyerTrade) buyerState.set(BuyerState.STEP4);
else if (trade instanceof SellerTrade) sellerState.set(SellerState.STEP3);
break;
case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG:
case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG:
case SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG:
case SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG: // PAYOUT_TX_PUBLISHED_MSG arrived
case SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG: // PAYOUT_TX_PUBLISHED_MSG mailbox
case SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG: // PAYOUT_TX_PUBLISHED_MSG failed - payout tx is published, peer will see it in network so we ignore failure and complete
sellerState.set(SellerState.STEP4);
break;
// buyer step 4
case BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG:
// Alternatively the maker could have seen the payout tx earlier before he received the PAYOUT_TX_PUBLISHED_MSG:
case PAYOUT_TX_SEEN_IN_NETWORK:
// Alternatively the buyer could fully sign and publish the payout tx
case BUYER_PUBLISHED_PAYOUT_TX:
buyerState.set(BuyerState.STEP4);
break;
case WITHDRAW_COMPLETED:
case TRADE_COMPLETED:
sellerState.set(UNDEFINED);
buyerState.set(BuyerState.UNDEFINED);
break;
@ -491,4 +502,21 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
break;
}
}
private void onPayoutStateChanged(Trade.PayoutState payoutState) {
log.info("UI payoutState={}, id={}",
payoutState,
trade != null ? trade.getShortId() : "trade is null");
if (trade instanceof ArbitratorTrade) return;
switch (payoutState) {
case PUBLISHED:
sellerState.set(SellerState.STEP4);
buyerState.set(BuyerState.STEP4);
break;
default:
break;
}
}
}

View file

@ -123,8 +123,6 @@ public class SellerStep3View extends TradeStepView {
busyAnimation.play();
statusLabel.setText(Res.get("Confirming payment received. This can take up to a few minutes. Please wait..."));
break;
case SELLER_PUBLISHED_PAYOUT_TX:
case SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG:
case SELLER_SENT_PAYMENT_RECEIVED_MSG:
busyAnimation.play();
statusLabel.setText(Res.get("shared.sendingConfirmation"));
@ -135,16 +133,14 @@ public class SellerStep3View extends TradeStepView {
}, 10);
break;
case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG:
case SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG:
busyAnimation.stop();
statusLabel.setText(Res.get("shared.messageArrived"));
break;
case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG:
case SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG:
busyAnimation.stop();
statusLabel.setText(Res.get("shared.messageStoredInMailbox"));
break;
case SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG:
case SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG:
// We get a popup and the trade closed, so we dont need to show anything here
busyAnimation.stop();
statusLabel.setText("");
@ -464,7 +460,7 @@ public class SellerStep3View extends TradeStepView {
busyAnimation.play();
statusLabel.setText(Res.get("shared.sendingConfirmation"));
model.dataModel.onFiatPaymentReceived(() -> {
model.dataModel.onPaymentReceived(() -> {
}, errorMessage -> {
busyAnimation.stop();
new Popup().warning(Res.get("popup.warning.sendMsgFailed")).show();