mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-06-24 14:50:28 -04:00
maker recreates reserve tx then cancels offer on trade nacks
This commit is contained in:
parent
264e5f436e
commit
33a91cf980
6 changed files with 231 additions and 15 deletions
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue