Skip to content

Commit f711bd5

Browse files
authored
recover if offer funding, deposit, or payout txs are invalidated (#1962)
1 parent 2bc877f commit f711bd5

File tree

12 files changed

+280
-250
lines changed

12 files changed

+280
-250
lines changed

core/src/main/java/haveno/core/support/dispute/DisputeManager.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -598,8 +598,8 @@ protected void handle(DisputeOpenedMessage message) {
598598
// TODO: DisputeOpenedMessage should include arbitrator's updated multisig hex too
599599
// TODO: arbitrator needs to import multisig info then scan for updated state?
600600

601-
// arbitrator syncs and polls wallet unless finalized
602-
if (trade.isArbitrator() && !trade.isPayoutFinalized()) {
601+
// sync and poll wallet unless finalized
602+
if (!trade.isPayoutFinalized()) {
603603
trade.syncAndPollWallet();
604604
trade.recoverIfMissingWalletData();
605605
}
@@ -1007,7 +1007,7 @@ public MoneroTxWallet createDisputePayoutTx(Trade trade, Contract contract, Disp
10071007
// update trade state
10081008
if (updateState) {
10091009
trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
1010-
trade.updatePayout(payoutTx);
1010+
trade.setPayoutTx(payoutTx);
10111011
if (trade.getBuyer().getUpdatedMultisigHex() != null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
10121012
if (trade.getSeller().getUpdatedMultisigHex() != null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
10131013
}

core/src/main/java/haveno/core/support/dispute/arbitration/ArbitrationManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ private MoneroTxSet processDisputePayoutTx(Trade trade) {
520520
}
521521

522522
// update state
523-
trade.updatePayout(disputeTxSet.getTxs().get(0));
523+
trade.setPayoutTx(disputeTxSet.getTxs().get(0));
524524
trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
525525
dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash());
526526
requestPersistence(trade);

core/src/main/java/haveno/core/trade/Trade.java

Lines changed: 212 additions & 206 deletions
Large diffs are not rendered by default.

core/src/main/java/haveno/core/trade/TradeManager.java

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
172172
private final PersistenceManager<TradableList<Trade>> persistenceManager;
173173
private final TradableList<Trade> tradableList = new TradableList<>();
174174
@Getter
175-
private final BooleanProperty persistedTradesInitialized = new SimpleBooleanProperty();
175+
private final BooleanProperty tradesInitialized = new SimpleBooleanProperty();
176176
@Getter
177177
private final LongProperty numPendingTrades = new SimpleLongProperty();
178178
private final ReferralIdService referralIdService;
@@ -454,18 +454,21 @@ private void initTrades() {
454454
// remove skipped trades
455455
trades.removeAll(tradesToSkip);
456456

457-
// sync idle trades once in background after active trades
457+
// arbitrator syncs idle trades once in background after active trades
458458
for (Trade trade : trades) {
459-
if (trade.isIdling()) ThreadUtils.submitToPool(() -> {
459+
if (!trade.isArbitrator()) continue;
460+
if (trade.isIdling()) {
461+
ThreadUtils.submitToPool(() -> {
460462

461-
// add random delay to avoid syncing at exactly the same time
462-
if (trades.size() > 1 && trade.walletExists()) {
463-
int delay = (int) (Math.random() * INIT_TRADE_RANDOM_DELAY_MS);
464-
HavenoUtils.waitFor(delay);
465-
}
466-
467-
trade.syncAndPollWallet();
468-
});
463+
// add random delay to avoid syncing at exactly the same time
464+
if (trades.size() > 1 && trade.walletExists()) {
465+
int delay = (int) (Math.random() * INIT_TRADE_RANDOM_DELAY_MS);
466+
HavenoUtils.waitFor(delay);
467+
}
468+
469+
trade.syncAndPollWallet();
470+
});
471+
}
469472
}
470473

471474
// process after all wallets initialized
@@ -494,7 +497,7 @@ private void initTrades() {
494497

495498
// notify that persisted trades initialized
496499
if (isShutDownStarted) return;
497-
persistedTradesInitialized.set(true);
500+
tradesInitialized.set(true);
498501
getObservableList().addListener((ListChangeListener<Trade>) change -> onTradesChanged());
499502
onTradesChanged();
500503

@@ -1306,8 +1309,8 @@ public ObservableList<Trade> getObservableList() {
13061309
}
13071310
}
13081311

1309-
public BooleanProperty persistedTradesInitializedProperty() {
1310-
return persistedTradesInitialized;
1312+
public BooleanProperty tradesInitializedProperty() {
1313+
return tradesInitialized;
13111314
}
13121315

13131316
public boolean isMyOffer(Offer offer) {

core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -711,7 +711,7 @@ private void handle(PaymentReceivedMessage message, NodeAddress peer, boolean re
711711

712712
// handle payout error
713713
lastAckedPaymentReceivedMessage = message;
714-
trade.onPayoutError(false, null);
714+
trade.onPayoutError(false, false, null);
715715
handleTaskRunnerFault(peer, message, null, errorMessage, trade.getSelf().getUpdatedMultisigHex()); // send nack
716716
}
717717
})))
@@ -812,10 +812,12 @@ private void onAckMessageAux(AckMessage ackMessage, NodeAddress sender) {
812812
peer.setNodeAddress(sender);
813813
}
814814

815-
// 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
815+
// handle nack of InitTradeRequest from arbitrator to maker
816816
if (!ackMessage.isSuccess() && trade.isMaker() && peer == trade.getArbitrator() && ackMessage.getSourceMsgClassName().equals(InitTradeRequest.class.getSimpleName())) {
817-
if (ackMessage.getErrorMessage() != null && ackMessage.getErrorMessage().contains(SEND_INIT_TRADE_REQUEST_FAILED)) {
817+
if (ignoreInitTradeRequestNackFromArbitrator(ackMessage)) {
818+
log.warn("Ignoring InitTradeRequest NACK from arbitrator, offerId={}, errorMessage={}", processModel.getOfferId(), ackMessage.getErrorMessage());
818819
// use default postprocessing
820+
} else {
819821
if (makerInitTradeRequestHasBeenNacked) {
820822
handleSecondMakerInitTradeRequestNack(ackMessage);
821823
// use default postprocessing
@@ -892,7 +894,7 @@ private void onAckMessageAux(AckMessage ackMessage, NodeAddress sender) {
892894
if (ackMessage.getUpdatedMultisigHex() != null) {
893895
trade.getBuyer().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex());
894896
processModel.getTradeManager().persistNow(null);
895-
boolean autoResent = onPayoutError(true, peer);
897+
boolean autoResent = onPaymentReceivedNack(true, peer);
896898
if (autoResent) return; // skip remaining processing if auto resent
897899
}
898900
}
@@ -911,7 +913,7 @@ else if (peer == trade.getArbitrator()) {
911913
if (ackMessage.getUpdatedMultisigHex() != null) {
912914
trade.getArbitrator().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex());
913915
processModel.getTradeManager().persistNow(null);
914-
boolean autoResent = onPayoutError(true, peer);
916+
boolean autoResent = onPaymentReceivedNack(true, peer);
915917
if (autoResent) return; // skip remaining processing if auto resent
916918
}
917919
}
@@ -939,17 +941,23 @@ else if (peer == trade.getArbitrator()) {
939941
trade.onAckMessage(ackMessage, sender);
940942
}
941943

942-
private boolean onPayoutError(boolean syncAndPoll, TradePeer peer) {
944+
private static boolean ignoreInitTradeRequestNackFromArbitrator(AckMessage ackMessage) {
945+
return ackMessage.getErrorMessage() != null && ackMessage.getErrorMessage().contains(SEND_INIT_TRADE_REQUEST_FAILED); // ignore if arbitrator's request failed to taker
946+
}
947+
948+
private boolean onPaymentReceivedNack(boolean syncAndPoll, TradePeer peer) {
943949

944950
// prevent infinite nack loop with max attempts
945951
numPaymentReceivedNacks++;
946952
if (numPaymentReceivedNacks > MAX_PAYMENT_RECEIVED_NACKS) {
947-
log.warn("Maximum number of PaymentReceivedMessage NACKs reached for {} {}, not retrying", trade.getClass().getSimpleName(), trade.getId());
953+
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.";
954+
log.warn(errorMsg);
955+
trade.setErrorMessage(errorMsg);
948956
return false;
949957
}
950958

951959
// handle payout error
952-
return trade.onPayoutError(syncAndPoll, peer);
960+
return trade.onPayoutError(syncAndPoll, true, peer);
953961
}
954962

955963
private void handleFirstMakerInitTradeRequestNack(AckMessage ackMessage) {

core/src/main/java/haveno/core/trade/protocol/tasks/BuyerPreparePaymentSentMessage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ protected void run() {
9090
// create payout tx
9191
log.info("Buyer creating unsigned payout tx for {} {} ", trade.getClass().getSimpleName(), trade.getShortId());
9292
MoneroTxWallet payoutTx = trade.createPayoutTx();
93-
trade.updatePayout(payoutTx);
93+
trade.setPayoutTx(payoutTx);
9494
trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
9595
trade.requestPersistence();
9696
}

core/src/main/java/haveno/core/trade/protocol/tasks/ProcessPaymentReceivedMessage.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import haveno.core.trade.BuyerTrade;
4242
import haveno.core.trade.HavenoUtils;
4343
import haveno.core.trade.Trade;
44+
import haveno.core.trade.Trade.State;
4445
import haveno.core.trade.messages.PaymentReceivedMessage;
4546
import haveno.core.trade.messages.PaymentSentMessage;
4647
import haveno.core.util.Validator;
@@ -82,6 +83,9 @@ protected void run() {
8283
return;
8384
}
8485

86+
// set state to confirmed payment receipt before processing
87+
trade.advanceState(State.SELLER_CONFIRMED_PAYMENT_RECEIPT);
88+
8589
// cannot process until wallet sees deposits unlocked
8690
if (!trade.isDepositsUnlocked()) {
8791
trade.syncAndPollWallet();
@@ -179,9 +183,6 @@ private void processPayoutTx(PaymentReceivedMessage message) {
179183
else throw e;
180184
}
181185
}
182-
} else {
183-
log.info("Payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
184-
if (message.getSignedPayoutTxHex() != null && !trade.isPayoutConfirmed()) trade.processPayoutTx(message.getSignedPayoutTxHex(), false, true);
185186
}
186187
}
187188
}

core/src/main/java/haveno/core/trade/protocol/tasks/SellerPreparePaymentReceivedMessage.java

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,14 @@ else if (trade.getSelf().getUnsignedPayoutTxHex() == null) {
112112

113113
private void createUnsignedPayoutTx() {
114114
log.info("Seller creating unsigned payout tx for trade {}", trade.getId());
115-
trade.getProcessModel().setPaymentSentPayoutTxStale(true);
116-
MoneroTxWallet payoutTx = trade.createPayoutTx();
117-
trade.updatePayout(payoutTx);
118-
trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
115+
try {
116+
trade.getProcessModel().setPaymentSentPayoutTxStale(true);
117+
MoneroTxWallet payoutTx = trade.createPayoutTx();
118+
trade.setPayoutTx(payoutTx);
119+
trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
120+
} catch (Exception e) {
121+
if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId());
122+
else throw e;
123+
}
119124
}
120125
}

core/src/main/java/haveno/core/trade/protocol/tasks/SellerSendPaymentReceivedMessage.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ protected boolean stopSending() {
255255
if (isMessageReceived()) return true; // stop if message received
256256
if (!trade.isPaymentReceived()) return true; // stop if trade state reset
257257
if (trade.isPayoutPublished() && !((SellerTrade) trade).resendPaymentReceivedMessagesWithinDuration()) return true; // stop if payout is published and we are not in the resend period
258+
259+
// check if message state is outdated
258260
if (unsignedPayoutTxHex != null && !StringUtils.equals(unsignedPayoutTxHex, trade.getSelf().getUnsignedPayoutTxHex())) return true;
259261
if (signedPayoutTxHex != null && !StringUtils.equals(signedPayoutTxHex, trade.getPayoutTxHex())) return true;
260262
if (updatedMultisigHex != null && !StringUtils.equals(updatedMultisigHex, trade.getSelf().getUpdatedMultisigHex())) return true;

desktop/src/main/java/haveno/desktop/main/MainViewModel.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ public MainViewModel(HavenoSetup havenoSetup,
217217
@Override
218218
public void onSetupComplete() {
219219
// We handle the trade period here as we display a global popup if we reached dispute time
220-
tradesAndUIReady = EasyBind.combine(isSplashScreenRemoved, tradeManager.persistedTradesInitializedProperty(),
220+
tradesAndUIReady = EasyBind.combine(isSplashScreenRemoved, tradeManager.tradesInitializedProperty(),
221221
(a, b) -> a && b);
222222
tradesAndUIReady.subscribe((observable, oldValue, newValue) -> {
223223
if (newValue) {

0 commit comments

Comments
 (0)