maker recreates reserve tx then cancels offer on trade nacks

This commit is contained in:
woodser 2025-06-01 08:54:04 -04:00 committed by woodser
parent 264e5f436e
commit 33a91cf980
6 changed files with 231 additions and 15 deletions

View file

@ -276,6 +276,10 @@ public final class OpenOffer implements Tradable {
return state == State.AVAILABLE;
}
public boolean isReserved() {
return state == State.RESERVED;
}
public boolean isDeactivated() {
return state == State.DEACTIVATED;
}

View file

@ -661,7 +661,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
ErrorMessageHandler errorMessageHandler) {
log.info("Canceling open offer: {}", openOffer.getId());
if (!offersToBeEdited.containsKey(openOffer.getId())) {
if (openOffer.isAvailable()) {
if (isOnOfferBook(openOffer)) {
openOffer.setState(OpenOffer.State.CANCELED);
offerBookService.removeOffer(openOffer.getOffer().getOfferPayload(),
() -> {
@ -683,6 +683,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}
}
private boolean isOnOfferBook(OpenOffer openOffer) {
return openOffer.isAvailable() || openOffer.isReserved();
}
public void editOpenOfferStart(OpenOffer openOffer,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {

View file

@ -997,7 +997,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
log.warn("Unregistering {} {}", trade.getClass().getSimpleName(), trade.getId());
removeTrade(trade, true);
removeFailedTrade(trade);
xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); // TODO The address entry should have been removed already. Check and if its the case remove that.
if (!trade.isMaker()) xmrWalletService.swapPayoutAddressEntryToAvailable(trade.getId()); // TODO The address entry should have been removed already. Check and if its the case remove that.
requestPersistence();
}

View file

@ -43,6 +43,7 @@ import haveno.common.handlers.ErrorMessageHandler;
import haveno.common.proto.network.NetworkEnvelope;
import haveno.common.taskrunner.Task;
import haveno.core.network.MessageState;
import haveno.core.offer.OpenOffer;
import haveno.core.trade.ArbitratorTrade;
import haveno.core.trade.BuyerTrade;
import haveno.core.trade.HavenoUtils;
@ -55,13 +56,17 @@ import haveno.core.trade.messages.DepositRequest;
import haveno.core.trade.messages.DepositResponse;
import haveno.core.trade.messages.DepositsConfirmedMessage;
import haveno.core.trade.messages.InitMultisigRequest;
import haveno.core.trade.messages.InitTradeRequest;
import haveno.core.trade.messages.PaymentReceivedMessage;
import haveno.core.trade.messages.PaymentSentMessage;
import haveno.core.trade.messages.SignContractRequest;
import haveno.core.trade.messages.SignContractResponse;
import haveno.core.trade.messages.TradeMessage;
import haveno.core.trade.protocol.FluentProtocol.Condition;
import haveno.core.trade.protocol.FluentProtocol.Event;
import haveno.core.trade.protocol.tasks.ApplyFilter;
import haveno.core.trade.protocol.tasks.MakerRecreateReserveTx;
import haveno.core.trade.protocol.tasks.MakerSendInitTradeRequestToArbitrator;
import haveno.core.trade.protocol.tasks.MaybeSendSignContractRequest;
import haveno.core.trade.protocol.tasks.ProcessDepositResponse;
import haveno.core.trade.protocol.tasks.ProcessDepositsConfirmedMessage;
@ -110,6 +115,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
private boolean depositsConfirmedTasksCalled;
private int reprocessPaymentSentMessageCount;
private int reprocessPaymentReceivedMessageCount;
private boolean makerInitTradeRequestNacked = false;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -758,6 +764,18 @@ 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
if (!ackMessage.isSuccess() && trade.isMaker() && peer == trade.getArbitrator() && ackMessage.getSourceMsgClassName().equals(InitTradeRequest.class.getSimpleName())) {
if (makerInitTradeRequestNacked) {
handleSecondMakerInitTradeRequestNack(ackMessage);
// use default postprocessing
} else {
makerInitTradeRequestNacked = true;
handleFirstMakerInitTradeRequestNack(ackMessage);
return;
}
}
// handle nack of deposit request
if (ackMessage.getSourceMsgClassName().equals(DepositRequest.class.getSimpleName())) {
if (!ackMessage.isSuccess()) {
@ -774,12 +792,12 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// handle ack message for PaymentSentMessage, which automatically re-sends if not ACKed in a certain time
if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) {
if (trade.getTradePeer(sender) == trade.getSeller()) {
if (peer == trade.getSeller()) {
trade.getSeller().setPaymentSentAckMessage(ackMessage);
if (ackMessage.isSuccess()) trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
else trade.setState(Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG);
processModel.getTradeManager().requestPersistence();
} else if (trade.getTradePeer(sender) == trade.getArbitrator()) {
} else if (peer == trade.getArbitrator()) {
trade.getArbitrator().setPaymentSentAckMessage(ackMessage);
processModel.getTradeManager().requestPersistence();
} else {
@ -792,7 +810,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
if (ackMessage.getSourceMsgClassName().equals(PaymentReceivedMessage.class.getSimpleName())) {
// ack message from buyer
if (trade.getTradePeer(sender) == trade.getBuyer()) {
if (peer == trade.getBuyer()) {
trade.getBuyer().setPaymentReceivedAckMessage(ackMessage);
// handle successful ack
@ -819,7 +837,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
}
// ack message from arbitrator
else if (trade.getTradePeer(sender) == trade.getArbitrator()) {
else if (peer == trade.getArbitrator()) {
trade.getArbitrator().setPaymentReceivedAckMessage(ackMessage);
// handle nack
@ -856,6 +874,48 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
trade.onAckMessage(ackMessage, sender);
}
private void handleFirstMakerInitTradeRequestNack(AckMessage ackMessage) {
log.warn("Maker received NACK to InitTradeRequest from arbitrator for {} {}, messageUid={}, errorMessage={}", trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage());
ThreadUtils.execute(() -> {
Event event = new Event() {
@Override
public String name() {
return "MakerRecreateReserveTx";
}
};
synchronized (trade.getLock()) {
latchTrade();
expect(phase(Trade.Phase.INIT)
.with(event))
.setup(tasks(
MakerRecreateReserveTx.class,
MakerSendInitTradeRequestToArbitrator.class)
.using(new TradeTaskRunner(trade,
() -> {
startTimeout();
unlatchTrade();
},
errorMessage -> {
handleError("Failed to re-send InitTradeRequest to arbitrator for " + trade.getClass().getSimpleName() + " " + trade.getId() + ": " + errorMessage);
}))
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true);
awaitTradeLatch();
}
}, trade.getId());
}
private void handleSecondMakerInitTradeRequestNack(AckMessage ackMessage) {
log.warn("Maker received 2nd NACK to InitTradeRequest from arbitrator for {} {}, messageUid={}, errorMessage={}", trade.getClass().getSimpleName(), trade.getId(), ackMessage.getSourceUid(), ackMessage.getErrorMessage());
String warningMessage = "Your offer (" + trade.getOffer().getShortId() + ") has been removed because there was a problem taking the trade.\n\nError message: " + ackMessage.getErrorMessage();
OpenOffer openOffer = HavenoUtils.openOfferManager.getOpenOffer(trade.getId()).orElse(null);
if (openOffer != null) {
HavenoUtils.openOfferManager.cancelOpenOffer(openOffer, null, null);
HavenoUtils.setTopError(warningMessage);
}
log.warn(warningMessage);
}
private boolean isPaymentReceivedMessageAckedByEither() {
if (trade.getBuyer().getPaymentReceivedMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return true;
if (trade.getArbitrator().getPaymentReceivedMessageStateProperty().get() == MessageState.ACKNOWLEDGED) return true;
@ -992,11 +1052,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
void handleTaskRunnerFault(NodeAddress ackReceiver, @Nullable TradeMessage message, String source, String errorMessage, String updatedMultisigHex) {
log.error("Task runner failed with error {}. Triggered from {}. Monerod={}" , errorMessage, source, trade.getXmrWalletService().getXmrConnectionService().getConnection());
handleError(errorMessage);
if (message != null) {
sendAckMessage(ackReceiver, message, false, errorMessage, updatedMultisigHex);
}
handleError(errorMessage);
}
// these are not thread safe, so they must be used within a lock on the trade
@ -1006,9 +1066,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
log.error(errorMessage);
trade.setErrorMessage(errorMessage);
processModel.getTradeManager().requestPersistence();
unlatchTrade();
if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(errorMessage);
errorMessageHandler = null;
unlatchTrade();
}
protected void latchTrade() {

View file

@ -0,0 +1,147 @@
/*
* This file is part of Haveno.
*
* Haveno is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Haveno is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.protocol.tasks;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.offer.Offer;
import haveno.core.offer.OfferDirection;
import haveno.core.offer.OpenOffer;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.MakerTrade;
import haveno.core.trade.Trade;
import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.xmr.model.XmrAddressEntry;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.wallet.model.MoneroTxWallet;
import java.math.BigInteger;
@Slf4j
public class MakerRecreateReserveTx extends TradeTask {
public MakerRecreateReserveTx(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
// maker trade expected
if (!(trade instanceof MakerTrade)) {
throw new RuntimeException("Expected maker trade but was " + trade.getClass().getSimpleName() + " " + trade.getShortId() + ". That should never happen.");
}
// get open offer
OpenOffer openOffer = HavenoUtils.openOfferManager.getOpenOffer(trade.getOffer().getId()).orElse(null);
if (openOffer == null) throw new RuntimeException("Open offer not found for " + trade.getClass().getSimpleName() + " " + trade.getId());
Offer offer = openOffer.getOffer();
// reset reserve tx state
trade.getSelf().setReserveTxHex(null);
trade.getSelf().setReserveTxHash(null);
trade.getSelf().setReserveTxKey(null);
trade.getSelf().setReserveTxKeyImages(null);
// recreate reserve tx
log.warn("Maker is recreating reserve tx for tradeId={}", trade.getShortId());
MoneroTxWallet reserveTx = null;
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
// check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId());
trade.startProtocolTimeout();
// thaw reserved key images
log.info("Thawing reserve tx key images for tradeId={}", trade.getShortId());
HavenoUtils.xmrWalletService.thawOutputs(openOffer.getOffer().getOfferPayload().getReserveTxKeyImages());
// check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while thawing key images, tradeId=" + trade.getShortId());
trade.startProtocolTimeout();
// collect relevant info
BigInteger penaltyFee = HavenoUtils.multiply(offer.getAmount(), offer.getPenaltyFeePct());
BigInteger makerFee = offer.getMaxMakerFee();
BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.ZERO : offer.getAmount();
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getMaxBuyerSecurityDeposit() : offer.getMaxSellerSecurityDeposit();
String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString();
XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null);
Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex();
// attempt re-creating reserve tx
try {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
try {
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
} catch (IllegalStateException e) {
log.warn("Illegal state creating reserve tx, tradeId={}, error={}", trade.getShortId(), i + 1, e.getMessage());
throw e;
} catch (Exception e) {
log.warn("Error creating reserve tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
// check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId());
if (reserveTx != null) break;
}
}
} catch (Exception e) {
// reset state
if (reserveTx != null) model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
model.getXmrWalletService().freezeOutputs(offer.getOfferPayload().getReserveTxKeyImages());
trade.getSelf().setReserveTxKeyImages(null);
throw e;
}
// reset protocol timeout
trade.startProtocolTimeout();
// update state
trade.getSelf().setReserveTxHash(reserveTx.getHash());
trade.getSelf().setReserveTxHex(reserveTx.getFullHex());
trade.getSelf().setReserveTxKey(reserveTx.getKey());
trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx));
trade.getXmrWalletService().freezeOutputs(HavenoUtils.getInputKeyImages(reserveTx));
}
// save process state
processModel.setReserveTx(reserveTx); // TODO: remove this? how is it used?
processModel.getTradeManager().requestPersistence();
complete();
} catch (Throwable t) {
trade.setErrorMessage("An error occurred.\n" +
"Error message:\n"
+ t.getMessage());
failed(t);
}
}
private boolean isTimedOut() {
return !processModel.getTradeManager().hasOpenTrade(trade);
}
}

View file

@ -88,7 +88,7 @@ public class TakerReserveTradeFunds extends TradeTask {
}
} catch (Exception e) {
// reset state with wallet lock
// reset state
model.getXmrWalletService().swapPayoutAddressEntryToAvailable(trade.getId());
if (reserveTx != null) {
model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
@ -101,11 +101,12 @@ public class TakerReserveTradeFunds extends TradeTask {
// reset protocol timeout
trade.startProtocolTimeout();
// update trade state
trade.getTaker().setReserveTxHash(reserveTx.getHash());
trade.getTaker().setReserveTxHex(reserveTx.getFullHex());
trade.getTaker().setReserveTxKey(reserveTx.getKey());
trade.getTaker().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx));
// update state
trade.getSelf().setReserveTxHash(reserveTx.getHash());
trade.getSelf().setReserveTxHex(reserveTx.getFullHex());
trade.getSelf().setReserveTxKey(reserveTx.getKey());
trade.getSelf().setReserveTxKeyImages(HavenoUtils.getInputKeyImages(reserveTx));
trade.getXmrWalletService().freezeOutputs(HavenoUtils.getInputKeyImages(reserveTx));
}
}