fixes from congestion testing

- refactor main wallet polling
- restart main wallet if connection changes before initial sync
- use cached wallet state throughout app
- avoid rescanning spent outputs until payout tx expected
- allow payment sent/received buttons to be clicked until arrived
- apply timeout to payment sent/received buttons
- load DepositView asynchronously
- remove separate timeout from OpenOffer
- tolerate error importing multisig hex until necessary
This commit is contained in:
woodser 2024-04-18 14:02:22 -04:00
parent 9cbf042da2
commit ca2d7704ab
22 changed files with 802 additions and 680 deletions

View file

@ -607,7 +607,7 @@ public final class XmrConnectionService {
long targetHeight = lastInfo.getTargetHeight();
long blocksLeft = targetHeight - lastInfo.getHeight();
if (syncStartHeight == null) syncStartHeight = lastInfo.getHeight();
double percent = targetHeight == syncStartHeight ? 1.0 : ((double) Math.max(1, lastInfo.getHeight() - syncStartHeight) / (double) (targetHeight - syncStartHeight)) * 100d; // grant at least 1 block to show progress
double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) Math.max(1, lastInfo.getHeight() - syncStartHeight) / (double) (targetHeight - syncStartHeight))); // grant at least 1 block to show progress
downloadListener.progress(percent, blocksLeft, null);
}

View file

@ -116,18 +116,16 @@ public class OfferBookService {
@Override
public void onAdded(Collection<ProtectedStorageEntry> protectedStorageEntries) {
protectedStorageEntries.forEach(protectedStorageEntry -> {
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
maybeInitializeKeyImagePoller();
keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
listener.onAdded(offer);
}
});
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
maybeInitializeKeyImagePoller();
keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onAdded(offer));
}
}
});
}
@ -135,18 +133,16 @@ public class OfferBookService {
@Override
public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) {
protectedStorageEntries.forEach(protectedStorageEntry -> {
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> {
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
maybeInitializeKeyImagePoller();
keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
listener.onRemoved(offer);
}
});
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
maybeInitializeKeyImagePoller();
keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages());
Offer offer = new Offer(offerPayload);
offer.setPriceFeedService(priceFeedService);
setReservedFundsSpent(offer);
synchronized (offerBookChangedListeners) {
offerBookChangedListeners.forEach(listener -> listener.onRemoved(offer));
}
}
});
}

View file

@ -34,8 +34,6 @@
package haveno.core.offer;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.proto.ProtoUtil;
import haveno.core.trade.Tradable;
import javafx.beans.property.ObjectProperty;
@ -55,9 +53,6 @@ import java.util.Optional;
@EqualsAndHashCode
@Slf4j
public final class OpenOffer implements Tradable {
// Timeout for offer reservation during takeoffer process. If deposit tx is not completed in that time we reset the offer to AVAILABLE state.
private static final long TIMEOUT = 60;
transient private Timer timeoutTimer;
public enum State {
SCHEDULED,
@ -227,13 +222,6 @@ public final class OpenOffer implements Tradable {
public void setState(State state) {
this.state = state;
stateProperty.set(state);
// We keep it reserved for a limited time, if trade preparation fails we revert to available state
if (this.state == State.RESERVED) { // TODO (woodser): remove this?
startTimeout();
} else {
stopTimeout();
}
}
public ReadOnlyObjectProperty<State> stateProperty() {
@ -252,26 +240,6 @@ public final class OpenOffer implements Tradable {
return state == State.DEACTIVATED;
}
private void startTimeout() {
stopTimeout();
timeoutTimer = UserThread.runAfter(() -> {
log.debug("Timeout for resetting State.RESERVED reached");
if (state == State.RESERVED) {
// we do not need to persist that as at startup any RESERVED state would be reset to AVAILABLE anyway
setState(State.AVAILABLE);
}
}, TIMEOUT);
}
private void stopTimeout() {
if (timeoutTimer != null) {
timeoutTimer.stop();
timeoutTimer = null;
}
}
@Override
public String toString() {
return "OpenOffer{" +

View file

@ -971,8 +971,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// return if awaiting scheduled tx
if (openOffer.getScheduledTxHashes() != null) return null;
// cache all transactions including from pool
List<MoneroTxWallet> allTxs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery().setIncludeOutputs(true));
// get all transactions including from pool
List<MoneroTxWallet> allTxs = xmrWalletService.getTransactions(false);
if (preferredSubaddressIndex != null) {

View file

@ -766,6 +766,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
trade.getSelf().getUpdatedMultisigHex(),
receiver.getUnsignedPayoutTxHex(), // include dispute payout tx if arbitrator has their updated multisig info
deferPublishPayout); // instruct trader to defer publishing payout tx because peer is expected to publish imminently
receiverPeer.setDisputeClosedMessage(disputeClosedMessage);
// send dispute closed message
log.info("Send {} to trader {}. tradeId={}, {}.uid={}, chatMessage.uid={}",

View file

@ -413,7 +413,7 @@ public abstract class Trade implements Tradable, Model {
transient private Subscription tradeStateSubscription;
transient private Subscription tradePhaseSubscription;
transient private Subscription payoutStateSubscription;
transient private TaskLooper txPollLooper;
transient private TaskLooper pollLooper;
transient private Long walletRefreshPeriodMs;
transient private Long syncNormalStartTimeMs;
@ -890,6 +890,10 @@ public abstract class Trade implements Tradable, Model {
public void saveWallet() {
synchronized (walletLock) {
if (!walletExists()) {
log.warn("Cannot save wallet for {} {} because it does not exist", getClass().getSimpleName(), getId());
return;
}
if (wallet == null) throw new RuntimeException("Trade wallet is not open for trade " + getId());
xmrWalletService.saveWallet(wallet);
maybeBackupWallet();
@ -1195,7 +1199,7 @@ public abstract class Trade implements Tradable, Model {
return trader.getDepositTx();
} catch (MoneroError e) {
log.error("Error getting {} deposit tx {}: {}", getPeerRole(trader), depositId, e.getMessage()); // TODO: peer.getRole()
return null;
throw e;
}
}
@ -1264,9 +1268,12 @@ public abstract class Trade implements Tradable, Model {
// TODO: clear other process data
setPayoutTxHex(null);
for (TradePeer peer : getPeers()) {
for (TradePeer peer : getAllTradeParties()) {
peer.setUnsignedPayoutTxHex(null);
peer.setUpdatedMultisigHex(null);
peer.setDisputeClosedMessage(null);
peer.setPaymentSentMessage(null);
peer.setPaymentReceivedMessage(null);
}
}
@ -1597,11 +1604,16 @@ public abstract class Trade implements Tradable, Model {
}
private List<TradePeer> getPeers() {
List<TradePeer> peers = getAllTradeParties();
if (!peers.remove(getSelf())) throw new IllegalStateException("Failed to remove self from list of peers");
return peers;
}
private List<TradePeer> getAllTradeParties() {
List<TradePeer> peers = new ArrayList<TradePeer>();
peers.add(getMaker());
peers.add(getTaker());
peers.add(getArbitrator());
if (!peers.remove(getSelf())) throw new IllegalStateException("Failed to remove self from list of peers");
return peers;
}
@ -1801,6 +1813,12 @@ public abstract class Trade implements Tradable, Model {
return (isSeller() ? getBuyer() : getSeller()).getPaymentReceivedMessage() != null; // seller stores message to buyer and arbitrator, peers store message from seller
}
public boolean hasDisputeClosedMessage() {
// arbitrator stores message to buyer and seller, peers store message from arbitrator
return isArbitrator() ? getBuyer().getDisputeClosedMessage() != null || getSeller().getDisputeClosedMessage() != null : getArbitrator().getDisputeClosedMessage() != null;
}
public boolean isPaymentReceived() {
return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal();
}
@ -1883,7 +1901,7 @@ public abstract class Trade implements Tradable, Model {
public BigInteger getFrozenAmount() {
BigInteger sum = BigInteger.ZERO;
for (String keyImage : getSelf().getReserveTxKeyImages()) {
List<MoneroOutputWallet> outputs = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage))); // TODO: will this check tx pool? avoid
List<MoneroOutputWallet> outputs = xmrWalletService.getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage)));
if (!outputs.isEmpty()) sum = sum.add(outputs.get(0).getAmount());
}
return sum;
@ -2077,23 +2095,23 @@ public abstract class Trade implements Tradable, Model {
synchronized (walletLock) {
if (isShutDownStarted || isPollInProgress()) return;
log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId());
txPollLooper = new TaskLooper(() -> pollWallet());
txPollLooper.start(walletRefreshPeriodMs);
pollLooper = new TaskLooper(() -> pollWallet());
pollLooper.start(walletRefreshPeriodMs);
}
}
private void stopPolling() {
synchronized (walletLock) {
if (isPollInProgress()) {
txPollLooper.stop();
txPollLooper = null;
pollLooper.stop();
pollLooper = null;
}
}
}
private boolean isPollInProgress() {
synchronized (walletLock) {
return txPollLooper != null;
return pollLooper != null;
}
}
@ -2117,8 +2135,14 @@ public abstract class Trade implements Tradable, Model {
// skip if payout unlocked
if (isPayoutUnlocked()) return;
// rescan spent outputs to detect payout tx after deposits unlocked
if (isDepositsUnlocked() && !isPayoutPublished()) wallet.rescanSpent();
// rescan spent outputs to detect unconfirmed payout tx after payment received message
if (!isPayoutPublished() && (hasPaymentReceivedMessage() || hasDisputeClosedMessage())) {
try {
wallet.rescanSpent();
} catch (Exception e) {
log.warn("Error rescanning spent outputs to detect payout tx for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage());
}
}
// get txs from trade wallet
boolean payoutExpected = isPaymentReceived() || getSeller().getPaymentReceivedMessage() != null || disputeState.ordinal() >= DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal() || getArbitrator().getDisputeClosedMessage() != null;
@ -2129,7 +2153,7 @@ public abstract class Trade implements Tradable, Model {
// warn on double spend // TODO: other handling?
for (MoneroTxWallet tx : txs) {
if (Boolean.TRUE.equals(tx.isDoubleSpendSeen())) log.warn("Double spend seen for tx {} for {} {}", tx.getHash(), getClass().getSimpleName(), getId());
if (Boolean.TRUE.equals(tx.isDoubleSpendSeen())) log.warn("Double spend seen for tx {} for {} {}", tx.getHash(), getClass().getSimpleName(), getShortId());
}
// check deposit txs
@ -2189,9 +2213,8 @@ public abstract class Trade implements Tradable, Model {
if (isConnectionRefused) forceRestartTradeWallet();
else {
boolean isWalletConnected = isWalletConnectedToDaemon();
if (!isWalletConnected) xmrConnectionService.checkConnection(); // check connection if wallet is not connected
if (!isShutDownStarted && wallet != null && isWalletConnected) {
log.warn("Error polling trade wallet for {} {}: {}. Monerod={}", getClass().getSimpleName(), getId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection());
log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection());
//e.printStackTrace();
}
}

View file

@ -1287,7 +1287,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
private void removeTradeOnError(Trade trade) {
log.warn("TradeManager.removeTradeOnError() tradeId={}, state={}", trade.getId(), trade.getState());
log.warn("TradeManager.removeTradeOnError() trade={}, tradeId={}, state={}", trade.getClass().getSimpleName(), trade.getShortId(), trade.getState());
synchronized (tradableList) {
// unreserve taker key images

View file

@ -134,14 +134,16 @@ public class BuyerProtocol extends DisputeProtocol {
BuyerSendPaymentSentMessageToArbitrator.class)
.using(new TradeTaskRunner(trade,
() -> {
stopTimeout();
this.errorMessageHandler = null;
resultHandler.handleResult();
handleTaskRunnerSuccess(event);
},
(errorMessage) -> {
handleTaskRunnerFault(event, errorMessage);
})))
.run(() -> trade.setState(Trade.State.BUYER_CONFIRMED_PAYMENT_SENT))
}))
.withTimeout(TradeProtocol.TRADE_TIMEOUT_SECONDS))
.run(() -> trade.advanceState(Trade.State.BUYER_CONFIRMED_PAYMENT_SENT))
.executeTasks(true);
} catch (Exception e) {
errorMessageHandler.handleErrorMessage("Error confirming payment sent: " + e.getMessage());

View file

@ -131,13 +131,15 @@ public class SellerProtocol extends DisputeProtocol {
SellerSendPaymentReceivedMessageToBuyer.class,
SellerSendPaymentReceivedMessageToArbitrator.class)
.using(new TradeTaskRunner(trade, () -> {
stopTimeout();
this.errorMessageHandler = null;
handleTaskRunnerSuccess(event);
resultHandler.handleResult();
}, (errorMessage) -> {
handleTaskRunnerFault(event, errorMessage);
})))
.run(() -> trade.setState(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT))
}))
.withTimeout(TradeProtocol.TRADE_TIMEOUT_SECONDS))
.run(() -> trade.advanceState(Trade.State.SELLER_CONFIRMED_PAYMENT_RECEIPT))
.executeTasks(true);
} catch (Exception e) {
errorMessageHandler.handleErrorMessage("Error confirming payment received: " + e.getMessage());

View file

@ -61,13 +61,17 @@ public class ProcessDepositsConfirmedMessage extends TradeTask {
log.info(trade.getClass().getSimpleName() + " decrypting using seller payment account key");
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
}
processModel.getTradeManager().requestPersistence(); // in case importing multisig hex fails
// import multisig hex
trade.importMultisigHex();
// persist and complete
// persist
processModel.getTradeManager().requestPersistence();
// try to import multisig hex (retry later)
try {
trade.importMultisigHex();
} catch (Exception e) {
e.printStackTrace();
}
complete();
} catch (Throwable t) {
failed(t);

View file

@ -95,7 +95,6 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
}
trade.requestPersistence();
// process payout tx unless already unlocked
if (!trade.isPayoutUnlocked()) processPayoutTx(message);

View file

@ -61,8 +61,12 @@ public class ProcessPaymentSentMessage extends TradeTask {
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
trade.requestPersistence();
// import multisig hex
trade.importMultisigHex();
// try to import multisig hex (retry later)
try {
trade.importMultisigHex();
} catch (Exception e) {
e.printStackTrace();
}
// update state
trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);

View file

@ -1,71 +0,0 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.core.trade.protocol.tasks;
import haveno.common.taskrunner.TaskRunner;
import haveno.core.trade.Trade;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SellerPublishDepositTx extends TradeTask {
public SellerPublishDepositTx(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
throw new RuntimeException("SellerPublishesDepositTx not implemented for xmr");
// final Transaction depositTx = processModel.getDepositTx();
// processModel.getTradeWalletService().broadcastTx(depositTx,
// new TxBroadcaster.Callback() {
// @Override
// public void onSuccess(Transaction transaction) {
// if (!completed) {
// // Now as we have published the deposit tx we set it in trade
// trade.applyDepositTx(depositTx);
//
// trade.setState(Trade.State.SELLER_PUBLISHED_DEPOSIT_TX);
//
// processModel.getBtcWalletService().swapAddressEntryToAvailable(processModel.getOffer().getId(),
// AddressEntry.Context.RESERVED_FOR_TRADE);
//
// processModel.getTradeManager().requestPersistence();
//
// complete();
// } else {
// log.warn("We got the onSuccess callback called after the timeout has been triggered a complete().");
// }
// }
//
// @Override
// public void onFailure(TxBroadcastException exception) {
// if (!completed) {
// failed(exception);
// } else {
// log.warn("We got the onFailure callback called after the timeout has been triggered a complete().");
// }
// }
// });
} catch (Throwable t) {
failed(t);
}
}
}

View file

@ -140,7 +140,7 @@ public class Balances {
// calculate reserved offer balance
reservedOfferBalance = BigInteger.ZERO;
if (xmrWalletService.getWallet() != null) {
List<MoneroOutputWallet> frozenOutputs = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false));
List<MoneroOutputWallet> frozenOutputs = xmrWalletService.getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false));
for (MoneroOutputWallet frozenOutput : frozenOutputs) reservedOfferBalance = reservedOfferBalance.add(frozenOutput.getAmount());
}
for (Trade trade : trades) {

View file

@ -11,7 +11,7 @@ public class DownloadListener {
private final DoubleProperty percentage = new SimpleDoubleProperty(-1);
public void progress(double percentage, long blocksLeft, Date date) {
UserThread.await(() -> this.percentage.set(percentage / 100d));
UserThread.await(() -> this.percentage.set(percentage));
}
public void doneDownload() {