use nack flow if cannot create payout tx and stop repeat sending

This commit is contained in:
woodser 2025-09-24 01:38:59 -04:00
parent 0bf6052b7c
commit 4989eab498
No known key found for this signature in database
GPG key ID: 55A10DD48ADEE5EF
7 changed files with 44 additions and 21 deletions

View file

@ -542,7 +542,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
break;
} catch (Exception e) {
if (trade.isPayoutPublished()) return null;
if (HavenoUtils.isTransactionRejected(e) || HavenoUtils.isNotEnoughSigners(e) || HavenoUtils.isFailedToParse(e)) throw new IllegalArgumentException(e);
if (HavenoUtils.isMultisigError(e)) throw new IllegalArgumentException(e);
log.warn("Failed to submit dispute payout tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
if (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection(sourceConnection);

View file

@ -630,22 +630,35 @@ public class HavenoUtils {
return isConnectionRefused(e) || isReadTimeout(e) || XmrWalletBase.isSyncWithProgressTimeout(e);
}
public static boolean isNotEnoughSigners(Throwable e) {
private static boolean isNotEnoughSigners(Throwable e) {
return e != null && e.getMessage().contains("Not enough signers");
}
public static boolean isFailedToParse(Throwable e) {
private static boolean isFailedToParse(Throwable e) {
return e != null && e.getMessage().contains("Failed to parse");
}
private static boolean isStaleData(Throwable e) {
return e != null && e.getMessage().contains("stale data");
}
private static boolean isNoTransactionCreated(Throwable e) {
return e != null && e.getMessage().contains("No transaction created");
}
private static boolean isLRNotFound(Throwable e) {
return e != null && e.getMessage().contains("LR not found for enough participants");
}
// TODO: handling specific error messages is brittle, inverse so all errors are illegal except known local issues?
public static boolean isMultisigError(Throwable e) {
return isLRNotFound(e) || isNotEnoughSigners(e) || isNoTransactionCreated(e) || isFailedToParse(e) || isStaleData(e);
}
public static boolean isTransactionRejected(Throwable e) {
return e != null && e.getMessage().contains("was rejected");
}
public static boolean isLRNotFound(Throwable e) {
return e != null && e.getMessage().contains("LR not found for enough participants");
}
public static boolean isIllegal(Throwable e) {
return e instanceof IllegalArgumentException || e instanceof IllegalStateException;
}

View file

@ -828,7 +828,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
setState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
for (TradePeer peer : getAllPeers()) {
peer.setPaymentReceivedMessage(null);
peer.setPaymentReceivedMessageState(MessageState.UNDEFINED);
}
setPayoutTxHex(null);
}
@ -1385,7 +1384,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
.setRelay(false)
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY));
} catch (Exception e) {
if (HavenoUtils.isLRNotFound(e)) throw new IllegalStateException(e);
if (HavenoUtils.isMultisigError(e)) throw new IllegalStateException(e);
else throw e;
}
@ -1409,6 +1408,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
} catch (IllegalArgumentException | IllegalStateException e) {
throw e;
} catch (Exception e) {
if (HavenoUtils.isMultisigError(e)) throw new IllegalStateException(e);
if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee");
handleWalletError(e, sourceConnection, i + 1);
doPollWallet();
@ -1557,7 +1557,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
setPayoutStatePublished();
} catch (Exception e) {
if (!isPayoutPublished()) {
if (HavenoUtils.isTransactionRejected(e) || HavenoUtils.isNotEnoughSigners(e) || HavenoUtils.isFailedToParse(e)) throw new IllegalArgumentException(e);
if (HavenoUtils.isTransactionRejected(e) || HavenoUtils.isMultisigError(e)) throw new IllegalArgumentException(e);
throw new RuntimeException("Failed to submit payout tx for " + getClass().getSimpleName() + " " + getId() + ", error=" + e.getMessage(), e);
}
}
@ -2191,13 +2191,13 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
throw new RuntimeException("Trade is not maker, taker, or arbitrator");
}
private List<TradePeer> getOtherPeers() {
public List<TradePeer> getOtherPeers() {
List<TradePeer> peers = getAllPeers();
if (!peers.remove(getSelf())) throw new IllegalStateException("Failed to remove self from list of peers");
return peers;
}
private List<TradePeer> getAllPeers() {
public List<TradePeer> getAllPeers() {
List<TradePeer> peers = new ArrayList<TradePeer>();
peers.add(getMaker());
peers.add(getTaker());
@ -3194,7 +3194,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
processModel.setPaymentSentPayoutTxStale(true);
if (paymentReceivedNackSender != null) {
paymentReceivedNackSender.setPaymentReceivedMessage(null);
paymentReceivedNackSender.setPaymentReceivedMessageState(MessageState.UNDEFINED);
paymentReceivedNackSender.setPaymentReceivedMessageState(MessageState.NACKED);
}
if (!isPayoutPublished()) {
getSelf().setUnsignedPayoutTxHex(null);
@ -3302,8 +3302,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
// rescan blockchain
rescanBlockchain();
// import multisig hex
log.warn("Importing multisig hex to recover wallet data for {} {}", getClass().getSimpleName(), getShortId());
// must import multisig hex after rescan
log.warn("Importing multisig hex after rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId());
importMultisigHex();
// poll wallet

View file

@ -120,7 +120,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
private boolean makerInitTradeRequestHasBeenNacked = false;
private PaymentReceivedMessage lastAckedPaymentReceivedMessage = null;
private static int MAX_PAYMENT_RECEIVED_NACKS = 5;
private static int MAX_PAYMENT_RECEIVED_NACKS = 6;
private int numPaymentReceivedNacks = 0;
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -47,7 +47,6 @@ import haveno.core.trade.messages.PaymentSentMessage;
import haveno.core.util.Validator;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
@ -66,7 +65,6 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
PaymentReceivedMessage message = (PaymentReceivedMessage) processModel.getTradeMessage();
checkNotNull(message);
Validator.checkTradeId(processModel.getOfferId(), message);
checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "No payout tx hex provided");
// verify signature of payment received message
HavenoUtils.verifyPaymentReceivedMessage(trade, message);
@ -146,6 +144,11 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
// handle if payout tx not published
if (!trade.isPayoutPublished()) {
// nack with updated multisig info if no payout tx provided
if (message.getUnsignedPayoutTxHex() == null && message.getSignedPayoutTxHex() == null && message.getPayoutTxId() == null) {
throw new IllegalStateException("No payout tx provided in PaymentReceivedMessage for " + trade.getClass().getSimpleName() + " " + trade.getId());
}
// wait to publish payout tx if defer flag set from seller (payout is expected)
if (message.isDeferPublishPayout()) {
log.info("Deferring publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());

View file

@ -107,7 +107,13 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
trade.requestPersistence();
complete();
} catch (Throwable t) {
failed(t);
if (HavenoUtils.isIllegal(t)) {
log.error("Illegal exception preparing payment received message in {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), t.getMessage(), t);
trade.exportMultisigHex();
complete(); // proceed to send the message to perform nack flow with updated multsig state
} else {
failed(t);
}
}
}

View file

@ -96,6 +96,9 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
try {
runInterceptHook();
// reset ack state
getReceiver().setPaymentReceivedMessageState(MessageState.UNDEFINED);
// skip if stopped
if (stopSending()) {
if (!isCompleted()) complete();
@ -149,8 +152,6 @@ public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessag
// verify message
if (trade.isPayoutPublished()) {
checkArgument(message.getUpdatedMultisigHex() != null || message.getPayoutTxId() != null, "PaymentReceivedMessage does not include updated multisig hex or payout tx id after payout published");
} else {
checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "PaymentReceivedMessage does not include payout tx hex");
}
// sign message