recover if offer funding, deposit, or payout txs are invalidated (#1962)

This commit is contained in:
woodser 2025-09-18 05:32:37 -04:00 committed by GitHub
parent 2bc877feba
commit f711bd5084
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 280 additions and 250 deletions

View file

@ -598,8 +598,8 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// TODO: DisputeOpenedMessage should include arbitrator's updated multisig hex too
// TODO: arbitrator needs to import multisig info then scan for updated state?
// arbitrator syncs and polls wallet unless finalized
if (trade.isArbitrator() && !trade.isPayoutFinalized()) {
// sync and poll wallet unless finalized
if (!trade.isPayoutFinalized()) {
trade.syncAndPollWallet();
trade.recoverIfMissingWalletData();
}
@ -1007,7 +1007,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// update trade state
if (updateState) {
trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
trade.updatePayout(payoutTx);
trade.setPayoutTx(payoutTx);
if (trade.getBuyer().getUpdatedMultisigHex() != null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
if (trade.getSeller().getUpdatedMultisigHex() != null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
}

View file

@ -520,7 +520,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
}
// update state
trade.updatePayout(disputeTxSet.getTxs().get(0));
trade.setPayoutTx(disputeTxSet.getTxs().get(0));
trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash());
requestPersistence(trade);

View file

@ -38,7 +38,6 @@ import com.google.common.base.Preconditions;
import com.google.protobuf.ByteString;
import com.google.protobuf.Message;
import haveno.common.ThreadUtils;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.config.Config;
import haveno.common.crypto.Encryption;
@ -156,6 +155,9 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
private static final int NUM_CONFIRMATIONS_FOR_SCHEDULED_IMPORT = 5;
public static final int NUM_BLOCKS_DEPOSITS_FINALIZED = 30; // ~1 hour before deposits are considered finalized
public static final int NUM_BLOCKS_PAYOUT_FINALIZED = Config.baseCurrencyNetwork().isTestnet() ? 60 : 720; // ~1 day before payout is considered finalized and multisig wallet deleted
public static final long DEFER_PUBLISH_MS = 25000; // 25 seconds
private static final long IDLE_SYNC_PERIOD_MS = Config.baseCurrencyNetwork().isTestnet() ? 30000 : 1680000; // 28 minutes (monero's default connection timeout is 30 minutes on a local connection, so beyond this the wallets will disconnect)
private static final long MAX_REPROCESS_DELAY_SECONDS = 7200; // max delay to reprocess messages (once per 2 hours)
protected final Object pollLock = new Object();
private final Object removeTradeOnErrorLock = new Object();
protected static final Object importMultisigLock = new Object();
@ -166,14 +168,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
public static final String PROTOCOL_VERSION = "protocolVersion"; // key for extraDataMap in trade statistics
public BooleanProperty wasWalletPolled = new SimpleBooleanProperty(false);
// missing or failed payout tx handling
private static final int HANDLE_MISSING_PAYOUT_AFTER_MINS = 6; // minimum delay before handling missing payout tx in minutes
private static final long MIN_MISSING_TX_POLL_INTERVAL_MS = 30000; // throttle missing payout tx processing to once per 30s
private Object missingPayoutTxLock = new Object();
private Timer missingPayoutTxTimer;
private boolean handleMissingPayoutTxOnNextPoll = false;
private long lastMissingTxPollTime = 0;
///////////////////////////////////////////////////////////////////////////////////////////
// Enums
///////////////////////////////////////////////////////////////////////////////////////////
@ -457,10 +451,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
transient private Long pollPeriodMs;
transient private Long pollNormalStartTimeMs;
public static final long DEFER_PUBLISH_MS = 25000; // 25 seconds
private static final long IDLE_SYNC_PERIOD_MS = 1680000; // 28 minutes (monero's default connection timeout is 30 minutes on a local connection, so beyond this the wallets will disconnect)
private static final long MAX_REPROCESS_DELAY_SECONDS = 7200; // max delay to reprocess messages (once per 2 hours)
// Mutable
@Getter
transient private boolean isInitialized;
@ -713,7 +703,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
payoutStateSubscription = EasyBind.subscribe(payoutStateProperty, newValue -> {
if (!isInitialized || isShutDownStarted) return;
ThreadUtils.submitToPool(() -> {
if (isPayoutPublished()) updatePollPeriod();
updatePollPeriod();
// handle when payout published
if (newValue == Trade.PayoutState.PAYOUT_PUBLISHED) {
@ -769,7 +759,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
xmrWalletService.addWalletListener(idlePayoutSyncer);
}
// TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed mismatch in v1.0.19, but can this check can be removed?
// TODO: buyer's payment sent message state property became unsynced if shut down while awaiting ack from seller. fixed mismatch in v1.0.19, but can this check be removed?
if (isBuyer()) {
MessageState expectedState = getPaymentSentMessageState();
if (expectedState != null && expectedState != getSeller().getPaymentSentMessageStateProperty().get()) {
@ -960,7 +950,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
public boolean isIdling() {
if (isPayoutUnlocked() && !Config.baseCurrencyNetwork().isTestnet()) return true; // idle after payout unlocked (unless testnet)
if (isPayoutUnlocked()) return true; // idle after payout unlocked
return this instanceof ArbitratorTrade && isDepositsConfirmed() && walletExists() && pollNormalStartTimeMs == null; // arbitrator idles trade after deposits confirm unless overriden
}
@ -1474,7 +1464,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack
MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0);
if (payoutTxId == null) updatePayout(payoutTx); // update payout tx if id currently unknown
if (payoutTxId == null) setPayoutTx(payoutTx); // update payout tx if id currently unknown
// verify payout tx has exactly 2 destinations
if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new IllegalArgumentException("Payout tx does not have exactly two destinations");
@ -1506,7 +1496,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout);
// update payout tx
updatePayout(payoutTx);
setPayoutTx(payoutTx);
// check connection
boolean doSign = sign && getPayoutTxHex() == null;
@ -1543,7 +1533,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
// describe result
describedTxSet = wallet.describeMultisigTxSet(getPayoutTxHex());
payoutTx = describedTxSet.getTxs().get(0);
updatePayout(payoutTx);
setPayoutTx(payoutTx);
}
// save trade state
@ -1577,9 +1567,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
// poll the main wallet
log.warn("Processing payout tx for {} {} by polling main wallet", getClass().getSimpleName(), getShortId());
long startTime = System.currentTimeMillis();
xmrWalletService.doPollWallet(true);
log.info("Done polling main wallet to verify payout tx for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime);
// fetch payout tx from main wallet
MoneroTxWallet payoutTx = xmrWalletService.getWallet().getTx(payoutTxId);
@ -1593,7 +1581,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
if (!payoutTx.getIncomingAmount().equals(expectedAmount)) throw new IllegalStateException("Payout tx incoming amount is not deposit amount + trade amount - 1/2 tx costs, " + payoutTx.getIncomingAmount() + " vs " + getBuyer().getSecurityDeposit().add(getAmount()).subtract(txCostSplit));
// update payout tx
updatePayout(payoutTx);
setPayoutTx(payoutTx);
}
/**
@ -2043,7 +2031,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
if (payoutState.ordinal() < this.payoutState.ordinal()) {
String message = "We got a payout state change to a previous phase (id=" + getShortId() + ").\n" +
"Old payout state is: " + this.state + ". New payout state is: " + payoutState;
"Old payout state is: " + this.payoutState + ". New payout state is: " + payoutState;
log.warn(message);
}
@ -2099,45 +2087,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
getVolumeProperty().set(getVolume());
}
public void updatePayout(MoneroTx payoutTx) {
// set payout tx fields
this.payoutTx = payoutTx;
this.payoutTxId = payoutTx.getHash();
this.payoutTxFee = payoutTx.getFee() == null ? 0 : payoutTx.getFee().longValueExact();
this.payoutTxKey = payoutTx.getKey();
if ("".equals(payoutTxId)) this.payoutTxId = null; // tx id is empty until signed
// set payout tx id in dispute(s)
for (Dispute dispute : getDisputes()) dispute.setDisputePayoutTxId(payoutTxId);
// set final payout amounts
if (isPaymentReceived()) {
BigInteger splitTxFee = payoutTx.getFee().divide(BigInteger.valueOf(2));
getBuyer().setPayoutTxFee(splitTxFee);
getSeller().setPayoutTxFee(splitTxFee);
getBuyer().setPayoutAmount(getBuyer().getSecurityDeposit().subtract(getBuyer().getPayoutTxFee()).add(getAmount()));
getSeller().setPayoutAmount(getSeller().getSecurityDeposit().subtract(getSeller().getPayoutTxFee()));
} else {
DisputeResult disputeResult = getDisputeResult();
if (disputeResult != null) {
BigInteger[] buyerSellerPayoutTxFees = ArbitrationManager.getBuyerSellerPayoutTxCost(disputeResult, payoutTx.getFee());
getBuyer().setPayoutTxFee(buyerSellerPayoutTxFees[0]);
getSeller().setPayoutTxFee(buyerSellerPayoutTxFees[1]);
getBuyer().setPayoutAmount(disputeResult.getBuyerPayoutAmountBeforeCost().subtract(getBuyer().getPayoutTxFee()));
getSeller().setPayoutAmount(disputeResult.getSellerPayoutAmountBeforeCost().subtract(getSeller().getPayoutTxFee()));
}
}
// set payout tx state
if (Boolean.TRUE.equals(payoutTx.isRelayed())) setPayoutStatePublished();
if (payoutTx.isConfirmed()) setPayoutStateConfirmed();
if (payoutTx.getNumConfirmations() != null) {
if (payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) setPayoutStateUnlocked();
if (payoutTx.getNumConfirmations() >= NUM_BLOCKS_PAYOUT_FINALIZED) setPayoutStateFinalized();
}
}
public DisputeResult getDisputeResult() {
if (getDisputes().isEmpty()) return null;
return getDisputes().get(getDisputes().size() - 1).getDisputeResultProperty().get();
@ -2707,7 +2656,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
///////////////////////////////////////////////////////////////////////////////////////////
private boolean tradeAmountTransferred() {
return isPaymentReceived() || (getDisputeResult() != null && getDisputeResult().getWinner() == DisputeResult.Winner.SELLER);
return isPayoutPublished() && (isPaymentReceived() || (getDisputeResult() != null && getDisputeResult().getWinner() == DisputeResult.Winner.SELLER));
}
private void doPublishTradeStatistics() {
@ -2769,17 +2718,16 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
if (isShutDownStarted) return;
// set known deposit txs
List<MoneroTxWallet> depositTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true).setInTxPool(false));
setDepositTxs(depositTxs);
doPollWallet(true);
// start polling
if (!isIdling()) {
doTryInitSyncing();
} else {
long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getPollPeriod()); // random time to start polling
if (isIdling()) {
long startSyncingInSec = Math.max(1, ThreadLocalRandom.current().nextLong(0, getPollPeriod()) / 1000l); // random seconds to start polling
UserThread.runAfter(() -> ThreadUtils.execute(() -> {
if (!isShutDownStarted) doTryInitSyncing();
}, getId()), startSyncingInMs / 1000l);
}, getId()), startSyncingInSec);
} else {
doTryInitSyncing();
}
}
@ -2930,50 +2878,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
// sync wallet if behind
if (!offlinePoll) syncWalletIfBehind();
// get txs from trade wallet
boolean updatePool = !offlinePoll && !isDepositsConfirmed() && (getMaker().getDepositTx() == null || (getTaker().getDepositTx() == null && !hasBuyerAsTakerWithoutDeposit()));
List<MoneroTxWallet> txs = getTxs(updatePool);
setDepositTxs(txs);
// set actual buyer security deposit
if (isSeen(getBuyer().getDepositTx())) {
BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount();
if (!getBuyer().getSecurityDeposit().equals(BigInteger.ZERO) && !buyerSecurityDeposit.equals(getBuyer().getSecurityDeposit())) {
log.warn("Overwriting buyer security deposit for {} {}, old={}, new={}", getClass().getSimpleName(), getShortId(), getBuyer().getSecurityDeposit(), buyerSecurityDeposit);
}
getBuyer().setSecurityDeposit(buyerSecurityDeposit);
}
// set actual seller security deposit
if (isSeen(getSeller().getDepositTx())) {
BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount());
if (!getSeller().getSecurityDeposit().equals(BigInteger.ZERO) && !sellerSecurityDeposit.equals(getSeller().getSecurityDeposit())) {
log.warn("Overwriting seller security deposit for {} {}, old={}, new={}", getClass().getSimpleName(), getShortId(), getSeller().getSecurityDeposit(), sellerSecurityDeposit);
}
getSeller().setSecurityDeposit(sellerSecurityDeposit);
}
// handle both deposits seen
if (isSeen(getMaker().getDepositTx()) && (hasBuyerAsTakerWithoutDeposit() || isSeen(getTaker().getDepositTx()))) {
setStateDepositsSeen();
// check for deposit txs confirmed
if (getMaker().getDepositTx().isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().isConfirmed())) {
setStateDepositsConfirmed();
}
// check for deposit txs unlocked
if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) {
setStateDepositsUnlocked();
}
// check for deposit txs finalized
if (getMaker().getDepositTx().getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED)) {
setStateDepositsFinalized();
}
} else if (isDepositsSeen()) {
log.warn("Resetting state to {} for {} {} because one or both deposit txs no longer seen as valid", Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS, getClass().getSimpleName(), getShortId());
setState(Trade.State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS);
// set deposit txs from trade wallet
List<MoneroTxWallet> txs = getTxs(false);
if (getValidMakerTx(txs) != null && (getValidTakerTx(txs) != null || hasBuyerAsTakerWithoutDeposit())) {
setDepositTxs(txs);
} else if (!offlinePoll) {
txs = getTxs(true); // check pool if deposits not found
setDepositTxs(txs);
}
}
@ -2988,7 +2899,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
if (!offlinePoll && (isPayoutExpected || isPayoutPublished())) syncWalletIfBehind();
// rescan spent outputs to detect unconfirmed payout tx
if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) {
if (getPayoutState() == PayoutState.PAYOUT_PUBLISHED || (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0)) {
try {
rescanSpent(true);
} catch (Exception e) {
@ -2997,30 +2908,27 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
// get txs from trade wallet
boolean updatePool = !offlinePoll && isPayoutExpected && !isPayoutConfirmed();
List<MoneroTxWallet> txs = getTxs(updatePool);
boolean checkPool = !offlinePoll && isPayoutExpected && !isPayoutConfirmed();
List<MoneroTxWallet> txs = getTxs(checkPool);
setDepositTxs(txs);
// update payout state
boolean hasValidPayout = false;
boolean hasPayoutTx = false;
MoneroTxWallet payoutTx = null;
for (MoneroTxWallet tx : txs) {
boolean isOutgoing = !Boolean.TRUE.equals(tx.isIncoming()); // outgoing tx observed after wallet submits payout or on first confirmation
if (isOutgoing && !tx.isFailed()) {
hasValidPayout = true;
updatePayout(tx);
if (!Boolean.TRUE.equals(tx.isIncoming()) && !tx.isFailed()) {
payoutTx = tx;
hasPayoutTx = true;
break;
} else {
for (MoneroOutputWallet output : tx.getOutputsWallet()) {
if (Boolean.TRUE.equals(output.isSpent())) hasValidPayout = true; // spent outputs observed on payout published (after rescanning)
if (Boolean.TRUE.equals(output.isSpent())) hasPayoutTx = true; // spent outputs observed on payout published (after rescanning)
}
}
}
// handle payout validity
if (hasValidPayout) {
onValidPayoutTxPoll();
} else if (isPayoutPublished() && !offlinePoll) {
onMissingPayoutTxPoll();
}
if (payoutTx != null) setPayoutTx(payoutTx);
else if (hasPayoutTx) setPayoutStatePublished();
else if (checkPool && isPayoutPublished()) onPayoutUnseen(); // payout tx seen then lost (e.g. reorg)
}
} catch (Exception e) {
if (!(e instanceof IllegalStateException) && !isShutDownStarted && !wasWalletPolled.get()) { // request connection switch if failure on first poll
@ -3049,11 +2957,11 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
}
private List<MoneroTxWallet> getTxs(boolean updatePool) {
private List<MoneroTxWallet> getTxs(boolean checkPool) {
MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true);
if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible
if (!checkPool) query.setInTxPool(false); // avoid checking pool if possible
List<MoneroTxWallet> txs = null;
if (!updatePool) txs = wallet.getTxs(query);
if (!checkPool) txs = wallet.getTxs(query);
else {
synchronized (walletLock) {
synchronized (HavenoUtils.getDaemonLock()) {
@ -3064,72 +2972,37 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
return txs;
}
private void onValidPayoutTxPoll() {
setPayoutStatePublished();
synchronized (missingPayoutTxLock) {
handleMissingPayoutTxOnNextPoll = false;
if (missingPayoutTxTimer == null) return;
log.warn("Valid payout tx seen after failed or missing for {} {} with payout state {}", getClass().getSimpleName(), getShortId(), getPayoutState());
missingPayoutTxTimer.stop();
missingPayoutTxTimer = null;
private void onPayoutUnseen() {
log.warn("Payout tx unseen for {} {} with payout state {}. Possible reorg?", getClass().getSimpleName(), getShortId(), getPayoutState());
for (TradePeer peer : getAllPeers()) {
peer.setPaymentReceivedMessage(null);
peer.setPaymentReceivedMessageState(MessageState.UNDEFINED);
peer.setDisputeClosedMessage(null);
}
}
private void onMissingPayoutTxPoll() {
// throttle processing
if (System.currentTimeMillis() - lastMissingTxPollTime < MIN_MISSING_TX_POLL_INTERVAL_MS) return;
lastMissingTxPollTime = System.currentTimeMillis();
// process missing payout tx by id
if (getPayoutTxId() != null) {
log.warn("The payout tx is failed or missing for {} {}, payout state={}, payout id={}. Possibly due to reorg?", getClass().getSimpleName(), getShortId(), getPayoutState(), getPayoutTxId());
// buyer can process payout tx received to their main wallet
try {
if (isBuyer()) processBuyerPayout(getPayoutTxId());
} catch (Exception e) {
log.warn("Error checking for payout tx for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage());
}
}
synchronized (missingPayoutTxLock) {
// handle missing payout tx if previously set
if (handleMissingPayoutTxOnNextPoll) {
handleMissingPayoutTxOnNextPoll = false;
ThreadUtils.execute(() -> handleMissingPayoutTx(), getId());
} else {
if (missingPayoutTxTimer != null) return;
log.info("Scheduling handling if payout becomes failed or missing for {} {}", getClass().getSimpleName(), getShortId());
missingPayoutTxTimer = UserThread.runAfter(() -> {
ThreadUtils.execute(() -> {
synchronized (missingPayoutTxLock) {
if (missingPayoutTxTimer == null) return;
missingPayoutTxTimer.stop();
missingPayoutTxTimer = null;
handleMissingPayoutTxOnNextPoll = true; // handle missing payout tx on next poll in case we're outdated
}
}, getId());
}, HANDLE_MISSING_PAYOUT_AFTER_MINS, TimeUnit.MINUTES);
}
}
}
private void handleMissingPayoutTx() {
log.warn("Handling failed or missing payout tx for {} {} with payout state {}. Possibly due to reorg?", getClass().getSimpleName(), getShortId(), getPayoutState());
setPayoutState(PayoutState.PAYOUT_UNPUBLISHED);
onPayoutError(false, null);
if (isCompleted()) processModel.getTradeManager().onMoveClosedTradeToPendingTrades(this);
String errorMsg = "The payout transaction is not seen for trade " + getShortId() + ". This can happen after a blockchain reorganization..\n\nIf the payout does not confirm automatically, you can contact support or mark the trade as failed.";
if (isSeller() && getState().ordinal() >= State.BUYER_RECEIVED_PAYMENT_RECEIVED_MSG.ordinal()) {
log.warn("Resetting state of {} {} from {} to {} because payout is unpublished", getClass().getSimpleName(), getId(), getState(), Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
setState(State.SELLER_SENT_PAYMENT_RECEIVED_MSG);
onPayoutError(false, true, null);
setErrorMessage(errorMsg);
} else if (getState().ordinal() >= State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal()) {
log.warn("Resetting state of {} {} from {} to {} because payout is unpublished", getClass().getSimpleName(), getId(), getState(), Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT);
setState(State.SELLER_CONFIRMED_PAYMENT_RECEIPT);
setErrorMessage(errorMsg);
}
}
/**
* Handle a payout error due to NACK or the transaction failing (e.g. due to reorg).
*
* @param syncAndPoll whether to sync and poll
* @param autoMarkPaymentReceived whether to automatically mark payment received if previously confirmed
* @return true if the payment received was auto marked, false otherwise
* @param resendPaymentReceivedMessages whether to resend payment received messages if previously confirmed
* @param paymentReceivedNackSender the peer that sent the payment received NACK, or null if not applicable
* @return true if the payment received were resent, false otherwise
*/
public boolean onPayoutError(boolean syncAndPoll, TradePeer paymentReceivedNackSender) {
public boolean onPayoutError(boolean syncAndPoll, boolean resendPaymentReceivedMessages, TradePeer paymentReceivedNackSender) {
log.warn("Handling payout error for {} {}", getClass().getSimpleName(), getId());
if (syncAndPoll) {
try {
@ -3149,12 +3022,15 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
if (!isPayoutPublished()) {
getSelf().setUnsignedPayoutTxHex(null);
setPayoutTxHex(null);
setPayoutTxId(null);
}
persistNow(null);
// send updated payment received message when payout is confirmed
if (paymentReceivedNackSender != null && isSeller()) {
if (resendPaymentReceivedMessages) {
if (!isSeller()) throw new IllegalArgumentException("Only the seller can resend PaymentReceivedMessages after a payout error for " + getClass().getSimpleName() + " " + getId());
if (!isPaymentReceived()) throw new IllegalStateException("Cannot resend PaymentReceivedMessages after a payout error for " + getClass().getSimpleName() + " " + getId() + " because payment not marked received");
log.warn("Sending updated PaymentReceivedMessages for {} {} after payout error", getClass().getSimpleName(), getId());
((SellerProtocol) getProtocol()).onPaymentReceived(() -> {
log.info("Done sending updated PaymentReceivedMessages on payout error for {} {}", getClass().getSimpleName(), getId());
@ -3166,13 +3042,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
return false;
}
private static boolean isSeen(MoneroTx tx) {
if (tx == null) return false;
if (Boolean.TRUE.equals(tx.isFailed())) return false;
if (!Boolean.TRUE.equals(tx.inTxPool()) && !Boolean.TRUE.equals(tx.isConfirmed())) return false;
return true;
}
private static boolean isUnlocked(MoneroTx tx) {
if (tx == null) return false;
if (tx.getNumConfirmations() == null || tx.getNumConfirmations() < XmrWalletService.NUM_BLOCKS_UNLOCK) return false;
@ -3197,17 +3066,154 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
private void setDepositTxs(List<MoneroTxWallet> txs) {
MoneroTxWallet makerDepositTx = null;
MoneroTxWallet takerDepositTx = null;
for (MoneroTxWallet tx : txs) {
if (tx.getHash().equals(getMaker().getDepositTxHash())) makerDepositTx = tx;
if (tx.getHash().equals(getTaker().getDepositTxHash())) takerDepositTx = tx;
// set deposit txs
getMaker().setDepositTx(getValidMakerTx(txs));
getTaker().setDepositTx(getValidTakerTx(txs));
// set actual buyer security deposit
if (isSeen(getBuyer().getDepositTx())) {
BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount();
if (!getBuyer().getSecurityDeposit().equals(BigInteger.ZERO) && !buyerSecurityDeposit.equals(getBuyer().getSecurityDeposit())) {
log.warn("Overwriting buyer security deposit for {} {}, old={}, new={}", getClass().getSimpleName(), getShortId(), getBuyer().getSecurityDeposit(), buyerSecurityDeposit);
}
getBuyer().setSecurityDeposit(buyerSecurityDeposit);
}
getMaker().setDepositTx(makerDepositTx);
getTaker().setDepositTx(takerDepositTx);
// set actual seller security deposit
if (isSeen(getSeller().getDepositTx())) {
BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(getAmount());
if (!getSeller().getSecurityDeposit().equals(BigInteger.ZERO) && !sellerSecurityDeposit.equals(getSeller().getSecurityDeposit())) {
log.warn("Overwriting seller security deposit for {} {}, old={}, new={}", getClass().getSimpleName(), getShortId(), getSeller().getSecurityDeposit(), sellerSecurityDeposit);
}
getSeller().setSecurityDeposit(sellerSecurityDeposit);
}
// advance deposit state
if (isSeen(getMaker().getDepositTx()) && (hasBuyerAsTakerWithoutDeposit() || isSeen(getTaker().getDepositTx()))) {
setStateDepositsSeen();
// check for deposit txs confirmed
if (getMaker().getDepositTx().isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().isConfirmed())) {
setStateDepositsConfirmed();
}
// check for deposit txs unlocked
if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) {
setStateDepositsUnlocked();
}
// check for deposit txs finalized
if (getMaker().getDepositTx().getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED)) {
setStateDepositsFinalized();
}
}
// revert deposit state if necessary
State depositsState = getDepositsState();
if (!isPaymentSent() && depositsState.ordinal() < getState().ordinal()) {
log.warn("Reverting deposits state to {} for {} {}. Possible reorg?", depositsState, getClass().getSimpleName(), getShortId());
if (depositsState == State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS) setErrorMessage("Deposit transactions are missing for trade " + getShortId() + ". This can happen after a blockchain reorganization..\n\nIf the issue continues, you can contact support or mark the trade as failed.");
setState(depositsState);
}
// announce deposits update
depositTxsUpdateCounter.set(depositTxsUpdateCounter.get() + 1);
}
private MoneroTxWallet getValidMakerTx(List<MoneroTxWallet> txs) {
for (MoneroTxWallet tx : txs) {
if (tx.getHash().equals(getMaker().getDepositTxHash()) && !Boolean.TRUE.equals(tx.isFailed())) {
return tx;
}
}
return null;
}
private MoneroTxWallet getValidTakerTx(List<MoneroTxWallet> txs) {
for (MoneroTxWallet tx : txs) {
if (tx.getHash().equals(getTaker().getDepositTxHash()) && !Boolean.TRUE.equals(tx.isFailed())) {
return tx;
}
}
return null;
}
private static boolean isSeen(MoneroTx tx) {
if (tx == null) return false;
if (Boolean.TRUE.equals(tx.isFailed())) return false;
if (!Boolean.TRUE.equals(tx.inTxPool()) && !Boolean.TRUE.equals(tx.isConfirmed())) return false;
return true;
}
private State getDepositsState() {
if (getMaker().getDepositTx() == null || (!hasBuyerAsTakerWithoutDeposit() && getTaker().getDepositTx() == null)) return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS;
if (getMaker().getDepositTx().isFailed() || (!hasBuyerAsTakerWithoutDeposit() && getTaker().getDepositTx().isFailed())) return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS;
if (getMaker().getDepositTx().getNumConfirmations() == null || (!hasBuyerAsTakerWithoutDeposit() && getTaker().getDepositTx().getNumConfirmations() == null)) return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS;
if (getMaker().getDepositTx().getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= NUM_BLOCKS_DEPOSITS_FINALIZED)) return State.DEPOSIT_TXS_FINALIZED_IN_BLOCKCHAIN;
if (getMaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK)) return State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN;
if (getMaker().getDepositTx().isConfirmed() && (hasBuyerAsTakerWithoutDeposit() || getTaker().getDepositTx().isConfirmed())) return State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN;
if (isSeen(getMaker().getDepositTx()) && (hasBuyerAsTakerWithoutDeposit() || isSeen(getTaker().getDepositTx()))) return State.DEPOSIT_TXS_SEEN_IN_NETWORK;
return State.ARBITRATOR_PUBLISHED_DEPOSIT_TXS;
}
public void setPayoutTx(MoneroTx payoutTx) {
// set payout tx fields
this.payoutTx = payoutTx;
this.payoutTxId = payoutTx.getHash();
this.payoutTxFee = payoutTx.getFee() == null ? 0 : payoutTx.getFee().longValueExact();
this.payoutTxKey = payoutTx.getKey();
if ("".equals(payoutTxId)) this.payoutTxId = null; // tx id is empty until signed
// set payout tx id in dispute(s)
for (Dispute dispute : getDisputes()) dispute.setDisputePayoutTxId(payoutTxId);
// set final payout amounts
if (isPaymentReceived()) {
BigInteger splitTxFee = payoutTx.getFee().divide(BigInteger.valueOf(2));
getBuyer().setPayoutTxFee(splitTxFee);
getSeller().setPayoutTxFee(splitTxFee);
getBuyer().setPayoutAmount(getBuyer().getSecurityDeposit().subtract(getBuyer().getPayoutTxFee()).add(getAmount()));
getSeller().setPayoutAmount(getSeller().getSecurityDeposit().subtract(getSeller().getPayoutTxFee()));
} else {
DisputeResult disputeResult = getDisputeResult();
if (disputeResult != null) {
BigInteger[] buyerSellerPayoutTxFees = ArbitrationManager.getBuyerSellerPayoutTxCost(disputeResult, payoutTx.getFee());
getBuyer().setPayoutTxFee(buyerSellerPayoutTxFees[0]);
getSeller().setPayoutTxFee(buyerSellerPayoutTxFees[1]);
getBuyer().setPayoutAmount(disputeResult.getBuyerPayoutAmountBeforeCost().subtract(getBuyer().getPayoutTxFee()));
getSeller().setPayoutAmount(disputeResult.getSellerPayoutAmountBeforeCost().subtract(getSeller().getPayoutTxFee()));
}
}
// advance payout state
if (Boolean.TRUE.equals(payoutTx.isRelayed()) || Boolean.TRUE.equals(payoutTx.inTxPool())) setPayoutStatePublished();
if (payoutTx.isConfirmed()) setPayoutStateConfirmed();
if (payoutTx.getNumConfirmations() != null) {
if (payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) setPayoutStateUnlocked();
if (payoutTx.getNumConfirmations() >= NUM_BLOCKS_PAYOUT_FINALIZED) setPayoutStateFinalized();
}
// revert payout state if necessary
PayoutState payoutState = getPayoutState(payoutTx);
if (payoutState.ordinal() < getPayoutState().ordinal()) {
log.warn("Reverting payout state to {} for {} {}. Possible reorg?", payoutState, getClass().getSimpleName(), getShortId());
setPayoutState(payoutState);
}
}
private static PayoutState getPayoutState(MoneroTx payoutTx) {
if (payoutTx.getHash() == null) return PayoutState.PAYOUT_UNPUBLISHED;
if (Boolean.TRUE.equals(payoutTx.isFailed())) return PayoutState.PAYOUT_UNPUBLISHED;
if (payoutTx.getNumConfirmations() != null) {
if (payoutTx.getNumConfirmations() >= NUM_BLOCKS_PAYOUT_FINALIZED) return PayoutState.PAYOUT_FINALIZED;
if (payoutTx.getNumConfirmations() >= XmrWalletService.NUM_BLOCKS_UNLOCK) return PayoutState.PAYOUT_UNLOCKED;
}
if (payoutTx.isConfirmed()) return PayoutState.PAYOUT_CONFIRMED;
return PayoutState.PAYOUT_PUBLISHED; // payout is published by default in the wallet
}
// TODO: wallet is sometimes missing balance or deposits, due to reorgs, specific daemon connections, not saving?
public void recoverIfMissingWalletData() {
synchronized (walletLock) {
@ -3333,7 +3339,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
private void setStateDepositsSeen() {
if (!isDepositsPublished()) setState(State.DEPOSIT_TXS_SEEN_IN_NETWORK);
if (getState().ordinal() < State.DEPOSIT_TXS_SEEN_IN_NETWORK.ordinal()) setState(State.DEPOSIT_TXS_SEEN_IN_NETWORK);
}
private void setStateDepositsConfirmed() {

View file

@ -172,7 +172,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
private final PersistenceManager<TradableList<Trade>> persistenceManager;
private final TradableList<Trade> tradableList = new TradableList<>();
@Getter
private final BooleanProperty persistedTradesInitialized = new SimpleBooleanProperty();
private final BooleanProperty tradesInitialized = new SimpleBooleanProperty();
@Getter
private final LongProperty numPendingTrades = new SimpleLongProperty();
private final ReferralIdService referralIdService;
@ -454,18 +454,21 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
// remove skipped trades
trades.removeAll(tradesToSkip);
// sync idle trades once in background after active trades
// arbitrator syncs idle trades once in background after active trades
for (Trade trade : trades) {
if (trade.isIdling()) ThreadUtils.submitToPool(() -> {
if (!trade.isArbitrator()) continue;
if (trade.isIdling()) {
ThreadUtils.submitToPool(() -> {
// add random delay to avoid syncing at exactly the same time
if (trades.size() > 1 && trade.walletExists()) {
int delay = (int) (Math.random() * INIT_TRADE_RANDOM_DELAY_MS);
HavenoUtils.waitFor(delay);
}
trade.syncAndPollWallet();
});
// add random delay to avoid syncing at exactly the same time
if (trades.size() > 1 && trade.walletExists()) {
int delay = (int) (Math.random() * INIT_TRADE_RANDOM_DELAY_MS);
HavenoUtils.waitFor(delay);
}
trade.syncAndPollWallet();
});
}
}
// process after all wallets initialized
@ -494,7 +497,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
// notify that persisted trades initialized
if (isShutDownStarted) return;
persistedTradesInitialized.set(true);
tradesInitialized.set(true);
getObservableList().addListener((ListChangeListener<Trade>) change -> onTradesChanged());
onTradesChanged();
@ -1306,8 +1309,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
}
public BooleanProperty persistedTradesInitializedProperty() {
return persistedTradesInitialized;
public BooleanProperty tradesInitializedProperty() {
return tradesInitialized;
}
public boolean isMyOffer(Offer offer) {

View file

@ -711,7 +711,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// handle payout error
lastAckedPaymentReceivedMessage = message;
trade.onPayoutError(false, null);
trade.onPayoutError(false, false, null);
handleTaskRunnerFault(peer, message, null, errorMessage, trade.getSelf().getUpdatedMultisigHex()); // send nack
}
})))
@ -812,10 +812,12 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
peer.setNodeAddress(sender);
}
// TODO: arbitrator may nack maker's InitTradeRequest if reserve tx has become invalid (e.g. check_tx_key shows 0 funds received). recreate reserve tx in this case
// handle nack of InitTradeRequest from arbitrator to maker
if (!ackMessage.isSuccess() && trade.isMaker() && peer == trade.getArbitrator() && ackMessage.getSourceMsgClassName().equals(InitTradeRequest.class.getSimpleName())) {
if (ackMessage.getErrorMessage() != null && ackMessage.getErrorMessage().contains(SEND_INIT_TRADE_REQUEST_FAILED)) {
if (ignoreInitTradeRequestNackFromArbitrator(ackMessage)) {
log.warn("Ignoring InitTradeRequest NACK from arbitrator, offerId={}, errorMessage={}", processModel.getOfferId(), ackMessage.getErrorMessage());
// use default postprocessing
} else {
if (makerInitTradeRequestHasBeenNacked) {
handleSecondMakerInitTradeRequestNack(ackMessage);
// use default postprocessing
@ -892,7 +894,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
if (ackMessage.getUpdatedMultisigHex() != null) {
trade.getBuyer().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex());
processModel.getTradeManager().persistNow(null);
boolean autoResent = onPayoutError(true, peer);
boolean autoResent = onPaymentReceivedNack(true, peer);
if (autoResent) return; // skip remaining processing if auto resent
}
}
@ -911,7 +913,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
if (ackMessage.getUpdatedMultisigHex() != null) {
trade.getArbitrator().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex());
processModel.getTradeManager().persistNow(null);
boolean autoResent = onPayoutError(true, peer);
boolean autoResent = onPaymentReceivedNack(true, peer);
if (autoResent) return; // skip remaining processing if auto resent
}
}
@ -939,17 +941,23 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
trade.onAckMessage(ackMessage, sender);
}
private boolean onPayoutError(boolean syncAndPoll, TradePeer peer) {
private static boolean ignoreInitTradeRequestNackFromArbitrator(AckMessage ackMessage) {
return ackMessage.getErrorMessage() != null && ackMessage.getErrorMessage().contains(SEND_INIT_TRADE_REQUEST_FAILED); // ignore if arbitrator's request failed to taker
}
private boolean onPaymentReceivedNack(boolean syncAndPoll, TradePeer peer) {
// prevent infinite nack loop with max attempts
numPaymentReceivedNacks++;
if (numPaymentReceivedNacks > MAX_PAYMENT_RECEIVED_NACKS) {
log.warn("Maximum number of PaymentReceivedMessage NACKs reached for {} {}, not retrying", trade.getClass().getSimpleName(), trade.getId());
String errorMsg = "The maximum number of attempts to process the payment confirmation has been reached for " + trade.getClass().getSimpleName() + " " + trade.getId() + ". Restart the application to try again.";
log.warn(errorMsg);
trade.setErrorMessage(errorMsg);
return false;
}
// handle payout error
return trade.onPayoutError(syncAndPoll, peer);
return trade.onPayoutError(syncAndPoll, true, peer);
}
private void handleFirstMakerInitTradeRequestNack(AckMessage ackMessage) {

View file

@ -90,7 +90,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
// create payout tx
log.info("Buyer creating unsigned payout tx for {} {} ", trade.getClass().getSimpleName(), trade.getShortId());
MoneroTxWallet payoutTx = trade.createPayoutTx();
trade.updatePayout(payoutTx);
trade.setPayoutTx(payoutTx);
trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
trade.requestPersistence();
}

View file

@ -41,6 +41,7 @@ import haveno.core.trade.ArbitratorTrade;
import haveno.core.trade.BuyerTrade;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade;
import haveno.core.trade.Trade.State;
import haveno.core.trade.messages.PaymentReceivedMessage;
import haveno.core.trade.messages.PaymentSentMessage;
import haveno.core.util.Validator;
@ -82,6 +83,9 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
return;
}
// set state to confirmed payment receipt before processing
trade.advanceState(State.SELLER_CONFIRMED_PAYMENT_RECEIPT);
// cannot process until wallet sees deposits unlocked
if (!trade.isDepositsUnlocked()) {
trade.syncAndPollWallet();
@ -179,9 +183,6 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
else throw e;
}
}
} else {
log.info("Payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
if (message.getSignedPayoutTxHex() != null && !trade.isPayoutConfirmed()) trade.processPayoutTx(message.getSignedPayoutTxHex(), false, true);
}
}
}

View file

@ -112,9 +112,14 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
private void createUnsignedPayoutTx() {
log.info("Seller creating unsigned payout tx for trade {}", trade.getId());
trade.getProcessModel().setPaymentSentPayoutTxStale(true);
MoneroTxWallet payoutTx = trade.createPayoutTx();
trade.updatePayout(payoutTx);
trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
try {
trade.getProcessModel().setPaymentSentPayoutTxStale(true);
MoneroTxWallet payoutTx = trade.createPayoutTx();
trade.setPayoutTx(payoutTx);
trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
} catch (Exception e) {
if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId());
else throw e;
}
}
}

View file

@ -255,6 +255,8 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
if (isMessageReceived()) return true; // stop if message received
if (!trade.isPaymentReceived()) return true; // stop if trade state reset
if (trade.isPayoutPublished() && !((SellerTrade) trade).resendPaymentReceivedMessagesWithinDuration()) return true; // stop if payout is published and we are not in the resend period
// check if message state is outdated
if (unsignedPayoutTxHex != null && !StringUtils.equals(unsignedPayoutTxHex, trade.getSelf().getUnsignedPayoutTxHex())) return true;
if (signedPayoutTxHex != null && !StringUtils.equals(signedPayoutTxHex, trade.getPayoutTxHex())) return true;
if (updatedMultisigHex != null && !StringUtils.equals(updatedMultisigHex, trade.getSelf().getUpdatedMultisigHex())) return true;

View file

@ -217,7 +217,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener
@Override
public void onSetupComplete() {
// We handle the trade period here as we display a global popup if we reached dispute time
tradesAndUIReady = EasyBind.combine(isSplashScreenRemoved, tradeManager.persistedTradesInitializedProperty(),
tradesAndUIReady = EasyBind.combine(isSplashScreenRemoved, tradeManager.tradesInitializedProperty(),
(a, b) -> a && b);
tradesAndUIReady.subscribe((observable, oldValue, newValue) -> {
if (newValue) {

View file

@ -27,11 +27,8 @@ import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.network.MessageState;
import haveno.core.offer.Offer;
import haveno.core.offer.OfferUtil;
import haveno.core.trade.ArbitratorTrade;
import haveno.core.trade.BuyerTrade;
import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.SellerTrade;
import haveno.core.trade.Trade;
import haveno.core.trade.TradeUtil;
import haveno.core.user.User;
@ -363,7 +360,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
trade != null ? trade.getShortId() : "trade is null");
// arbitrator trade view only shows tx status
if (trade instanceof ArbitratorTrade) {
if (trade.isArbitrator()) {
buyerState.set(BuyerState.STEP1);
sellerState.set(SellerState.STEP1);
return;
@ -424,18 +421,26 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
buyerState.set(BuyerState.STEP2);
break;
// payment marked as received
case SELLER_CONFIRMED_PAYMENT_RECEIPT:
if (trade.isBuyer()) {
buyerState.set(BuyerState.STEP3);
} else if (trade.isSeller()) {
sellerState.set(trade.isPayoutPublished() ? SellerState.STEP4 : SellerState.STEP3);
}
break;
// payment received
case SELLER_SENT_PAYMENT_RECEIVED_MSG:
if (trade instanceof BuyerTrade) {
if (trade.isBuyer()) {
buyerState.set(BuyerState.UNDEFINED); // TODO: resetting screen to populate summary information which can be missing before payout message processed
buyerState.set(BuyerState.STEP4);
} else if (trade instanceof SellerTrade) {
} else if (trade.isSeller()) {
sellerState.set(trade.isPayoutPublished() ? SellerState.STEP4 : SellerState.STEP3);
}
break;
// seller step 3 or 4 if published
case SELLER_CONFIRMED_PAYMENT_RECEIPT:
case SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG:
case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG:
case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG:
@ -457,7 +462,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
payoutState,
trade != null ? trade.getShortId() : "trade is null");
if (trade instanceof ArbitratorTrade) return;
if (trade.isArbitrator()) return;
switch (payoutState) {
case PAYOUT_PUBLISHED:

View file

@ -225,7 +225,7 @@ public abstract class TradeStepView extends AnchorPane {
infoLabel.setText(getInfoText());
}
BooleanProperty initialized = model.dataModel.tradeManager.getPersistedTradesInitialized();
BooleanProperty initialized = model.dataModel.tradeManager.getTradesInitialized();
if (initialized.get()) {
onPendingTradesInitialized();
} else {