preserve offers unless invalid #1115

This commit is contained in:
woodser 2024-07-12 14:05:07 -04:00
parent 06b0c20bad
commit d69dcae875
6 changed files with 90 additions and 75 deletions

View File

@ -53,7 +53,7 @@ import java.util.Optional;
public final class OpenOffer implements Tradable { public final class OpenOffer implements Tradable {
public enum State { public enum State {
SCHEDULED, PENDING,
AVAILABLE, AVAILABLE,
RESERVED, RESERVED,
CLOSED, CLOSED,
@ -120,7 +120,7 @@ public final class OpenOffer implements Tradable {
this.offer = offer; this.offer = offer;
this.triggerPrice = triggerPrice; this.triggerPrice = triggerPrice;
this.reserveExactAmount = reserveExactAmount; this.reserveExactAmount = reserveExactAmount;
state = State.SCHEDULED; state = State.PENDING;
} }
public OpenOffer(Offer offer, long triggerPrice, OpenOffer openOffer) { public OpenOffer(Offer offer, long triggerPrice, OpenOffer openOffer) {
@ -165,8 +165,8 @@ public final class OpenOffer implements Tradable {
this.reserveTxHex = reserveTxHex; this.reserveTxHex = reserveTxHex;
this.reserveTxKey = reserveTxKey; this.reserveTxKey = reserveTxKey;
if (this.state == State.RESERVED) // reset reserved state to available
setState(State.AVAILABLE); if (this.state == State.RESERVED) setState(State.AVAILABLE);
} }
@Override @Override
@ -232,8 +232,8 @@ public final class OpenOffer implements Tradable {
return stateProperty; return stateProperty;
} }
public boolean isScheduled() { public boolean isPending() {
return state == State.SCHEDULED; return state == State.PENDING;
} }
public boolean isAvailable() { public boolean isAvailable() {

View File

@ -132,7 +132,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private static final long REPUBLISH_AGAIN_AT_STARTUP_DELAY_SEC = 30; private static final long REPUBLISH_AGAIN_AT_STARTUP_DELAY_SEC = 30;
private static final long REPUBLISH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(30); private static final long REPUBLISH_INTERVAL_MS = TimeUnit.MINUTES.toMillis(30);
private static final long REFRESH_INTERVAL_MS = OfferPayload.TTL / 2; private static final long REFRESH_INTERVAL_MS = OfferPayload.TTL / 2;
private static final int MAX_PROCESS_ATTEMPTS = 5; private static final int NUM_ATTEMPTS_THRESHOLD = 5; // process pending offer only on republish cycle after this many attempts
private final CoreContext coreContext; private final CoreContext coreContext;
private final KeyRing keyRing; private final KeyRing keyRing;
@ -252,17 +252,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// read open offers // read open offers
persistenceManager.readPersisted(persisted -> { persistenceManager.readPersisted(persisted -> {
openOffers.setAll(persisted.getList()); openOffers.setAll(persisted.getList());
openOffers.forEach(openOffer -> openOffer.getOffer().setPriceFeedService(priceFeedService)); openOffers.forEach(openOffer -> openOffer.getOffer().setPriceFeedService(priceFeedService));
// read signed offers // read signed offers
signedOfferPersistenceManager.readPersisted(signedOfferPersisted -> { signedOfferPersistenceManager.readPersisted(signedOfferPersisted -> {
signedOffers.setAll(signedOfferPersisted.getList()); signedOffers.setAll(signedOfferPersisted.getList());
completeHandler.run(); completeHandler.run();
}, },
completeHandler); completeHandler);
}, },
completeHandler); completeHandler);
} }
private synchronized void maybeInitializeKeyImagePoller() { private synchronized void maybeInitializeKeyImagePoller() {
@ -472,17 +472,17 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// .forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService) // .forEach(openOffer -> OfferUtil.getInvalidMakerFeeTxErrorMessage(openOffer.getOffer(), btcWalletService)
// .ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg)))); // .ifPresent(errorMsg -> invalidOffers.add(new Tuple2<>(openOffer, errorMsg))));
// process scheduled offers // process pending offers
processScheduledOffers((transaction) -> {}, (errorMessage) -> { processPendingOffers(false);
log.warn("Error processing unposted offers: " + errorMessage);
});
// register to process unposted offers on new block // register to process pending offers on new block
xmrWalletService.addWalletListener(new MoneroWalletListener() { xmrWalletService.addWalletListener(new MoneroWalletListener() {
@Override @Override
public void onNewBlock(long height) { public void onNewBlock(long height) {
processScheduledOffers((transaction) -> {}, (errorMessage) -> {
log.warn("Error processing unposted offers on new block {}: {}", height, errorMessage); // process pending offers on new block a few times
processPendingOffers(true, (transaction) -> {}, (errorMessage) -> {
log.warn("Error processing pending offers on new block {}: {}", height, errorMessage);
}); });
} }
}); });
@ -549,16 +549,15 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
synchronized (processOffersLock) { synchronized (processOffersLock) {
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
addOpenOffer(openOffer); addOpenOffer(openOffer);
processUnpostedOffer(getOpenOffers(), openOffer, (transaction) -> { processPendingOffer(getOpenOffers(), openOffer, (transaction) -> {
requestPersistence(); requestPersistence();
latch.countDown(); latch.countDown();
resultHandler.handleResult(transaction); resultHandler.handleResult(transaction);
}, (errorMessage) -> { }, (errorMessage) -> {
if (openOffer.isCanceled()) latch.countDown(); if (openOffer.isCanceled()) latch.countDown();
else { else {
log.warn("Error processing unposted offer {}: {}", openOffer.getId(), errorMessage); log.warn("Error processing pending offer {}: {}", openOffer.getId(), errorMessage);
doCancelOffer(openOffer); doCancelOffer(openOffer);
offer.setErrorMessage(errorMessage);
latch.countDown(); latch.countDown();
errorMessageHandler.handleErrorMessage(errorMessage); errorMessageHandler.handleErrorMessage(errorMessage);
} }
@ -583,9 +582,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
public void activateOpenOffer(OpenOffer openOffer, public void activateOpenOffer(OpenOffer openOffer,
ResultHandler resultHandler, ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
if (openOffer.isScheduled()) { if (openOffer.isPending()) {
resultHandler.handleResult(); // ignore if scheduled resultHandler.handleResult(); // ignore if pending
} else if (!offersToBeEdited.containsKey(openOffer.getId())) { } else if (offersToBeEdited.containsKey(openOffer.getId())) {
errorMessageHandler.handleErrorMessage("You can't activate an offer that is currently edited.");
} else {
Offer offer = openOffer.getOffer(); Offer offer = openOffer.getOffer();
offerBookService.activateOffer(offer, offerBookService.activateOffer(offer,
() -> { () -> {
@ -595,8 +596,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
resultHandler.handleResult(); resultHandler.handleResult();
}, },
errorMessageHandler); errorMessageHandler);
} else {
errorMessageHandler.handleErrorMessage("You can't activate an offer that is currently edited.");
} }
} }
@ -858,26 +857,35 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// Place offer helpers // Place offer helpers
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
private void processScheduledOffers(TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler private void processPendingOffers(boolean skipOffersWithTooManyAttempts) {
processPendingOffers(skipOffersWithTooManyAttempts, (transaction) -> {}, (errorMessage) -> {
log.warn("Error processing pending offers: " + errorMessage);
});
}
private void processPendingOffers(boolean skipOffersWithTooManyAttempts,
TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler
ErrorMessageHandler errorMessageHandler) { ErrorMessageHandler errorMessageHandler) {
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (processOffersLock) { synchronized (processOffersLock) {
List<String> errorMessages = new ArrayList<String>(); List<String> errorMessages = new ArrayList<String>();
List<OpenOffer> openOffers = getOpenOffers(); List<OpenOffer> openOffers = getOpenOffers();
for (OpenOffer scheduledOffer : openOffers) { for (OpenOffer pendingOffer : openOffers) {
if (scheduledOffer.getState() != OpenOffer.State.SCHEDULED) continue; if (pendingOffer.getState() != OpenOffer.State.PENDING) continue;
if (skipOffersWithTooManyAttempts && pendingOffer.getNumProcessingAttempts() > NUM_ATTEMPTS_THRESHOLD) continue; // skip offers with too many attempts
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
processUnpostedOffer(openOffers, scheduledOffer, (transaction) -> { processPendingOffer(openOffers, pendingOffer, (transaction) -> {
latch.countDown(); latch.countDown();
}, errorMessage -> { }, errorMessage -> {
if (!scheduledOffer.isCanceled()) { if (!pendingOffer.isCanceled()) {
log.warn("Error processing unposted offer, offerId={}, attempt={}/{}, error={}", scheduledOffer.getId(), scheduledOffer.getNumProcessingAttempts(), MAX_PROCESS_ATTEMPTS, errorMessage); log.warn("Error processing pending offer, offerId={}, attempt={}, error={}", pendingOffer.getId(), pendingOffer.getNumProcessingAttempts(), errorMessage);
if (scheduledOffer.getNumProcessingAttempts() >= MAX_PROCESS_ATTEMPTS) {
log.warn("Offer canceled after {} attempts, offerId={}, error={}", scheduledOffer.getNumProcessingAttempts(), scheduledOffer.getId(), errorMessage);
HavenoUtils.havenoSetup.getTopErrorMsg().set("Offer canceled after " + scheduledOffer.getNumProcessingAttempts() + " attempts. Please switch to a better Monero connection and try again.\n\nOffer ID: " + scheduledOffer.getId() + "\nError: " + errorMessage);
doCancelOffer(scheduledOffer);
}
errorMessages.add(errorMessage); errorMessages.add(errorMessage);
// cancel offer if invalid
if (pendingOffer.getOffer().getState() == Offer.State.INVALID) {
log.warn("Canceling offer because it's invalid: {}", pendingOffer.getId());
doCancelOffer(pendingOffer);
}
} }
latch.countDown(); latch.countDown();
}); });
@ -890,7 +898,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}, THREAD_ID); }, THREAD_ID);
} }
private void processUnpostedOffer(List<OpenOffer> openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { private void processPendingOffer(List<OpenOffer> openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
// skip if already processing // skip if already processing
if (openOffer.isProcessing()) { if (openOffer.isProcessing()) {
@ -900,17 +908,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// process offer // process offer
openOffer.setProcessing(true); openOffer.setProcessing(true);
doProcessUnpostedOffer(openOffers, openOffer, (transaction) -> { doProcessPendingOffer(openOffers, openOffer, (transaction) -> {
openOffer.setProcessing(false); openOffer.setProcessing(false);
resultHandler.handleResult(transaction); resultHandler.handleResult(transaction);
}, (errorMsg) -> { }, (errorMsg) -> {
openOffer.setProcessing(false); openOffer.setProcessing(false);
openOffer.setNumProcessingAttempts(openOffer.getNumProcessingAttempts() + 1); openOffer.setNumProcessingAttempts(openOffer.getNumProcessingAttempts() + 1);
openOffer.getOffer().setErrorMessage(errorMsg);
errorMessageHandler.handleErrorMessage(errorMsg); errorMessageHandler.handleErrorMessage(errorMsg);
}); });
} }
private void doProcessUnpostedOffer(List<OpenOffer> openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { private void doProcessPendingOffer(List<OpenOffer> openOffers, OpenOffer openOffer, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
new Thread(() -> { new Thread(() -> {
try { try {
@ -1075,7 +1084,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
openOffer.setSplitOutputTxFee(splitOutputTx.getFee().longValueExact()); openOffer.setSplitOutputTxFee(splitOutputTx.getFee().longValueExact());
openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash())); openOffer.setScheduledTxHashes(Arrays.asList(splitOutputTx.getHash()));
openOffer.setScheduledAmount(openOffer.getOffer().getAmountNeeded().toString()); openOffer.setScheduledAmount(openOffer.getOffer().getAmountNeeded().toString());
openOffer.setState(OpenOffer.State.SCHEDULED); openOffer.setState(OpenOffer.State.PENDING);
} }
private void scheduleWithEarliestTxs(List<OpenOffer> openOffers, OpenOffer openOffer) { private void scheduleWithEarliestTxs(List<OpenOffer> openOffers, OpenOffer openOffer) {
@ -1106,13 +1115,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// schedule txs // schedule txs
openOffer.setScheduledTxHashes(scheduledTxHashes); openOffer.setScheduledTxHashes(scheduledTxHashes);
openOffer.setScheduledAmount(scheduledAmount.toString()); openOffer.setScheduledAmount(scheduledAmount.toString());
openOffer.setState(OpenOffer.State.SCHEDULED); openOffer.setState(OpenOffer.State.PENDING);
} }
private BigInteger getScheduledAmount(List<OpenOffer> openOffers) { private BigInteger getScheduledAmount(List<OpenOffer> openOffers) {
BigInteger scheduledAmount = BigInteger.ZERO; BigInteger scheduledAmount = BigInteger.ZERO;
for (OpenOffer openOffer : openOffers) { for (OpenOffer openOffer : openOffers) {
if (openOffer.getState() != OpenOffer.State.SCHEDULED) continue; if (openOffer.getState() != OpenOffer.State.PENDING) continue;
if (openOffer.getScheduledTxHashes() == null) continue; if (openOffer.getScheduledTxHashes() == null) continue;
List<MoneroTxWallet> fundingTxs = xmrWalletService.getTxs(openOffer.getScheduledTxHashes()); List<MoneroTxWallet> fundingTxs = xmrWalletService.getTxs(openOffer.getScheduledTxHashes());
for (MoneroTxWallet fundingTx : fundingTxs) { for (MoneroTxWallet fundingTx : fundingTxs) {
@ -1129,7 +1138,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private boolean isTxScheduledByOtherOffer(List<OpenOffer> openOffers, OpenOffer openOffer, String txHash) { private boolean isTxScheduledByOtherOffer(List<OpenOffer> openOffers, OpenOffer openOffer, String txHash) {
for (OpenOffer otherOffer : openOffers) { for (OpenOffer otherOffer : openOffers) {
if (otherOffer == openOffer) continue; if (otherOffer == openOffer) continue;
if (otherOffer.getState() != OpenOffer.State.SCHEDULED) continue; if (otherOffer.getState() != OpenOffer.State.PENDING) continue;
if (otherOffer.getScheduledTxHashes() == null) continue; if (otherOffer.getScheduledTxHashes() == null) continue;
for (String scheduledTxHash : otherOffer.getScheduledTxHashes()) { for (String scheduledTxHash : otherOffer.getScheduledTxHashes()) {
if (txHash.equals(scheduledTxHash)) return true; if (txHash.equals(scheduledTxHash)) return true;
@ -1732,25 +1741,29 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
}); });
} else { } else {
// cancel and recreate offer // reset offer state to pending
doCancelOffer(openOffer); openOffer.getOffer().getOfferPayload().setArbitratorSignature(null);
Offer updatedOffer = new Offer(openOffer.getOffer().getOfferPayload()); openOffer.getOffer().getOfferPayload().setArbitratorSigner(null);
updatedOffer.setPriceFeedService(priceFeedService); openOffer.getOffer().setState(Offer.State.UNKNOWN);
OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, openOffer.getTriggerPrice()); openOffer.setState(OpenOffer.State.PENDING);
// repost offer // republish offer
synchronized (processOffersLock) { synchronized (processOffersLock) {
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
addOpenOffer(updatedOpenOffer); processPendingOffer(getOpenOffers(), openOffer, (transaction) -> {
processUnpostedOffer(getOpenOffers(), updatedOpenOffer, (transaction) -> {
requestPersistence(); requestPersistence();
latch.countDown(); latch.countDown();
if (completeHandler != null) completeHandler.run(); if (completeHandler != null) completeHandler.run();
}, (errorMessage) -> { }, (errorMessage) -> {
if (!updatedOpenOffer.isCanceled()) { if (!openOffer.isCanceled()) {
log.warn("Error reposting offer {}: {}", updatedOpenOffer.getId(), errorMessage); log.warn("Error republishing offer {}: {}", openOffer.getId(), errorMessage);
doCancelOffer(updatedOpenOffer); openOffer.getOffer().setErrorMessage(errorMessage);
updatedOffer.setErrorMessage(errorMessage);
// cancel offer if invalid
if (openOffer.getOffer().getState() == Offer.State.INVALID) {
log.warn("Canceling offer because it's invalid: {}", openOffer.getId());
doCancelOffer(openOffer);
}
} }
latch.countDown(); latch.countDown();
if (completeHandler != null) completeHandler.run(); if (completeHandler != null) completeHandler.run();

View File

@ -66,7 +66,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
synchronized (XmrWalletService.WALLET_LOCK) { synchronized (XmrWalletService.WALLET_LOCK) {
// reset protocol timeout // reset protocol timeout
verifyScheduled(); verifyPending();
model.getProtocol().startTimeoutTimer(); model.getProtocol().startTimeoutTimer();
// collect relevant info // collect relevant info
@ -94,7 +94,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
} }
// verify still open // verify still open
verifyScheduled(); verifyPending();
if (reserveTx != null) break; if (reserveTx != null) break;
} }
} }
@ -104,6 +104,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
model.getXmrWalletService().resetAddressEntriesForOpenOffer(offer.getId()); model.getXmrWalletService().resetAddressEntriesForOpenOffer(offer.getId());
if (reserveTx != null) { if (reserveTx != null) {
model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx)); model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
offer.getOfferPayload().setReserveTxKeyImages(null);
} }
throw e; throw e;
@ -131,7 +132,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
} }
} }
public void verifyScheduled() { public void verifyPending() {
if (!model.getOpenOffer().isScheduled()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled"); if (!model.getOpenOffer().isPending()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled");
} }
} }

View File

@ -116,9 +116,10 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
model.getOpenOffer().getOffer().getOfferPayload().setArbitratorSigner(arbitratorNodeAddress); model.getOpenOffer().getOffer().getOfferPayload().setArbitratorSigner(arbitratorNodeAddress);
model.getOpenOffer().getOffer().setState(Offer.State.OFFER_FEE_RESERVED); model.getOpenOffer().getOffer().setState(Offer.State.OFFER_FEE_RESERVED);
resultHandler.handleResult(); resultHandler.handleResult();
} else { } else {
errorMessageHandler.handleErrorMessage("Arbitrator nacked SignOfferRequest for offer " + request.getOfferId() + ": " + ackMessage.getErrorMessage()); model.getOpenOffer().getOffer().setState(Offer.State.INVALID);
} errorMessageHandler.handleErrorMessage("Arbitrator nacked SignOfferRequest for offer " + request.getOfferId() + ": " + ackMessage.getErrorMessage());
}
} }
}; };
model.getP2PService().addDecryptedDirectMessageListener(ackListener); model.getP2PService().addDecryptedDirectMessageListener(ackListener);
@ -137,9 +138,9 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
log.warn("Arbitrator unavailable: address={}, error={}", arbitratorNodeAddress, errorMessage); log.warn("Arbitrator unavailable: address={}, error={}", arbitratorNodeAddress, errorMessage);
excludedArbitrators.add(arbitratorNodeAddress); excludedArbitrators.add(arbitratorNodeAddress);
// check if offer still scheduled // check if offer still pending
if (!model.getOpenOffer().isScheduled()) { if (!model.getOpenOffer().isPending()) {
errorMessageHandler.handleErrorMessage("Offer is no longer scheduled, offerId=" + model.getOpenOffer().getId()); errorMessageHandler.handleErrorMessage("Offer is no longer pending, offerId=" + model.getOpenOffer().getId());
return; return;
} }

View File

@ -296,7 +296,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
private void updateSelectToggleButtonState() { private void updateSelectToggleButtonState() {
List<OpenOfferListItem> availableItems = sortedList.stream() List<OpenOfferListItem> availableItems = sortedList.stream()
.filter(openOfferListItem -> !openOfferListItem.getOpenOffer().isScheduled()) .filter(openOfferListItem -> !openOfferListItem.getOpenOffer().isPending())
.collect(Collectors.toList()); .collect(Collectors.toList());
if (availableItems.size() == 0) { if (availableItems.size() == 0) {
selectToggleButton.setDisable(true); selectToggleButton.setDisable(true);
@ -710,7 +710,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
offerStateChangeListeners.put(openOffer.getId(), listener); offerStateChangeListeners.put(openOffer.getId(), listener);
openOffer.stateProperty().addListener(listener); openOffer.stateProperty().addListener(listener);
if (openOffer.getState() == OpenOffer.State.SCHEDULED) { if (openOffer.getState() == OpenOffer.State.PENDING) {
setGraphic(new AutoTooltipLabel(Res.get("shared.pending"))); setGraphic(new AutoTooltipLabel(Res.get("shared.pending")));
return; return;
} }

View File

@ -1393,7 +1393,7 @@ message SignedOffer {
message OpenOffer { message OpenOffer {
enum State { enum State {
PB_ERROR = 0; PB_ERROR = 0;
SCHEDULED = 1; PENDING = 1;
AVAILABLE = 2; AVAILABLE = 2;
RESERVED = 3; RESERVED = 3;
CLOSED = 4; CLOSED = 4;