Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1358,7 +1358,7 @@ private MoneroTxWallet splitAndSchedule(OpenOffer openOffer) {
} catch (Exception e) {
if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds
log.warn("Error creating split output tx to fund offer, offerId={}, subaddress={}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
xmrWalletService.handleWalletError(e, sourceConnection);
xmrWalletService.handleWalletError(e, sourceConnection, i + 1);
if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ protected void run() {
throw e;
} catch (Exception e) {
log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", openOffer.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
model.getXmrWalletService().handleWalletError(e, sourceConnection);
model.getXmrWalletService().handleWalletError(e, sourceConnection, i + 1);
verifyPending();
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
model.getProtocol().startTimeoutTimer(); // reset protocol timeout
Expand Down
112 changes: 58 additions & 54 deletions core/src/main/java/haveno/core/support/dispute/DisputeManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -497,11 +497,11 @@ protected void handleDisputeOpenedMessage(DisputeOpenedMessage message) {
// get trade
Trade trade = tradeManager.getTrade(msgDispute.getTradeId());
if (trade == null) {
log.warn("Dispute trade {} does not exist", msgDispute.getTradeId());
log.warn("Ignoring DisputeOpenedMessage for trade {} because it does not exist", msgDispute.getTradeId());
return;
}
if (trade.isPayoutPublished()) {
log.warn("Dispute trade {} payout already published", msgDispute.getTradeId());
log.warn("Ignoring DisputeOpenedMessage for {} {} because payout is already published", trade.getClass().getSimpleName(), trade.getId());
return;
}

Expand Down Expand Up @@ -934,66 +934,70 @@ public MoneroTxWallet createDisputePayoutTx(Trade trade, Contract contract, Disp
// sync and poll
trade.syncAndPollWallet();

// create unsigned dispute payout tx if not already published
if (!trade.isPayoutPublished()) {
// check if payout tx already published
String alreadyPublishedMsg = "Cannot create dispute payout tx because payout tx is already published for trade " + trade.getId();
if (trade.isPayoutPublished()) throw new RuntimeException(alreadyPublishedMsg);

// create unsigned dispute payout tx
if (updateState) log.info("Creating unsigned dispute payout tx for trade {}", trade.getId());
try {
// create unsigned dispute payout tx
if (updateState) log.info("Creating unsigned dispute payout tx for trade {}", trade.getId());
try {

// trade wallet must be synced
if (trade.getWallet().isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx for trade " + trade.getId());
// trade wallet must be synced
if (trade.getWallet().isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx for trade " + trade.getId());

// check amounts
if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Buyer payout cannot be negative");
if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Seller payout cannot be negative");
if (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost()).compareTo(trade.getWallet().getUnlockedBalance()) > 0) {
throw new RuntimeException("The payout amounts are more than the wallet's unlocked balance, unlocked balance=" + trade.getWallet().getUnlockedBalance() + " vs " + disputeResult.getBuyerPayoutAmountBeforeCost() + " + " + disputeResult.getSellerPayoutAmountBeforeCost() + " = " + (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost())));
}
// check amounts
if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Buyer payout cannot be negative");
if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) < 0) throw new RuntimeException("Seller payout cannot be negative");
if (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost()).compareTo(trade.getWallet().getUnlockedBalance()) > 0) {
throw new RuntimeException("The payout amounts are more than the wallet's unlocked balance, unlocked balance=" + trade.getWallet().getUnlockedBalance() + " vs " + disputeResult.getBuyerPayoutAmountBeforeCost() + " + " + disputeResult.getSellerPayoutAmountBeforeCost() + " = " + (disputeResult.getBuyerPayoutAmountBeforeCost().add(disputeResult.getSellerPayoutAmountBeforeCost())));
}

// create dispute payout tx config
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0);
String buyerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString();
String sellerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString();
txConfig.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY);
if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(buyerPayoutAddress, disputeResult.getBuyerPayoutAmountBeforeCost());
if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(sellerPayoutAddress, disputeResult.getSellerPayoutAmountBeforeCost());

// configure who pays mining fee
BigInteger loserPayoutAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmountBeforeCost() : disputeResult.getBuyerPayoutAmountBeforeCost();
if (loserPayoutAmount.equals(BigInteger.ZERO)) txConfig.setSubtractFeeFrom(0); // winner pays fee if loser gets 0
else {
switch (disputeResult.getSubtractFeeFrom()) {
case BUYER_AND_SELLER:
txConfig.setSubtractFeeFrom(0, 1);
break;
case BUYER_ONLY:
txConfig.setSubtractFeeFrom(0);
break;
case SELLER_ONLY:
txConfig.setSubtractFeeFrom(1);
break;
}
// create dispute payout tx config
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0);
String buyerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString();
String sellerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString();
txConfig.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY);
if (disputeResult.getBuyerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(buyerPayoutAddress, disputeResult.getBuyerPayoutAmountBeforeCost());
if (disputeResult.getSellerPayoutAmountBeforeCost().compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(sellerPayoutAddress, disputeResult.getSellerPayoutAmountBeforeCost());

// configure who pays mining fee
BigInteger loserPayoutAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmountBeforeCost() : disputeResult.getBuyerPayoutAmountBeforeCost();
if (loserPayoutAmount.equals(BigInteger.ZERO)) txConfig.setSubtractFeeFrom(0); // winner pays fee if loser gets 0
else {
switch (disputeResult.getSubtractFeeFrom()) {
case BUYER_AND_SELLER:
txConfig.setSubtractFeeFrom(0, 1);
break;
case BUYER_ONLY:
txConfig.setSubtractFeeFrom(0);
break;
case SELLER_ONLY:
txConfig.setSubtractFeeFrom(1);
break;
}
}

// create dispute payout tx
MoneroTxWallet payoutTx = trade.createDisputePayoutTx(txConfig);

// update trade state
if (updateState) {
trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
trade.updatePayout(payoutTx);
if (trade.getBuyer().getUpdatedMultisigHex() != null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
if (trade.getSeller().getUpdatedMultisigHex() != null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
}
trade.requestPersistence();
return payoutTx;
} catch (Exception e) {
trade.syncAndPollWallet();
if (!trade.isPayoutPublished()) throw e;
// create dispute payout tx
MoneroTxWallet payoutTx = trade.createDisputePayoutTx(txConfig);

// update trade state
if (updateState) {
trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
trade.updatePayout(payoutTx);
if (trade.getBuyer().getUpdatedMultisigHex() != null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
if (trade.getSeller().getUpdatedMultisigHex() != null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
}
trade.requestPersistence();
return payoutTx;
} catch (Exception e) {
trade.syncAndPollWallet();
if (trade.isPayoutPublished()) throw new IllegalStateException(alreadyPublishedMsg);
throw e;
} catch (AssertionError e) { // tx creation throws assertion error with invalid config
trade.syncAndPollWallet();
if (trade.isPayoutPublished()) throw new IllegalStateException(alreadyPublishedMsg);
throw new RuntimeException(e);
}
return null; // can be null if already published or we don't have receiver's multisig hex
}

private Tuple2<NodeAddress, PubKeyRing> getNodeAddressPubKeyRingTuple(Dispute dispute) {
Expand Down
18 changes: 10 additions & 8 deletions core/src/main/java/haveno/core/trade/Trade.java
Original file line number Diff line number Diff line change
Expand Up @@ -1152,7 +1152,7 @@ public void importMultisigHex() {
throw e;
} catch (Exception e) {
log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
handleWalletError(e, sourceConnection);
handleWalletError(e, sourceConnection, i + 1);
doPollWallet();
if (isPayoutPublished()) break;
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
Expand Down Expand Up @@ -1237,9 +1237,9 @@ private void doImportMultisigHex() {
log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size());
}

private void handleWalletError(Exception e, MoneroRpcConnection sourceConnection) {
private void handleWalletError(Exception e, MoneroRpcConnection sourceConnection, int numAttempts) {
if (HavenoUtils.isUnresponsive(e)) forceCloseWallet(); // wallet can be stuck a while
if (!HavenoUtils.isIllegal(e) && xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection);
if (numAttempts % TradeProtocol.REQUEST_CONNECTION_SWITCH_EVERY_NUM_ATTEMPTS == 0 && !HavenoUtils.isIllegal(e) && xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection); // request connection switch every n attempts
getWallet(); // re-open wallet
}

Expand Down Expand Up @@ -1275,7 +1275,7 @@ public MoneroTxWallet createPayoutTx() {
} catch (IllegalArgumentException | IllegalStateException e) {
throw e;
} catch (Exception e) {
handleWalletError(e, sourceConnection);
handleWalletError(e, sourceConnection, i + 1);
doPollWallet();
if (isPayoutPublished()) break;
log.warn("Failed to create payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
Expand Down Expand Up @@ -1337,7 +1337,7 @@ public MoneroTxWallet createDisputePayoutTx(MoneroTxConfig txConfig) {
throw e;
} catch (Exception e) {
if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee");
handleWalletError(e, sourceConnection);
handleWalletError(e, sourceConnection, i + 1);
doPollWallet();
if (isPayoutPublished()) break;
log.warn("Failed to create dispute payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
Expand Down Expand Up @@ -1368,7 +1368,7 @@ public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) {
} catch (IllegalArgumentException | IllegalStateException e) {
throw e;
} catch (Exception e) {
handleWalletError(e, sourceConnection);
handleWalletError(e, sourceConnection, i + 1);
doPollWallet();
if (isPayoutPublished()) break;
log.warn("Failed to process payout tx, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage(), e);
Expand Down Expand Up @@ -2581,7 +2581,7 @@ private void syncWallet(boolean pollWallet) {
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
try {
synchronized (walletLock) {
if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId());
if (getWallet() == null) throw new IllegalStateException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId());
if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId());
if (isWalletBehind()) {
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getShortId());
Expand All @@ -2601,7 +2601,9 @@ private void syncWallet(boolean pollWallet) {

if (pollWallet) doPollWallet();
} catch (Exception e) {
if (!isShutDownStarted) ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId());
if (!(e instanceof IllegalStateException) && !isShutDownStarted) {
ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId());
}
throw e;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
public static final int TRADE_STEP_TIMEOUT_SECONDS = Config.baseCurrencyNetwork().isTestnet() ? 60 : 180;
private static final String TIMEOUT_REACHED = "Timeout reached.";
public static final int MAX_ATTEMPTS = 5; // max attempts to create txs and other wallet functions
public static final int REQUEST_CONNECTION_SWITCH_EVERY_NUM_ATTEMPTS = 2; // request connection switch on even attempts
public static final long REPROCESS_DELAY_MS = 5000;
public static final String LOG_HIGHLIGHT = ""; // TODO: how to highlight some logs with cyan? ("\u001B[36m")? coloring works in the terminal but prints character literals to .log files

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ protected void run() {
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);
trade.getXmrWalletService().handleWalletError(e, sourceConnection, i + 1);
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ protected void run() {
depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
} catch (Exception e) {
log.warn("Error creating deposit tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
trade.getXmrWalletService().handleWalletError(e, sourceConnection, i + 1);
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ protected void run() {
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);
trade.getXmrWalletService().handleWalletError(e, sourceConnection, i + 1);
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
Expand Down
Loading
Loading