refactoring based on congestion testing

retry creating and processing trade txs on failure
do not use connection manager polling to reduce requests
use global daemon lock for wallet sync operations
sync wallets on poll if behind
use local util to get payment uri to avoid blocking
all peers share multisig hex on deposits confirmed
import multisig hex when needed
This commit is contained in:
woodser 2024-04-29 07:02:05 -04:00
parent f519ac12a5
commit e63141279c
36 changed files with 799 additions and 568 deletions

View File

@ -199,15 +199,15 @@ public class CoreApi {
// Monero Connections // Monero Connections
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void addMoneroConnection(MoneroRpcConnection connection) { public void addXmrConnection(MoneroRpcConnection connection) {
xmrConnectionService.addConnection(connection); xmrConnectionService.addConnection(connection);
} }
public void removeMoneroConnection(String connectionUri) { public void removeXmrConnection(String connectionUri) {
xmrConnectionService.removeConnection(connectionUri); xmrConnectionService.removeConnection(connectionUri);
} }
public MoneroRpcConnection getMoneroConnection() { public MoneroRpcConnection getXmrConnection() {
return xmrConnectionService.getConnection(); return xmrConnectionService.getConnection();
} }
@ -215,15 +215,15 @@ public class CoreApi {
return xmrConnectionService.getConnections(); return xmrConnectionService.getConnections();
} }
public void setMoneroConnection(String connectionUri) { public void setXmrConnection(String connectionUri) {
xmrConnectionService.setConnection(connectionUri); xmrConnectionService.setConnection(connectionUri);
} }
public void setMoneroConnection(MoneroRpcConnection connection) { public void setXmrConnection(MoneroRpcConnection connection) {
xmrConnectionService.setConnection(connection); xmrConnectionService.setConnection(connection);
} }
public MoneroRpcConnection checkMoneroConnection() { public MoneroRpcConnection checkXmrConnection() {
return xmrConnectionService.checkConnection(); return xmrConnectionService.checkConnection();
} }
@ -231,19 +231,19 @@ public class CoreApi {
return xmrConnectionService.checkConnections(); return xmrConnectionService.checkConnections();
} }
public void startCheckingMoneroConnection(Long refreshPeriod) { public void startCheckingXmrConnection(Long refreshPeriod) {
xmrConnectionService.startCheckingConnection(refreshPeriod); xmrConnectionService.startCheckingConnection(refreshPeriod);
} }
public void stopCheckingMoneroConnection() { public void stopCheckingXmrConnection() {
xmrConnectionService.stopCheckingConnection(); xmrConnectionService.stopCheckingConnection();
} }
public MoneroRpcConnection getBestAvailableMoneroConnection() { public MoneroRpcConnection getBestAvailableXmrConnection() {
return xmrConnectionService.getBestAvailableConnection(); return xmrConnectionService.getBestAvailableConnection();
} }
public void setMoneroConnectionAutoSwitch(boolean autoSwitch) { public void setXmrConnectionAutoSwitch(boolean autoSwitch) {
xmrConnectionService.setAutoSwitch(autoSwitch); xmrConnectionService.setAutoSwitch(autoSwitch);
} }

View File

@ -90,6 +90,7 @@ public final class XmrConnectionService {
private boolean isInitialized; private boolean isInitialized;
private boolean pollInProgress; private boolean pollInProgress;
private MoneroDaemonRpc daemon; private MoneroDaemonRpc daemon;
private Boolean isConnected = false;
@Getter @Getter
private MoneroDaemonInfo lastInfo; private MoneroDaemonInfo lastInfo;
private Long syncStartHeight = null; private Long syncStartHeight = null;
@ -148,7 +149,6 @@ public final class XmrConnectionService {
isInitialized = false; isInitialized = false;
synchronized (lock) { synchronized (lock) {
if (daemonPollLooper != null) daemonPollLooper.stop(); if (daemonPollLooper != null) daemonPollLooper.stop();
connectionManager.stopPolling();
daemon = null; daemon = null;
} }
} }
@ -171,7 +171,7 @@ public final class XmrConnectionService {
} }
public Boolean isConnected() { public Boolean isConnected() {
return connectionManager.isConnected(); return isConnected;
} }
public void addConnection(MoneroRpcConnection connection) { public void addConnection(MoneroRpcConnection connection) {
@ -196,6 +196,12 @@ public final class XmrConnectionService {
return connectionManager.getConnections(); return connectionManager.getConnections();
} }
public void switchToBestConnection() {
if (isFixedConnection() || !connectionManager.getAutoSwitch()) return;
MoneroRpcConnection bestConnection = getBestAvailableConnection();
if (bestConnection != null) setConnection(bestConnection);
}
public void setConnection(String connectionUri) { public void setConnection(String connectionUri) {
accountService.checkAccountOpen(); accountService.checkAccountOpen();
connectionManager.setConnection(connectionUri); // listener will update connection list connectionManager.setConnection(connectionUri); // listener will update connection list
@ -226,8 +232,8 @@ public final class XmrConnectionService {
public void stopCheckingConnection() { public void stopCheckingConnection() {
accountService.checkAccountOpen(); accountService.checkAccountOpen();
connectionManager.stopPolling();
connectionList.setRefreshPeriod(-1L); connectionList.setRefreshPeriod(-1L);
updatePolling();
} }
public MoneroRpcConnection getBestAvailableConnection() { public MoneroRpcConnection getBestAvailableConnection() {
@ -472,8 +478,6 @@ public final class XmrConnectionService {
if (!isFixedConnection() && (connectionManager.getConnection() == null || connectionManager.getAutoSwitch())) { if (!isFixedConnection() && (connectionManager.getConnection() == null || connectionManager.getAutoSwitch())) {
MoneroRpcConnection bestConnection = getBestAvailableConnection(); MoneroRpcConnection bestConnection = getBestAvailableConnection();
if (bestConnection != null) setConnection(bestConnection); if (bestConnection != null) setConnection(bestConnection);
} else {
checkConnection();
} }
} else if (!isInitialized) { } else if (!isInitialized) {
@ -485,19 +489,11 @@ public final class XmrConnectionService {
// start local node if applicable // start local node if applicable
maybeStartLocalNode(); maybeStartLocalNode();
// update connection
checkConnection();
} }
// register connection listener // register connection listener
connectionManager.addListener(this::onConnectionChanged); connectionManager.addListener(this::onConnectionChanged);
// start polling after delay
UserThread.runAfter(() -> {
if (!isShutDownStarted) connectionManager.startPolling(getRefreshPeriodMs() * 2);
}, getDefaultRefreshPeriodMs() * 2 / 1000);
isInitialized = true; isInitialized = true;
} }
@ -524,7 +520,6 @@ public final class XmrConnectionService {
private void onConnectionChanged(MoneroRpcConnection currentConnection) { private void onConnectionChanged(MoneroRpcConnection currentConnection) {
if (isShutDownStarted) return; if (isShutDownStarted) return;
log.info("XmrConnectionService.onConnectionChanged() uri={}, connected={}", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : currentConnection.isConnected());
if (currentConnection == null) { if (currentConnection == null) {
log.warn("Setting daemon connection to null"); log.warn("Setting daemon connection to null");
Thread.dumpStack(); Thread.dumpStack();
@ -532,9 +527,11 @@ public final class XmrConnectionService {
synchronized (lock) { synchronized (lock) {
if (currentConnection == null) { if (currentConnection == null) {
daemon = null; daemon = null;
isConnected = false;
connectionList.setCurrentConnectionUri(null); connectionList.setCurrentConnectionUri(null);
} else { } else {
daemon = new MoneroDaemonRpc(currentConnection); daemon = new MoneroDaemonRpc(currentConnection);
isConnected = currentConnection.isConnected();
connectionList.removeConnection(currentConnection.getUri()); connectionList.removeConnection(currentConnection.getUri());
connectionList.addConnection(currentConnection); connectionList.addConnection(currentConnection);
connectionList.setCurrentConnectionUri(currentConnection.getUri()); connectionList.setCurrentConnectionUri(currentConnection.getUri());
@ -546,9 +543,13 @@ public final class XmrConnectionService {
numUpdates.set(numUpdates.get() + 1); numUpdates.set(numUpdates.get() + 1);
}); });
} }
updatePolling();
// update polling
doPollDaemon();
UserThread.runAfter(() -> updatePolling(), getRefreshPeriodMs() / 1000);
// notify listeners in parallel // notify listeners in parallel
log.info("XmrConnectionService.onConnectionChanged() uri={}, connected={}", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : isConnected);
synchronized (listenerLock) { synchronized (listenerLock) {
for (MoneroConnectionManagerListener listener : listeners) { for (MoneroConnectionManagerListener listener : listeners) {
ThreadUtils.submitToPool(() -> listener.onConnectionChanged(currentConnection)); ThreadUtils.submitToPool(() -> listener.onConnectionChanged(currentConnection));
@ -557,18 +558,14 @@ public final class XmrConnectionService {
} }
private void updatePolling() { private void updatePolling() {
new Thread(() -> {
synchronized (lock) {
stopPolling(); stopPolling();
if (connectionList.getRefreshPeriod() >= 0) startPolling(); // 0 means default refresh poll if (connectionList.getRefreshPeriod() >= 0) startPolling(); // 0 means default refresh poll
} }
}).start();
}
private void startPolling() { private void startPolling() {
synchronized (lock) { synchronized (lock) {
if (daemonPollLooper != null) daemonPollLooper.stop(); if (daemonPollLooper != null) daemonPollLooper.stop();
daemonPollLooper = new TaskLooper(() -> pollDaemonInfo()); daemonPollLooper = new TaskLooper(() -> pollDaemon());
daemonPollLooper.start(getRefreshPeriodMs()); daemonPollLooper.start(getRefreshPeriodMs());
} }
} }
@ -582,17 +579,34 @@ public final class XmrConnectionService {
} }
} }
private void pollDaemonInfo() { private void pollDaemon() {
if (pollInProgress) return; if (pollInProgress) return;
doPollDaemon();
}
private void doPollDaemon() {
synchronized (pollLock) { synchronized (pollLock) {
pollInProgress = true; pollInProgress = true;
if (isShutDownStarted) return; if (isShutDownStarted) return;
try { try {
// poll daemon // poll daemon
log.debug("Polling daemon info"); if (daemon == null) switchToBestConnection();
if (daemon == null) throw new RuntimeException("No daemon connection"); if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
try {
lastInfo = daemon.getInfo(); lastInfo = daemon.getInfo();
} catch (Exception e) {
try {
log.warn("Failed to fetch daemon info, trying to switch to best connection: " + e.getMessage());
switchToBestConnection();
lastInfo = daemon.getInfo();
} catch (Exception e2) {
throw e2; // caught internally
}
}
// connected to daemon
isConnected = true;
// update properties on user thread // update properties on user thread
UserThread.execute(() -> { UserThread.execute(() -> {
@ -632,19 +646,15 @@ public final class XmrConnectionService {
lastErrorTimestamp = null; lastErrorTimestamp = null;
} }
// update and notify connected state
if (!Boolean.TRUE.equals(connectionManager.isConnected())) {
connectionManager.checkConnection();
}
// clear error message // clear error message
if (Boolean.TRUE.equals(connectionManager.isConnected()) && HavenoUtils.havenoSetup != null) { if (HavenoUtils.havenoSetup != null) HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(null);
HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(null);
}
} catch (Exception e) { } catch (Exception e) {
// skip if shut down or connected // not connected to daemon
if (isShutDownStarted || Boolean.TRUE.equals(isConnected())) return; isConnected = false;
// skip if shut down
if (isShutDownStarted) return;
// log error message periodically // log error message periodically
if ((lastErrorTimestamp == null || System.currentTimeMillis() - lastErrorTimestamp > MIN_ERROR_LOG_PERIOD_MS)) { if ((lastErrorTimestamp == null || System.currentTimeMillis() - lastErrorTimestamp > MIN_ERROR_LOG_PERIOD_MS)) {
@ -653,20 +663,8 @@ public final class XmrConnectionService {
if (DevEnv.isDevMode()) e.printStackTrace(); if (DevEnv.isDevMode()) e.printStackTrace();
} }
new Thread(() -> {
if (isShutDownStarted) return;
if (!isFixedConnection() && connectionManager.getAutoSwitch()) {
MoneroRpcConnection bestConnection = getBestAvailableConnection();
if (bestConnection != null) connectionManager.setConnection(bestConnection);
} else {
connectionManager.checkConnection();
}
// set error message // set error message
if (!Boolean.TRUE.equals(connectionManager.isConnected()) && HavenoUtils.havenoSetup != null) { if (HavenoUtils.havenoSetup != null) HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(e.getMessage());
HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(e.getMessage());
}
}).start();
} finally { } finally {
pollInProgress = false; pollInProgress = false;
} }

View File

@ -36,7 +36,6 @@ package haveno.core.offer;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.name.Named; import com.google.inject.name.Named;
import common.utils.GenUtils;
import haveno.common.UserThread; import haveno.common.UserThread;
import haveno.common.config.Config; import haveno.common.config.Config;
import haveno.common.file.JsonFileManager; import haveno.common.file.JsonFileManager;
@ -46,6 +45,7 @@ import haveno.core.api.XmrConnectionService;
import haveno.core.filter.FilterManager; import haveno.core.filter.FilterManager;
import haveno.core.locale.Res; import haveno.core.locale.Res;
import haveno.core.provider.price.PriceFeedService; import haveno.core.provider.price.PriceFeedService;
import haveno.core.trade.HavenoUtils;
import haveno.core.util.JsonUtil; import haveno.core.util.JsonUtil;
import haveno.core.xmr.wallet.XmrKeyImageListener; import haveno.core.xmr.wallet.XmrKeyImageListener;
import haveno.core.xmr.wallet.XmrKeyImagePoller; import haveno.core.xmr.wallet.XmrKeyImagePoller;
@ -287,7 +287,7 @@ public class OfferBookService {
// first poll after 20s // first poll after 20s
// TODO: remove? // TODO: remove?
new Thread(() -> { new Thread(() -> {
GenUtils.waitFor(20000); HavenoUtils.waitFor(20000);
keyImagePoller.poll(); keyImagePoller.poll();
}).start(); }).start();
} }

View File

@ -36,7 +36,6 @@ package haveno.core.offer;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import com.google.inject.Inject; import com.google.inject.Inject;
import common.utils.GenUtils;
import haveno.common.ThreadUtils; import haveno.common.ThreadUtils;
import haveno.common.Timer; import haveno.common.Timer;
import haveno.common.UserThread; import haveno.common.UserThread;
@ -71,6 +70,7 @@ import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.trade.TradableList; import haveno.core.trade.TradableList;
import haveno.core.trade.handlers.TransactionResultHandler; import haveno.core.trade.handlers.TransactionResultHandler;
import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.trade.statistics.TradeStatisticsManager; import haveno.core.trade.statistics.TradeStatisticsManager;
import haveno.core.user.Preferences; import haveno.core.user.Preferences;
import haveno.core.user.User; import haveno.core.user.User;
@ -278,7 +278,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// first poll in 5s // first poll in 5s
// TODO: remove? // TODO: remove?
new Thread(() -> { new Thread(() -> {
GenUtils.waitFor(5000); HavenoUtils.waitFor(5000);
signedOfferKeyImagePoller.poll(); signedOfferKeyImagePoller.poll();
}).start(); }).start();
} }
@ -349,7 +349,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// For typical number of offers we are tolerant with delay to give enough time to broadcast. // For typical number of offers we are tolerant with delay to give enough time to broadcast.
// If number of offers is very high we limit to 3 sec. to not delay other shutdown routines. // If number of offers is very high we limit to 3 sec. to not delay other shutdown routines.
long delayMs = Math.min(3000, size * 200 + 500); long delayMs = Math.min(3000, size * 200 + 500);
GenUtils.waitFor(delayMs); HavenoUtils.waitFor(delayMs);
}, THREAD_ID); }, THREAD_ID);
} else { } else {
broadcaster.flush(); broadcaster.flush();
@ -705,9 +705,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// remove open offer which thaws its key images // remove open offer which thaws its key images
private void onCancelled(@NotNull OpenOffer openOffer) { private void onCancelled(@NotNull OpenOffer openOffer) {
Offer offer = openOffer.getOffer(); Offer offer = openOffer.getOffer();
if (offer.getOfferPayload().getReserveTxKeyImages() != null) {
xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages()); xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages());
}
offer.setState(Offer.State.REMOVED); offer.setState(Offer.State.REMOVED);
openOffer.setState(OpenOffer.State.CANCELED); openOffer.setState(OpenOffer.State.CANCELED);
removeOpenOffer(openOffer); removeOpenOffer(openOffer);
@ -1029,6 +1027,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// handle sufficient available balance to split output // handle sufficient available balance to split output
boolean sufficientAvailableBalance = xmrWalletService.getWallet().getUnlockedBalance(0).compareTo(offerReserveAmount) >= 0; boolean sufficientAvailableBalance = xmrWalletService.getWallet().getUnlockedBalance(0).compareTo(offerReserveAmount) >= 0;
if (sufficientAvailableBalance) { if (sufficientAvailableBalance) {
log.info("Splitting and scheduling outputs for offer {} at subaddress {}", openOffer.getShortId());
splitAndSchedule(openOffer); splitAndSchedule(openOffer);
} else if (openOffer.getScheduledTxHashes() == null) { } else if (openOffer.getScheduledTxHashes() == null) {
scheduleWithEarliestTxs(openOffers, openOffer); scheduleWithEarliestTxs(openOffers, openOffer);
@ -1038,16 +1037,28 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
private MoneroTxWallet splitAndSchedule(OpenOffer openOffer) { private MoneroTxWallet splitAndSchedule(OpenOffer openOffer) {
BigInteger reserveAmount = openOffer.getOffer().getReserveAmount(); BigInteger reserveAmount = openOffer.getOffer().getReserveAmount();
xmrWalletService.swapAddressEntryToAvailable(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output(s) xmrWalletService.swapAddressEntryToAvailable(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output(s)
MoneroTxWallet splitOutputTx = null;
synchronized (XmrWalletService.WALLET_LOCK) {
XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getId(), entry.getSubaddressIndex()); log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex());
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
MoneroTxWallet splitOutputTx = xmrWalletService.getWallet().createTx(new MoneroTxConfig() for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig()
.setAccountIndex(0) .setAccountIndex(0)
.setAddress(entry.getAddressString()) .setAddress(entry.getAddressString())
.setAmount(reserveAmount) .setAmount(reserveAmount)
.setRelay(true) .setRelay(true)
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY)); .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY));
break;
} catch (Exception e) {
log.warn("Error creating split output tx to fund offer {} at subaddress {}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
}
log.info("Done creating split output tx to fund offer {} in {} ms", openOffer.getId(), System.currentTimeMillis() - startTime); log.info("Done creating split output tx to fund offer {} in {} ms", openOffer.getId(), System.currentTimeMillis() - startTime);
}
// schedule txs // schedule txs
openOffer.setSplitOutputTxHash(splitOutputTx.getHash()); openOffer.setSplitOutputTxHash(splitOutputTx.getHash());

View File

@ -133,7 +133,7 @@ public class PlaceOfferProtocol {
stopTimeoutTimer(); stopTimeoutTimer();
timeoutTimer = UserThread.runAfter(() -> { timeoutTimer = UserThread.runAfter(() -> {
handleError(Res.get("createOffer.timeoutAtPublishing")); handleError(Res.get("createOffer.timeoutAtPublishing"));
}, TradeProtocol.TRADE_TIMEOUT_SECONDS); }, TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
} }
private void stopTimeoutTimer() { private void stopTimeoutTimer() {

View File

@ -17,12 +17,22 @@
package haveno.core.offer.placeoffer.tasks; package haveno.core.offer.placeoffer.tasks;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import haveno.common.taskrunner.Task; import haveno.common.taskrunner.Task;
import haveno.common.taskrunner.TaskRunner; import haveno.common.taskrunner.TaskRunner;
import haveno.core.offer.Offer; import haveno.core.offer.Offer;
import haveno.core.offer.OfferDirection;
import haveno.core.offer.OpenOffer;
import haveno.core.offer.placeoffer.PlaceOfferModel; import haveno.core.offer.placeoffer.PlaceOfferModel;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@Slf4j @Slf4j
@ -35,7 +45,8 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
@Override @Override
protected void run() { protected void run() {
Offer offer = model.getOpenOffer().getOffer(); OpenOffer openOffer = model.getOpenOffer();
Offer offer = openOffer.getOffer();
try { try {
runInterceptHook(); runInterceptHook();
@ -44,16 +55,51 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
model.getXmrWalletService().getConnectionService().verifyConnection(); model.getXmrWalletService().getConnectionService().verifyConnection();
// create reserve tx // create reserve tx
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(model.getOpenOffer()); MoneroTxWallet reserveTx = null;
model.setReserveTx(reserveTx); synchronized (XmrWalletService.WALLET_LOCK) {
// 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().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
XmrAddressEntry fundingEntry = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null);
Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex();
// attempt creating reserve tx
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
} catch (Exception e) {
log.warn("Error creating reserve tx, attempt={}/{}, offerId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, openOffer.getShortId(), e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
// check for error in case creating reserve tx exceeded timeout // TODO: better way? // check for error in case creating reserve tx exceeded timeout // TODO: better way?
if (!model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).isPresent()) { if (!model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).isPresent()) {
throw new RuntimeException("An error has occurred posting offer " + offer.getId() + " causing its subaddress entry to be deleted"); throw new RuntimeException("An error has occurred posting offer " + offer.getId() + " causing its subaddress entry to be deleted");
} }
if (reserveTx != null) break;
}
}
// collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// update offer state
openOffer.setReserveTxHash(reserveTx.getHash());
openOffer.setReserveTxHex(reserveTx.getFullHex());
openOffer.setReserveTxKey(reserveTx.getKey());
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
}
// reset protocol timeout // reset protocol timeout
model.getProtocol().startTimeoutTimer(); model.getProtocol().startTimeoutTimer();
model.setReserveTx(reserveTx);
complete(); complete();
} catch (Throwable t) { } catch (Throwable t) {
offer.setErrorMessage("An error occurred.\n" + offer.setErrorMessage("An error occurred.\n" +

View File

@ -524,7 +524,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// update multisig hex // update multisig hex
if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex()); if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex());
if (trade.walletExists()) trade.importMultisigHex();
// add chat message with price info // add chat message with price info
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0); if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
@ -896,17 +895,12 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
} }
// create dispute payout tx // create dispute payout tx
MoneroTxWallet payoutTx = null; MoneroTxWallet payoutTx = trade.createDisputePayoutTx(txConfig);
try {
payoutTx = trade.getWallet().createTx(txConfig);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Loser payout is too small to cover the mining fee");
}
// update trade state // update trade state
if (updateState) { if (updateState) {
trade.getProcessModel().setUnsignedPayoutTx(payoutTx); trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());
trade.setPayoutTx(payoutTx); trade.setPayoutTx(payoutTx);
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
if (trade.getBuyer().getUpdatedMultisigHex() != null && trade.getBuyer().getUnsignedPayoutTxHex() == null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); if (trade.getBuyer().getUpdatedMultisigHex() != null && trade.getBuyer().getUnsignedPayoutTxHex() == null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());

View File

@ -36,7 +36,6 @@ package haveno.core.support.dispute.arbitration;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import common.utils.GenUtils;
import haveno.common.ThreadUtils; import haveno.common.ThreadUtils;
import haveno.common.Timer; import haveno.common.Timer;
import haveno.common.UserThread; import haveno.common.UserThread;
@ -68,6 +67,7 @@ import haveno.core.trade.Contract;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager; import haveno.core.trade.TradeManager;
import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.xmr.wallet.TradeWalletService; import haveno.core.xmr.wallet.TradeWalletService;
import haveno.core.xmr.wallet.XmrWalletService; import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.AckMessageSourceType; import haveno.network.p2p.AckMessageSourceType;
@ -290,7 +290,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
log.info("Deferring signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId()); log.info("Deferring signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
if (trade.isPayoutPublished()) break; if (trade.isPayoutPublished()) break;
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5); HavenoUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5);
} }
if (!trade.isPayoutPublished()) trade.syncAndPollWallet(); if (!trade.isPayoutPublished()) trade.syncAndPollWallet();
} }
@ -310,7 +310,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId()); log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
} else { } else {
if (e instanceof IllegalArgumentException) throw e; if (e instanceof IllegalArgumentException) throw e;
else throw new RuntimeException("Failed to sign and publish dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId, e); else throw new RuntimeException("Failed to sign and publish dispute payout tx from arbitrator for " + trade.getClass().getSimpleName() + " " + tradeId + ": " + e.getMessage(), e);
} }
} }
} }
@ -430,8 +430,8 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
if (!expectedBuyerAmount.equals(actualBuyerAmount)) throw new IllegalArgumentException("Unexpected buyer payout: " + expectedBuyerAmount + " vs " + actualBuyerAmount); if (!expectedBuyerAmount.equals(actualBuyerAmount)) throw new IllegalArgumentException("Unexpected buyer payout: " + expectedBuyerAmount + " vs " + actualBuyerAmount);
if (!expectedSellerAmount.equals(actualSellerAmount)) throw new IllegalArgumentException("Unexpected seller payout: " + expectedSellerAmount + " vs " + actualSellerAmount); if (!expectedSellerAmount.equals(actualSellerAmount)) throw new IllegalArgumentException("Unexpected seller payout: " + expectedSellerAmount + " vs " + actualSellerAmount);
// check wallet's daemon connection // check daemon connection
trade.checkAndVerifyDaemonConnection(); trade.verifyDaemonConnection();
// determine if we already signed dispute payout tx // determine if we already signed dispute payout tx
// TODO: better way, such as by saving signed dispute payout tx hex in designated field instead of shared payoutTxHex field? // TODO: better way, such as by saving signed dispute payout tx hex in designated field instead of shared payoutTxHex field?
@ -471,8 +471,17 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
} }
// submit fully signed payout tx to the network // submit fully signed payout tx to the network
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex()); List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex());
disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
break;
} catch (Exception e) {
log.warn("Failed to submit dispute payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
}
// update state // update state
trade.setPayoutTx(disputeTxSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx? trade.setPayoutTx(disputeTxSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?

View File

@ -19,6 +19,8 @@ package haveno.core.trade;
import com.google.common.base.CaseFormat; import com.google.common.base.CaseFormat;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import common.utils.GenUtils;
import haveno.common.config.Config; import haveno.common.config.Config;
import haveno.common.crypto.CryptoException; import haveno.common.crypto.CryptoException;
import haveno.common.crypto.Hash; import haveno.common.crypto.Hash;
@ -70,6 +72,17 @@ public class HavenoUtils {
public static final double TAKER_FEE_PCT = 0.0075; // 0.75% public static final double TAKER_FEE_PCT = 0.0075; // 0.75%
public static final double PENALTY_FEE_PCT = 0.02; // 2% public static final double PENALTY_FEE_PCT = 0.02; // 2%
// synchronize requests to the daemon
private static boolean SYNC_DAEMON_REQUESTS = true; // sync long requests to daemon (e.g. refresh, update pool)
private static boolean SYNC_WALLET_REQUESTS = false; // additionally sync wallet functions to daemon (e.g. create tx, import multisig hex)
private static Object DAEMON_LOCK = new Object();
public static Object getDaemonLock() {
return SYNC_DAEMON_REQUESTS ? DAEMON_LOCK : new Object();
}
public static Object getWalletFunctionLock() {
return SYNC_WALLET_REQUESTS ? getDaemonLock() : new Object();
}
// non-configurable // non-configurable
public static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US); // use the US locale as a base for all DecimalFormats (commas should be omitted from number strings) public static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance(Locale.US); // use the US locale as a base for all DecimalFormats (commas should be omitted from number strings)
public static int XMR_SMALLEST_UNIT_EXPONENT = 12; public static int XMR_SMALLEST_UNIT_EXPONENT = 12;
@ -108,6 +121,10 @@ public class HavenoUtils {
return new Date().before(releaseDatePlusDays); return new Date().before(releaseDatePlusDays);
} }
public static void waitFor(long waitMs) {
GenUtils.waitFor(waitMs);
}
// ----------------------- CONVERSION UTILS ------------------------------- // ----------------------- CONVERSION UTILS -------------------------------
public static BigInteger coinToAtomicUnits(Coin coin) { public static BigInteger coinToAtomicUnits(Coin coin) {

View File

@ -37,7 +37,6 @@ package haveno.core.trade;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import com.google.protobuf.Message; import com.google.protobuf.Message;
import common.utils.GenUtils;
import haveno.common.ThreadUtils; import haveno.common.ThreadUtils;
import haveno.common.UserThread; import haveno.common.UserThread;
import haveno.common.crypto.Encryption; import haveno.common.crypto.Encryption;
@ -120,7 +119,6 @@ import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -479,7 +477,6 @@ public abstract class Trade implements Tradable, Model {
private long payoutTxFee; private long payoutTxFee;
private Long payoutHeight; private Long payoutHeight;
private IdlePayoutSyncer idlePayoutSyncer; private IdlePayoutSyncer idlePayoutSyncer;
@Getter @Getter
@Setter @Setter
private boolean isCompleted; private boolean isCompleted;
@ -638,18 +635,14 @@ public abstract class Trade implements Tradable, Model {
// handle trade state events // handle trade state events
tradeStateSubscription = EasyBind.subscribe(stateProperty, newValue -> { tradeStateSubscription = EasyBind.subscribe(stateProperty, newValue -> {
if (!isInitialized || isShutDownStarted) return; if (!isInitialized || isShutDownStarted) return;
ThreadUtils.execute(() -> { // no processing
if (newValue == Trade.State.MULTISIG_COMPLETED) {
updatePollPeriod();
startPolling();
}
}, getId());
}); });
// handle trade phase events // handle trade phase events
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> { tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
if (!isInitialized || isShutDownStarted) return; if (!isInitialized || isShutDownStarted) return;
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
if (newValue == Trade.Phase.DEPOSIT_REQUESTED) startPolling();
if (isDepositsPublished() && !isPayoutUnlocked()) updatePollPeriod(); if (isDepositsPublished() && !isPayoutUnlocked()) updatePollPeriod();
if (isPaymentReceived()) { if (isPaymentReceived()) {
UserThread.execute(() -> { UserThread.execute(() -> {
@ -674,9 +667,9 @@ public abstract class Trade implements Tradable, Model {
// sync main wallet to update pending balance // sync main wallet to update pending balance
new Thread(() -> { new Thread(() -> {
GenUtils.waitFor(1000); HavenoUtils.waitFor(1000);
if (isShutDownStarted) return; if (isShutDownStarted) return;
if (Boolean.TRUE.equals(xmrConnectionService.isConnected())) xmrWalletService.syncWallet(xmrWalletService.getWallet()); if (xmrConnectionService.isConnected()) xmrWalletService.syncWallet();
}).start(); }).start();
// complete disputed trade // complete disputed trade
@ -731,16 +724,17 @@ public abstract class Trade implements Tradable, Model {
setPayoutStateUnlocked(); setPayoutStateUnlocked();
return; return;
} else { } else {
throw new IllegalStateException("Missing trade wallet for " + getClass().getSimpleName() + " " + getId()); log.warn("Missing trade wallet for {} {}, state={}, marked completed={}", getClass().getSimpleName(), getShortId(), getState(), isCompleted());
return;
} }
} }
// initialize syncing and polling // start polling if deposit requested
tryInitPolling(); if (isDepositRequested()) tryInitPolling();
} }
public void requestPersistence() { public void requestPersistence() {
processModel.getTradeManager().requestPersistence(); if (processModel.getTradeManager() != null) processModel.getTradeManager().requestPersistence();
} }
public TradeProtocol getProtocol() { public TradeProtocol getProtocol() {
@ -793,21 +787,8 @@ public abstract class Trade implements Tradable, Model {
return MONERO_TRADE_WALLET_PREFIX + getShortId() + "_" + getShortUid(); return MONERO_TRADE_WALLET_PREFIX + getShortId() + "_" + getShortUid();
} }
public void checkAndVerifyDaemonConnection() { public void verifyDaemonConnection() {
if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Connection service is not connected to a Monero node");
// check connection which might update
xmrConnectionService.checkConnection();
xmrConnectionService.verifyConnection();
// check wallet connection on same thread as connection change
CountDownLatch latch = new CountDownLatch(1);
ThreadUtils.submitToPool((() -> {
ThreadUtils.execute(() -> {
if (!isWalletConnectedToDaemon()) throw new RuntimeException("Trade wallet is not connected to a Monero node"); // wallet connection is updated on trade thread
latch.countDown();
}, getConnectionChangedThreadId());
}));
HavenoUtils.awaitLatch(latch); // TODO: better way?
} }
public boolean isWalletConnectedToDaemon() { public boolean isWalletConnectedToDaemon() {
@ -848,7 +829,7 @@ public abstract class Trade implements Tradable, Model {
// reset wallet poll period after duration // reset wallet poll period after duration
new Thread(() -> { new Thread(() -> {
GenUtils.waitFor(pollNormalDuration); HavenoUtils.waitFor(pollNormalDuration);
Long pollNormalStartTimeMsCopy = pollNormalStartTimeMs; // copy to avoid race condition Long pollNormalStartTimeMsCopy = pollNormalStartTimeMs; // copy to avoid race condition
if (pollNormalStartTimeMsCopy == null) return; if (pollNormalStartTimeMsCopy == null) return;
if (!isShutDown && System.currentTimeMillis() >= pollNormalStartTimeMsCopy + pollNormalDuration) { if (!isShutDown && System.currentTimeMillis() >= pollNormalStartTimeMsCopy + pollNormalDuration) {
@ -860,21 +841,38 @@ public abstract class Trade implements Tradable, Model {
public void importMultisigHex() { public void importMultisigHex() {
synchronized (walletLock) { synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
doImportMultisigHex();
break;
} catch (Exception e) {
log.warn("Failed to import multisig hex, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
}
}
}
}
private void doImportMultisigHex() {
// ensure wallet sees deposits confirmed // ensure wallet sees deposits confirmed
if (!isDepositsConfirmed()) syncAndPollWallet(); if (!isDepositsConfirmed()) syncAndPollWallet();
// import multisig hexes // collect multisig hex from peers
List<String> multisigHexes = new ArrayList<String>(); List<String> multisigHexes = new ArrayList<String>();
for (TradePeer node : getAllTradeParties()) if (node.getUpdatedMultisigHex() != null) multisigHexes.add(node.getUpdatedMultisigHex()); for (TradePeer peer : getOtherPeers()) if (peer.getUpdatedMultisigHex() != null) multisigHexes.add(peer.getUpdatedMultisigHex());
if (!multisigHexes.isEmpty()) {
log.info("Importing multisig hex for {} {}", getClass().getSimpleName(), getId()); // import multisig hex
log.info("Importing multisig hexes for {} {}, count={}", getClass().getSimpleName(), getShortId(), multisigHexes.size());
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
getWallet().importMultisigHex(multisigHexes.toArray(new String[0])); if (!multisigHexes.isEmpty()) {
log.info("Done importing multisig hex for {} {} in {} ms", getClass().getSimpleName(), getId(), System.currentTimeMillis() - startTime); wallet.importMultisigHex(multisigHexes.toArray(new String[0]));
}
requestSaveWallet(); requestSaveWallet();
} }
log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size());
} }
public void changeWalletPassword(String oldPassword, String newPassword) { public void changeWalletPassword(String oldPassword, String newPassword) {
@ -891,10 +889,10 @@ public abstract class Trade implements Tradable, Model {
public void saveWallet() { public void saveWallet() {
synchronized (walletLock) { synchronized (walletLock) {
if (!walletExists()) { if (!walletExists()) {
log.warn("Cannot save wallet for {} {} because it does not exist", getClass().getSimpleName(), getId()); log.warn("Cannot save wallet for {} {} because it does not exist", getClass().getSimpleName(), getShortId());
return; return;
} }
if (wallet == null) throw new RuntimeException("Trade wallet is not open for trade " + getId()); if (wallet == null) throw new RuntimeException("Trade wallet is not open for trade " + getShortId());
xmrWalletService.saveWallet(wallet); xmrWalletService.saveWallet(wallet);
maybeBackupWallet(); maybeBackupWallet();
} }
@ -953,7 +951,13 @@ public abstract class Trade implements Tradable, Model {
// check for balance // check for balance
if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) { if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) {
throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because it has a balance"); synchronized (HavenoUtils.getDaemonLock()) {
log.warn("Rescanning spent outputs for {} {}", getClass().getSimpleName(), getId());
wallet.rescanSpent();
if (wallet.getBalance().compareTo(BigInteger.ZERO) > 0) {
throw new IllegalStateException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because it has a balance of " + wallet.getBalance());
}
}
} }
// force close wallet without warning // force close wallet without warning
@ -1021,17 +1025,44 @@ public abstract class Trade implements Tradable, Model {
return contract; return contract;
} }
public MoneroTxWallet createTx(MoneroTxConfig txConfig) {
synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
return wallet.createTx(txConfig);
}
}
}
/** /**
* Create the payout tx. * Create the payout tx.
* *
* @return MoneroTxWallet the payout tx when the trade is successfully completed * @return the payout tx when the trade is successfully completed
*/ */
public MoneroTxWallet createPayoutTx() { public MoneroTxWallet createPayoutTx() {
// check connection to monero daemon // check connection to monero daemon
checkAndVerifyDaemonConnection(); verifyDaemonConnection();
// check multisig import // create payout tx
synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
return doCreatePayoutTx();
} catch (Exception e) {
log.warn("Failed to create payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
}
throw new RuntimeException("Failed to create payout tx for " + getClass().getSimpleName() + " " + getId());
}
}
}
private MoneroTxWallet doCreatePayoutTx() {
// check if multisig import needed
MoneroWallet multisigWallet = getWallet(); MoneroWallet multisigWallet = getWallet();
if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed"); if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed");
@ -1047,7 +1078,7 @@ public abstract class Trade implements Tradable, Model {
BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount); BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount);
// create payout tx // create payout tx
MoneroTxWallet payoutTx = multisigWallet.createTx(new MoneroTxConfig() MoneroTxWallet payoutTx = createTx(new MoneroTxConfig()
.setAccountIndex(0) .setAccountIndex(0)
.addDestination(buyerPayoutAddress, buyerPayoutAmount) .addDestination(buyerPayoutAddress, buyerPayoutAmount)
.addDestination(sellerPayoutAddress, sellerPayoutAmount) .addDestination(sellerPayoutAddress, sellerPayoutAmount)
@ -1066,6 +1097,24 @@ public abstract class Trade implements Tradable, Model {
return payoutTx; return payoutTx;
} }
public MoneroTxWallet createDisputePayoutTx(MoneroTxConfig txConfig) {
synchronized (walletLock) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
return createTx(txConfig);
} catch (Exception e) {
if (e.getMessage().contains("not possible")) throw new RuntimeException("Loser payout is too small to cover the mining fee");
log.warn("Failed to create payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
}
}
throw new RuntimeException("Failed to create payout tx for " + getClass().getSimpleName() + " " + getId());
}
}
}
/** /**
* Process a payout tx. * Process a payout tx.
* *
@ -1074,6 +1123,7 @@ public abstract class Trade implements Tradable, Model {
* @param publish publishes the signed payout tx if true * @param publish publishes the signed payout tx if true
*/ */
public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) { public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) {
synchronized (walletLock) {
log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId()); log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId());
// gather relevant info // gather relevant info
@ -1117,8 +1167,8 @@ public abstract class Trade implements Tradable, Model {
BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCostSplit); BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCostSplit);
if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout); if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout);
// check wallet connection // check connection
if (sign || publish) checkAndVerifyDaemonConnection(); if (sign || publish) verifyDaemonConnection();
// handle tx signing // handle tx signing
if (sign) { if (sign) {
@ -1147,10 +1197,20 @@ public abstract class Trade implements Tradable, Model {
// submit payout tx // submit payout tx
if (publish) { if (publish) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
wallet.submitMultisigTxHex(payoutTxHex); wallet.submitMultisigTxHex(payoutTxHex);
pollWallet(); break;
} catch (Exception e) {
log.warn("Failed to submit payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
} }
} }
}
}
pollWallet();
}
/** /**
* Decrypt the peer's payment account payload using the given key. * Decrypt the peer's payment account payload using the given key.
@ -1173,6 +1233,7 @@ public abstract class Trade implements Tradable, Model {
// set payment account payload // set payment account payload
getTradePeer().setPaymentAccountPayload(paymentAccountPayload); getTradePeer().setPaymentAccountPayload(paymentAccountPayload);
processModel.getPaymentAccountDecryptedProperty().set(true);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -1220,6 +1281,7 @@ public abstract class Trade implements Tradable, Model {
public void clearAndShutDown() { public void clearAndShutDown() {
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
clearProcessData(); clearProcessData();
onShutDownStarted();
ThreadUtils.submitToPool(() -> shutDown()); // run off trade thread ThreadUtils.submitToPool(() -> shutDown()); // run off trade thread
}, getId()); }, getId());
} }
@ -1237,7 +1299,7 @@ public abstract class Trade implements Tradable, Model {
// TODO: clear other process data // TODO: clear other process data
setPayoutTxHex(null); setPayoutTxHex(null);
for (TradePeer peer : getAllTradeParties()) { for (TradePeer peer : getAllPeers()) {
peer.setUnsignedPayoutTxHex(null); peer.setUnsignedPayoutTxHex(null);
peer.setUpdatedMultisigHex(null); peer.setUpdatedMultisigHex(null);
peer.setDisputeClosedMessage(null); peer.setDisputeClosedMessage(null);
@ -1294,7 +1356,7 @@ public abstract class Trade implements Tradable, Model {
// repeatedly acquire lock to clear tasks // repeatedly acquire lock to clear tasks
for (int i = 0; i < 20; i++) { for (int i = 0; i < 20; i++) {
synchronized (this) { synchronized (this) {
GenUtils.waitFor(10); HavenoUtils.waitFor(10);
} }
} }
@ -1390,6 +1452,7 @@ public abstract class Trade implements Tradable, Model {
} }
this.state = state; this.state = state;
requestPersistence();
UserThread.await(() -> { UserThread.await(() -> {
stateProperty.set(state); stateProperty.set(state);
phaseProperty.set(state.getPhase()); phaseProperty.set(state.getPhase());
@ -1421,6 +1484,7 @@ public abstract class Trade implements Tradable, Model {
} }
this.payoutState = payoutState; this.payoutState = payoutState;
requestPersistence();
UserThread.await(() -> payoutStateProperty.set(payoutState)); UserThread.await(() -> payoutStateProperty.set(payoutState));
} }
@ -1572,13 +1636,13 @@ public abstract class Trade implements Tradable, Model {
throw new RuntimeException("Trade is not maker, taker, or arbitrator"); throw new RuntimeException("Trade is not maker, taker, or arbitrator");
} }
private List<TradePeer> getPeers() { private List<TradePeer> getOtherPeers() {
List<TradePeer> peers = getAllTradeParties(); List<TradePeer> peers = getAllPeers();
if (!peers.remove(getSelf())) throw new IllegalStateException("Failed to remove self from list of peers"); if (!peers.remove(getSelf())) throw new IllegalStateException("Failed to remove self from list of peers");
return peers; return peers;
} }
private List<TradePeer> getAllTradeParties() { private List<TradePeer> getAllPeers() {
List<TradePeer> peers = new ArrayList<TradePeer>(); List<TradePeer> peers = new ArrayList<TradePeer>();
peers.add(getMaker()); peers.add(getMaker());
peers.add(getTaker()); peers.add(getTaker());
@ -1765,7 +1829,7 @@ public abstract class Trade implements Tradable, Model {
if (this instanceof BuyerTrade) { if (this instanceof BuyerTrade) {
return getArbitrator().isDepositsConfirmedMessageAcked(); return getArbitrator().isDepositsConfirmedMessageAcked();
} else { } else {
for (TradePeer peer : getPeers()) if (!peer.isDepositsConfirmedMessageAcked()) return false; for (TradePeer peer : getOtherPeers()) if (!peer.isDepositsConfirmedMessageAcked()) return false;
return true; return true;
} }
} }
@ -1982,13 +2046,19 @@ public abstract class Trade implements Tradable, Model {
} }
// sync and reprocess messages on new thread // sync and reprocess messages on new thread
if (isInitialized && connection != null && !Boolean.FALSE.equals(connection.isConnected())) { if (isInitialized && connection != null && !Boolean.FALSE.equals(xmrConnectionService.isConnected())) {
ThreadUtils.execute(() -> tryInitPolling(), getId()); ThreadUtils.execute(() -> tryInitPolling(), getId());
} }
} }
} }
private void tryInitPolling() { private void tryInitPolling() {
if (isShutDownStarted) return; if (isShutDownStarted) return;
// set known deposit txs
List<MoneroTxWallet> depositTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true).setInTxPool(false));
setDepositTxs(depositTxs);
// start polling
if (!isIdling()) { if (!isIdling()) {
tryInitPollingAux(); tryInitPollingAux();
} else { } else {
@ -2023,9 +2093,12 @@ public abstract class Trade implements Tradable, Model {
private void syncWallet(boolean pollWallet) { private void syncWallet(boolean pollWallet) {
if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId()); if (getWallet() == null) throw new RuntimeException("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 (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId());
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId()); if (isWalletBehind()) {
xmrWalletService.syncWallet(getWallet()); log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getShortId());
log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId()); long startTime = System.currentTimeMillis();
syncWalletIfBehind();
log.info("Done syncing wallet for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime);
}
// apply tor after wallet synced depending on configuration // apply tor after wallet synced depending on configuration
if (!wasWalletSynced) { if (!wasWalletSynced) {
@ -2063,6 +2136,7 @@ public abstract class Trade implements Tradable, Model {
private void startPolling() { private void startPolling() {
synchronized (walletLock) { synchronized (walletLock) {
if (isShutDownStarted || isPollInProgress()) return; if (isShutDownStarted || isPollInProgress()) return;
updatePollPeriod();
log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId()); log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId());
pollLooper = new TaskLooper(() -> pollWallet()); pollLooper = new TaskLooper(() -> pollWallet());
pollLooper.start(pollPeriodMs); pollLooper.start(pollPeriodMs);
@ -2110,7 +2184,15 @@ public abstract class Trade implements Tradable, Model {
MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true);
Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null); Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null);
if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible
List<MoneroTxWallet> txs = wallet.getTxs(query); List<MoneroTxWallet> txs;
if (!updatePool) txs = wallet.getTxs(query);
else {
synchronized (walletLock) {
synchronized (HavenoUtils.getDaemonLock()) {
txs = wallet.getTxs(query);
}
}
}
setDepositTxs(txs); setDepositTxs(txs);
if (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null) return; // skip if either deposit tx not seen if (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null) return; // skip if either deposit tx not seen
setStateDepositsSeen(); setStateDepositsSeen();
@ -2142,7 +2224,7 @@ public abstract class Trade implements Tradable, Model {
if (isPayoutExpected || isPayoutPublished()) syncWalletIfBehind(); if (isPayoutExpected || isPayoutPublished()) syncWalletIfBehind();
// rescan spent outputs to detect unconfirmed payout tx // rescan spent outputs to detect unconfirmed payout tx
if (isPayoutExpected && !isPayoutPublished()) { if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) {
try { try {
wallet.rescanSpent(); wallet.rescanSpent();
} catch (Exception e) { } catch (Exception e) {
@ -2154,7 +2236,15 @@ public abstract class Trade implements Tradable, Model {
MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true); MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true);
boolean updatePool = isPayoutExpected && !isPayoutConfirmed(); boolean updatePool = isPayoutExpected && !isPayoutConfirmed();
if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible if (!updatePool) query.setInTxPool(false); // avoid updating from pool if possible
List<MoneroTxWallet> txs = wallet.getTxs(query); List<MoneroTxWallet> txs = null;
if (!updatePool) txs = wallet.getTxs(query);
else {
synchronized (walletLock) {
synchronized (HavenoUtils.getDaemonLock()) {
txs = wallet.getTxs(query);
}
}
}
setDepositTxs(txs); setDepositTxs(txs);
// check if any outputs spent (observed on payout published) // check if any outputs spent (observed on payout published)
@ -2191,7 +2281,15 @@ public abstract class Trade implements Tradable, Model {
} }
private void syncWalletIfBehind() { private void syncWalletIfBehind() {
if (wallet.getHeight() < xmrConnectionService.getTargetHeight()) syncWallet(false); if (isWalletBehind()) {
synchronized (walletLock) {
xmrWalletService.syncWallet(wallet);
}
}
}
private boolean isWalletBehind() {
return wallet.getHeight() < xmrConnectionService.getTargetHeight();
} }
private void setDepositTxs(List<? extends MoneroTx> txs) { private void setDepositTxs(List<? extends MoneroTx> txs) {
@ -2278,9 +2376,8 @@ public abstract class Trade implements Tradable, Model {
processing = false; processing = false;
} catch (Exception e) { } catch (Exception e) {
processing = false; processing = false;
boolean isWalletConnected = isWalletConnectedToDaemon(); if (!isInitialized || isShutDownStarted) return;
if (!isWalletConnected) xmrConnectionService.checkConnection(); // check connection if wallet is not connected if (isWalletConnectedToDaemon()) {
if (isInitialized &&!isShutDownStarted && isWalletConnected) {
e.printStackTrace(); e.printStackTrace();
log.warn("Error polling idle trade for {} {}: {}. Monerod={}", getClass().getSimpleName(), getId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection()); log.warn("Error polling idle trade for {} {}: {}. Monerod={}", getClass().getSimpleName(), getId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection());
}; };

View File

@ -38,7 +38,6 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.inject.Inject; import com.google.inject.Inject;
import common.utils.GenUtils;
import haveno.common.ClockWatcher; import haveno.common.ClockWatcher;
import haveno.common.ThreadUtils; import haveno.common.ThreadUtils;
import haveno.common.UserThread; import haveno.common.UserThread;
@ -512,7 +511,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}).start(); }).start();
// allow execution to start // allow execution to start
GenUtils.waitFor(100); HavenoUtils.waitFor(100);
} }
private void initPersistedTrade(Trade trade) { private void initPersistedTrade(Trade trade) {
@ -1249,27 +1248,20 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
} }
private void addTrade(Trade trade) { private void addTrade(Trade trade) {
UserThread.execute(() -> {
synchronized (tradableList) { synchronized (tradableList) {
if (tradableList.add(trade)) { if (tradableList.add(trade)) {
requestPersistence(); requestPersistence();
} }
} }
});
} }
private void removeTrade(Trade trade) { private void removeTrade(Trade trade) {
log.info("TradeManager.removeTrade() " + trade.getId()); log.info("TradeManager.removeTrade() " + trade.getId());
synchronized (tradableList) {
if (!tradableList.contains(trade)) return;
}
// remove trade // remove trade
UserThread.execute(() -> {
synchronized (tradableList) { synchronized (tradableList) {
tradableList.remove(trade); if (!tradableList.remove(trade)) return;
} }
});
// unregister and persist // unregister and persist
p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade)); p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade));
@ -1277,21 +1269,18 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
} }
private void maybeRemoveTradeOnError(Trade trade) { private void maybeRemoveTradeOnError(Trade trade) {
synchronized (tradableList) {
if (trade.isDepositRequested() && !trade.isDepositFailed()) { if (trade.isDepositRequested() && !trade.isDepositFailed()) {
listenForCleanup(trade); listenForCleanup(trade);
} else { } else {
removeTradeOnError(trade); removeTradeOnError(trade);
} }
} }
}
private void removeTradeOnError(Trade trade) { private void removeTradeOnError(Trade trade) {
log.warn("TradeManager.removeTradeOnError() trade={}, tradeId={}, state={}", trade.getClass().getSimpleName(), trade.getShortId(), trade.getState()); log.warn("TradeManager.removeTradeOnError() trade={}, tradeId={}, state={}", trade.getClass().getSimpleName(), trade.getShortId(), trade.getState());
synchronized (tradableList) {
// unreserve taker key images // unreserve taker key images
if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) { if (trade instanceof TakerTrade) {
xmrWalletService.thawOutputs(trade.getSelf().getReserveTxKeyImages()); xmrWalletService.thawOutputs(trade.getSelf().getReserveTxKeyImages());
trade.getSelf().setReserveTxKeyImages(null); trade.getSelf().setReserveTxKeyImages(null);
} }
@ -1301,7 +1290,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
if (trade instanceof MakerTrade && openOffer.isPresent()) { if (trade instanceof MakerTrade && openOffer.isPresent()) {
openOfferManager.unreserveOpenOffer(openOffer.get()); openOfferManager.unreserveOpenOffer(openOffer.get());
} }
}
// clear and shut down trade // clear and shut down trade
trade.clearAndShutDown(); trade.clearAndShutDown();
@ -1358,7 +1346,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
new Thread(() -> { new Thread(() -> {
// wait minimum time // wait minimum time
GenUtils.waitFor(Math.max(0, REMOVE_AFTER_MS - (System.currentTimeMillis() - startTime))); HavenoUtils.waitFor(Math.max(0, REMOVE_AFTER_MS - (System.currentTimeMillis() - startTime)));
// get trade's deposit txs from daemon // get trade's deposit txs from daemon
MoneroTx makerDepositTx = trade.getMaker().getDepositTxHash() == null ? null : xmrWalletService.getDaemon().getTx(trade.getMaker().getDepositTxHash()); MoneroTx makerDepositTx = trade.getMaker().getDepositTxHash() == null ? null : xmrWalletService.getDaemon().getTx(trade.getMaker().getDepositTxHash());

View File

@ -59,13 +59,13 @@ public class ArbitratorProtocol extends DisputeProtocol {
ArbitratorSendInitTradeOrMultisigRequests.class) ArbitratorSendInitTradeOrMultisigRequests.class)
.using(new TradeTaskRunner(trade, .using(new TradeTaskRunner(trade,
() -> { () -> {
startTimeout(TRADE_TIMEOUT_SECONDS); startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
handleTaskRunnerSuccess(peer, message); handleTaskRunnerSuccess(peer, message);
}, },
errorMessage -> { errorMessage -> {
handleTaskRunnerFault(peer, message, errorMessage); handleTaskRunnerFault(peer, message, errorMessage);
})) }))
.withTimeout(TRADE_TIMEOUT_SECONDS)) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true); .executeTasks(true);
awaitTradeLatch(); awaitTradeLatch();
} }
@ -100,7 +100,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
errorMessage -> { errorMessage -> {
handleTaskRunnerFault(sender, request, errorMessage); handleTaskRunnerFault(sender, request, errorMessage);
})) }))
.withTimeout(TRADE_TIMEOUT_SECONDS)) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true); .executeTasks(true);
awaitTradeLatch(); awaitTradeLatch();
} }

View File

@ -74,13 +74,13 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
MakerSendInitTradeRequest.class) MakerSendInitTradeRequest.class)
.using(new TradeTaskRunner(trade, .using(new TradeTaskRunner(trade,
() -> { () -> {
startTimeout(TRADE_TIMEOUT_SECONDS); startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
handleTaskRunnerSuccess(peer, message); handleTaskRunnerSuccess(peer, message);
}, },
errorMessage -> { errorMessage -> {
handleTaskRunnerFault(peer, message, errorMessage); handleTaskRunnerFault(peer, message, errorMessage);
})) }))
.withTimeout(TRADE_TIMEOUT_SECONDS)) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true); .executeTasks(true);
awaitTradeLatch(); awaitTradeLatch();
} }

View File

@ -79,13 +79,13 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
TakerSendInitTradeRequestToArbitrator.class) TakerSendInitTradeRequestToArbitrator.class)
.using(new TradeTaskRunner(trade, .using(new TradeTaskRunner(trade,
() -> { () -> {
startTimeout(TRADE_TIMEOUT_SECONDS); startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
unlatchTrade(); unlatchTrade();
}, },
errorMessage -> { errorMessage -> {
handleError(errorMessage); handleError(errorMessage);
})) }))
.withTimeout(TRADE_TIMEOUT_SECONDS)) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true); .executeTasks(true);
awaitTradeLatch(); awaitTradeLatch();
} }

View File

@ -46,6 +46,7 @@ import haveno.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage;
import haveno.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToArbitrator; import haveno.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToArbitrator;
import haveno.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToSeller; import haveno.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToSeller;
import haveno.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator; import haveno.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator;
import haveno.core.trade.protocol.tasks.SendDepositsConfirmedMessageToSeller;
import haveno.core.trade.protocol.tasks.TradeTask; import haveno.core.trade.protocol.tasks.TradeTask;
import haveno.network.p2p.NodeAddress; import haveno.network.p2p.NodeAddress;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -158,6 +159,6 @@ public class BuyerProtocol extends DisputeProtocol {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public Class<? extends TradeTask>[] getDepositsConfirmedTasks() { public Class<? extends TradeTask>[] getDepositsConfirmedTasks() {
return new Class[] { SendDepositsConfirmedMessageToArbitrator.class }; return new Class[] { SendDepositsConfirmedMessageToSeller.class, SendDepositsConfirmedMessageToArbitrator.class };
} }
} }

View File

@ -163,6 +163,7 @@ public class ProcessModel implements Model, PersistablePayload {
private ObjectProperty<MessageState> paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); private ObjectProperty<MessageState> paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
@Setter @Setter
private ObjectProperty<MessageState> paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED); private ObjectProperty<MessageState> paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED);
private ObjectProperty<Boolean> paymentAccountDecryptedProperty = new SimpleObjectProperty<>(false);
public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing) { public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing) {
this(offerId, accountId, pubKeyRing, new TradePeer(), new TradePeer(), new TradePeer()); this(offerId, accountId, pubKeyRing, new TradePeer(), new TradePeer(), new TradePeer());

View File

@ -79,13 +79,13 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
MakerSendInitTradeRequest.class) MakerSendInitTradeRequest.class)
.using(new TradeTaskRunner(trade, .using(new TradeTaskRunner(trade,
() -> { () -> {
startTimeout(TRADE_TIMEOUT_SECONDS); startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
handleTaskRunnerSuccess(peer, message); handleTaskRunnerSuccess(peer, message);
}, },
errorMessage -> { errorMessage -> {
handleTaskRunnerFault(peer, message, errorMessage); handleTaskRunnerFault(peer, message, errorMessage);
})) }))
.withTimeout(TRADE_TIMEOUT_SECONDS)) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true); .executeTasks(true);
awaitTradeLatch(); awaitTradeLatch();
} }

View File

@ -80,13 +80,13 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
TakerSendInitTradeRequestToArbitrator.class) TakerSendInitTradeRequestToArbitrator.class)
.using(new TradeTaskRunner(trade, .using(new TradeTaskRunner(trade,
() -> { () -> {
startTimeout(TRADE_TIMEOUT_SECONDS); startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
unlatchTrade(); unlatchTrade();
}, },
errorMessage -> { errorMessage -> {
handleError(errorMessage); handleError(errorMessage);
})) }))
.withTimeout(TRADE_TIMEOUT_SECONDS)) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true); .executeTasks(true);
awaitTradeLatch(); awaitTradeLatch();
} }

View File

@ -37,6 +37,7 @@ package haveno.core.trade.protocol;
import haveno.common.ThreadUtils; import haveno.common.ThreadUtils;
import haveno.common.Timer; import haveno.common.Timer;
import haveno.common.UserThread; import haveno.common.UserThread;
import haveno.common.config.Config;
import haveno.common.crypto.PubKeyRing; import haveno.common.crypto.PubKeyRing;
import haveno.common.handlers.ErrorMessageHandler; import haveno.common.handlers.ErrorMessageHandler;
import haveno.common.proto.network.NetworkEnvelope; import haveno.common.proto.network.NetworkEnvelope;
@ -67,7 +68,7 @@ import haveno.core.trade.protocol.tasks.ProcessInitMultisigRequest;
import haveno.core.trade.protocol.tasks.ProcessPaymentReceivedMessage; import haveno.core.trade.protocol.tasks.ProcessPaymentReceivedMessage;
import haveno.core.trade.protocol.tasks.ProcessPaymentSentMessage; import haveno.core.trade.protocol.tasks.ProcessPaymentSentMessage;
import haveno.core.trade.protocol.tasks.ProcessSignContractRequest; import haveno.core.trade.protocol.tasks.ProcessSignContractRequest;
import haveno.core.trade.protocol.tasks.ProcessSignContractResponse; import haveno.core.trade.protocol.tasks.SendDepositRequest;
import haveno.core.trade.protocol.tasks.RemoveOffer; import haveno.core.trade.protocol.tasks.RemoveOffer;
import haveno.core.trade.protocol.tasks.SellerPublishTradeStatistics; import haveno.core.trade.protocol.tasks.SellerPublishTradeStatistics;
import haveno.core.trade.protocol.tasks.MaybeResendDisputeClosedMessageWithPayout; import haveno.core.trade.protocol.tasks.MaybeResendDisputeClosedMessageWithPayout;
@ -93,8 +94,10 @@ import java.util.concurrent.CountDownLatch;
@Slf4j @Slf4j
public abstract class TradeProtocol implements DecryptedDirectMessageListener, DecryptedMailboxListener { public abstract class TradeProtocol implements DecryptedDirectMessageListener, DecryptedMailboxListener {
public static final int TRADE_TIMEOUT_SECONDS = 120; public static final int TRADE_STEP_TIMEOUT_SECONDS = Config.baseCurrencyNetwork().isTestnet() ? 45 : 180;
private static final String TIMEOUT_REACHED = "Timeout reached."; private static final String TIMEOUT_REACHED = "Timeout reached.";
public static final int MAX_ATTEMPTS = 3;
public static final long REPROCESS_DELAY_MS = 5000;
protected final ProcessModel processModel; protected final ProcessModel processModel;
protected final Trade trade; protected final Trade trade;
@ -104,6 +107,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
protected TradeResultHandler tradeResultHandler; protected TradeResultHandler tradeResultHandler;
protected ErrorMessageHandler errorMessageHandler; protected ErrorMessageHandler errorMessageHandler;
private boolean depositsConfirmedTasksCalled;
private int reprocessPaymentReceivedMessageCount; private int reprocessPaymentReceivedMessageCount;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -251,14 +255,14 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
} }
// send deposits confirmed message if applicable // send deposits confirmed message if applicable
maybeSendDepositsConfirmedMessages();
EasyBind.subscribe(trade.stateProperty(), state -> maybeSendDepositsConfirmedMessages()); EasyBind.subscribe(trade.stateProperty(), state -> maybeSendDepositsConfirmedMessages());
} }
public void maybeSendDepositsConfirmedMessages() { public void maybeSendDepositsConfirmedMessages() {
if (!trade.isInitialized() || trade.isShutDownStarted()) return; if (!trade.isInitialized() || trade.isShutDownStarted()) return;
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished()) return; if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished() || depositsConfirmedTasksCalled) return;
depositsConfirmedTasksCalled = true;
synchronized (trade) { synchronized (trade) {
if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down
latchTrade(); latchTrade();
@ -316,13 +320,13 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
MaybeSendSignContractRequest.class) MaybeSendSignContractRequest.class)
.using(new TradeTaskRunner(trade, .using(new TradeTaskRunner(trade,
() -> { () -> {
startTimeout(TRADE_TIMEOUT_SECONDS); startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
handleTaskRunnerSuccess(sender, request); handleTaskRunnerSuccess(sender, request);
}, },
errorMessage -> { errorMessage -> {
handleTaskRunnerFault(sender, request, errorMessage); handleTaskRunnerFault(sender, request, errorMessage);
})) }))
.withTimeout(TRADE_TIMEOUT_SECONDS)) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true); .executeTasks(true);
awaitTradeLatch(); awaitTradeLatch();
} }
@ -354,13 +358,13 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
ProcessSignContractRequest.class) ProcessSignContractRequest.class)
.using(new TradeTaskRunner(trade, .using(new TradeTaskRunner(trade,
() -> { () -> {
startTimeout(TRADE_TIMEOUT_SECONDS); startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
handleTaskRunnerSuccess(sender, message); handleTaskRunnerSuccess(sender, message);
}, },
errorMessage -> { errorMessage -> {
handleTaskRunnerFault(sender, message, errorMessage); handleTaskRunnerFault(sender, message, errorMessage);
})) }))
.withTimeout(TRADE_TIMEOUT_SECONDS)) // extend timeout .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) // extend timeout
.executeTasks(true); .executeTasks(true);
awaitTradeLatch(); awaitTradeLatch();
} else { } else {
@ -396,16 +400,16 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
.from(sender)) .from(sender))
.setup(tasks( .setup(tasks(
// TODO (woodser): validate request // TODO (woodser): validate request
ProcessSignContractResponse.class) SendDepositRequest.class)
.using(new TradeTaskRunner(trade, .using(new TradeTaskRunner(trade,
() -> { () -> {
startTimeout(TRADE_TIMEOUT_SECONDS); startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
handleTaskRunnerSuccess(sender, message); handleTaskRunnerSuccess(sender, message);
}, },
errorMessage -> { errorMessage -> {
handleTaskRunnerFault(sender, message, errorMessage); handleTaskRunnerFault(sender, message, errorMessage);
})) }))
.withTimeout(TRADE_TIMEOUT_SECONDS)) // extend timeout .withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) // extend timeout
.executeTasks(true); .executeTasks(true);
awaitTradeLatch(); awaitTradeLatch();
} else { } else {
@ -451,7 +455,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
errorMessage -> { errorMessage -> {
handleTaskRunnerFault(sender, response, errorMessage); handleTaskRunnerFault(sender, response, errorMessage);
})) }))
.withTimeout(TRADE_TIMEOUT_SECONDS)) .withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
.executeTasks(true); .executeTasks(true);
awaitTradeLatch(); awaitTradeLatch();
} }

View File

@ -64,7 +64,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
// skip if payout tx already created // skip if payout tx already created
if (trade.getPayoutTxHex() != null) { if (trade.getPayoutTxHex() != null) {
log.warn("Skipping preparation of payment sent message because payout tx is already created for {} {}", trade.getClass().getSimpleName(), trade.getId()); log.warn("Skipping preparation of payment sent message because payout tx is already created for {} {}", trade.getClass().getSimpleName(), trade.getShortId());
complete(); complete();
return; return;
} }
@ -83,7 +83,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
trade.importMultisigHex(); trade.importMultisigHex();
// create payout tx // create payout tx
log.info("Buyer creating unsigned payout tx"); log.info("Buyer creating unsigned payout tx for {} {} ", trade.getClass().getSimpleName(), trade.getShortId());
MoneroTxWallet payoutTx = trade.createPayoutTx(); MoneroTxWallet payoutTx = trade.createPayoutTx();
trade.setPayoutTx(payoutTx); trade.setPayoutTx(payoutTx);
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());

View File

@ -28,6 +28,7 @@ import haveno.core.trade.Trade.State;
import haveno.core.trade.messages.SignContractRequest; import haveno.core.trade.messages.SignContractRequest;
import haveno.core.trade.protocol.TradeProtocol; import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService;
import haveno.network.p2p.SendDirectMessageListener; import haveno.network.p2p.SendDirectMessageListener;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroOutput; import monero.daemon.model.MoneroOutput;
@ -78,37 +79,70 @@ public class MaybeSendSignContractRequest extends TradeTask {
trade.addInitProgressStep(); trade.addInitProgressStep();
// create deposit tx and freeze inputs // create deposit tx and freeze inputs
MoneroTxWallet depositTx = null;
synchronized (XmrWalletService.WALLET_LOCK) {
// check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId());
// collect relevant info
Integer subaddressIndex = null; Integer subaddressIndex = null;
boolean reserveExactAmount = false; boolean reserveExactAmount = false;
if (trade instanceof MakerTrade) { if (trade instanceof MakerTrade) {
reserveExactAmount = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isReserveExactAmount(); reserveExactAmount = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isReserveExactAmount();
if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex(); if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex();
} }
MoneroTxWallet depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
// check if trade still exists // thaw reserved outputs
if (!processModel.getTradeManager().hasOpenTrade(trade)) { if (trade.getSelf().getReserveTxKeyImages() != null) {
throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getId()); trade.getXmrWalletService().thawOutputs(trade.getSelf().getReserveTxKeyImages());
} }
// attempt creating deposit tx
try {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
} catch (Exception e) {
log.warn("Error creating deposit tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
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 deposit tx, tradeId=" + trade.getShortId());
if (depositTx != null) break;
}
}
} catch (Exception e) {
// re-freeze reserved outputs
if (trade.getSelf().getReserveTxKeyImages() != null) {
trade.getXmrWalletService().freezeOutputs(trade.getSelf().getReserveTxKeyImages());
}
throw e;
}
// reset protocol timeout // reset protocol timeout
trade.getProtocol().startTimeout(TradeProtocol.TRADE_TIMEOUT_SECONDS); trade.getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
// collect reserved key images // collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>(); List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : depositTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); for (MoneroOutput input : depositTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// save process state // update trade state
BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee()));
trade.getSelf().setDepositTx(depositTx); trade.getSelf().setDepositTx(depositTx);
trade.getSelf().setDepositTxHash(depositTx.getHash()); trade.getSelf().setDepositTxHash(depositTx.getHash());
trade.getSelf().setDepositTxFee(depositTx.getFee()); trade.getSelf().setDepositTxFee(depositTx.getFee());
trade.getSelf().setReserveTxKeyImages(reservedKeyImages); trade.getSelf().setReserveTxKeyImages(reservedKeyImages);
trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getOrCreateAddressEntry(processModel.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); // TODO (woodser): allow custom payout address? trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString()); // TODO (woodser): allow custom payout address?
trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId())); trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId()));
}
// TODO: security deposit should be based on trade amount, not max offer amount
BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
trade.getSelf().setSecurityDeposit(securityDeposit.subtract(depositTx.getFee()));
// maker signs deposit hash nonce to avoid challenge protocol // maker signs deposit hash nonce to avoid challenge protocol
byte[] sig = null; byte[] sig = null;
@ -170,4 +204,8 @@ public class MaybeSendSignContractRequest extends TradeTask {
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();
complete(); complete();
} }
private boolean isTimedOut() {
return !processModel.getTradeManager().hasOpenTrade(trade);
}
} }

View File

@ -18,6 +18,7 @@
package haveno.core.trade.protocol.tasks; package haveno.core.trade.protocol.tasks;
import haveno.common.ThreadUtils;
import haveno.common.taskrunner.TaskRunner; import haveno.common.taskrunner.TaskRunner;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.messages.DepositsConfirmedMessage; import haveno.core.trade.messages.DepositsConfirmedMessage;
@ -53,25 +54,26 @@ public class ProcessDepositsConfirmedMessage extends TradeTask {
if (sender.getNodeAddress().equals(trade.getSeller().getNodeAddress()) && sender != trade.getSeller()) trade.getSeller().setNodeAddress(null); if (sender.getNodeAddress().equals(trade.getSeller().getNodeAddress()) && sender != trade.getSeller()) trade.getSeller().setNodeAddress(null);
if (sender.getNodeAddress().equals(trade.getArbitrator().getNodeAddress()) && sender != trade.getArbitrator()) trade.getArbitrator().setNodeAddress(null); if (sender.getNodeAddress().equals(trade.getArbitrator().getNodeAddress()) && sender != trade.getArbitrator()) trade.getArbitrator().setNodeAddress(null);
// update multisig hex
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
// decrypt seller payment account payload if key given // decrypt seller payment account payload if key given
if (request.getSellerPaymentAccountKey() != null && trade.getTradePeer().getPaymentAccountPayload() == null) { if (request.getSellerPaymentAccountKey() != null && trade.getTradePeer().getPaymentAccountPayload() == null) {
log.info(trade.getClass().getSimpleName() + " decrypting using seller payment account key"); log.info(trade.getClass().getSimpleName() + " decrypting using seller payment account key");
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey()); trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
} }
// persist // update multisig hex
processModel.getTradeManager().requestPersistence(); sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
// try to import multisig hex (retry later) // try to import multisig hex (retry later)
ThreadUtils.submitToPool(() -> {
try { try {
trade.importMultisigHex(); trade.importMultisigHex();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
});
// persist
processModel.getTradeManager().requestPersistence();
complete(); complete();
} catch (Throwable t) { } catch (Throwable t) {
failed(t); failed(t);

View File

@ -34,7 +34,6 @@
package haveno.core.trade.protocol.tasks; package haveno.core.trade.protocol.tasks;
import common.utils.GenUtils;
import haveno.common.taskrunner.TaskRunner; import haveno.common.taskrunner.TaskRunner;
import haveno.core.account.sign.SignedWitness; import haveno.core.account.sign.SignedWitness;
import haveno.core.support.dispute.Dispute; import haveno.core.support.dispute.Dispute;
@ -145,7 +144,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId()); log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
if (trade.isPayoutPublished()) break; if (trade.isPayoutPublished()) break;
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5); HavenoUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5);
} }
if (!trade.isPayoutPublished()) trade.syncAndPollWallet(); if (!trade.isPayoutPublished()) trade.syncAndPollWallet();
} }

View File

@ -61,15 +61,6 @@ public class ProcessPaymentSentMessage extends TradeTask {
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey()); if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
trade.requestPersistence(); trade.requestPersistence();
// try to import multisig hex off main thread (retry later)
new Thread(() -> {
try {
trade.importMultisigHex();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
// update state // update state
trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
trade.requestPersistence(); trade.requestPersistence();

View File

@ -37,7 +37,7 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
runInterceptHook(); runInterceptHook();
// check connection // check connection
trade.checkAndVerifyDaemonConnection(); trade.verifyDaemonConnection();
// handle first time preparation // handle first time preparation
if (trade.getArbitrator().getPaymentReceivedMessage() == null) { if (trade.getArbitrator().getPaymentReceivedMessage() == null) {

View File

@ -29,14 +29,16 @@ import haveno.core.trade.protocol.TradePeer;
import haveno.network.p2p.SendDirectMessageListener; import haveno.network.p2p.SendDirectMessageListener;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.UUID; import java.util.UUID;
@Slf4j @Slf4j
public class ProcessSignContractResponse extends TradeTask { public class SendDepositRequest extends TradeTask {
@SuppressWarnings({"unused"}) @SuppressWarnings({"unused"})
public ProcessSignContractResponse(TaskRunner taskHandler, Trade trade) { public SendDepositRequest(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade); super(taskHandler, trade);
} }
@ -107,7 +109,11 @@ public class ProcessSignContractResponse extends TradeTask {
} }
}); });
} else { } else {
log.info("Waiting for another contract signature to send deposit request"); List<String> awaitingSignaturesFrom = new ArrayList<>();
if (processModel.getArbitrator().getContractSignature() == null) awaitingSignaturesFrom.add("arbitrator");
if (processModel.getMaker().getContractSignature() == null) awaitingSignaturesFrom.add("maker");
if (processModel.getTaker().getContractSignature() == null) awaitingSignaturesFrom.add("taker");
log.info("Waiting for contract signature from {} to send deposit request", awaitingSignaturesFrom);
complete(); // does not yet have needed signatures complete(); // does not yet have needed signatures
} }
} catch (Throwable t) { } catch (Throwable t) {

View File

@ -23,6 +23,8 @@ import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade; import haveno.core.trade.Trade;
import haveno.core.trade.protocol.TradeProtocol; import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.xmr.model.XmrAddressEntry; import haveno.core.xmr.model.XmrAddressEntry;
import haveno.core.xmr.wallet.XmrWalletService;
import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroOutput; import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
@ -30,6 +32,7 @@ import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@Slf4j
public class TakerReserveTradeFunds extends TradeTask { public class TakerReserveTradeFunds extends TradeTask {
public TakerReserveTradeFunds(TaskRunner taskHandler, Trade trade) { public TakerReserveTradeFunds(TaskRunner taskHandler, Trade trade) {
@ -42,28 +45,49 @@ public class TakerReserveTradeFunds extends TradeTask {
runInterceptHook(); runInterceptHook();
// create reserve tx // create reserve tx
MoneroTxWallet reserveTx = null;
synchronized (XmrWalletService.WALLET_LOCK) {
// check for timeout
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId());
// collect relevant info
BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct()); BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct());
BigInteger takerFee = trade.getTakerFee(); BigInteger takerFee = trade.getTakerFee();
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO; BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO;
BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getSellerSecurityDepositBeforeMiningFee() : trade.getBuyerSecurityDepositBeforeMiningFee(); BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getSellerSecurityDepositBeforeMiningFee() : trade.getBuyerSecurityDepositBeforeMiningFee();
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString(); String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
// check if trade still exists // attempt creating reserve tx
if (!processModel.getTradeManager().hasOpenTrade(trade)) { synchronized (HavenoUtils.getWalletFunctionLock()) {
throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getId()); for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
try {
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
} catch (Exception e) {
log.warn("Error creating reserve tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
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;
}
}
// reset protocol timeout
trade.getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
// collect reserved key images // collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>(); List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex()); for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// reset protocol timeout // update trade state
trade.getProtocol().startTimeout(TradeProtocol.TRADE_TIMEOUT_SECONDS); trade.getTaker().setReserveTxKeyImages(reservedKeyImages);
}
// save process state // save process state
processModel.setReserveTx(reserveTx); processModel.setReserveTx(reserveTx);
processModel.getTaker().setReserveTxKeyImages(reservedKeyImages);
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();
trade.addInitProgressStep(); trade.addInitProgressStep();
complete(); complete();
@ -74,4 +98,8 @@ public class TakerReserveTradeFunds extends TradeTask {
failed(t); failed(t);
} }
} }
private boolean isTimedOut() {
return !processModel.getTradeManager().hasOpenTrade(trade);
}
} }

View File

@ -22,7 +22,6 @@ import com.google.common.util.concurrent.Service.State;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.name.Named; import com.google.inject.name.Named;
import common.utils.GenUtils;
import common.utils.JsonUtils; import common.utils.JsonUtils;
import haveno.common.ThreadUtils; import haveno.common.ThreadUtils;
import haveno.common.UserThread; import haveno.common.UserThread;
@ -33,8 +32,6 @@ import haveno.common.util.Utilities;
import haveno.core.api.AccountServiceListener; import haveno.core.api.AccountServiceListener;
import haveno.core.api.CoreAccountService; import haveno.core.api.CoreAccountService;
import haveno.core.api.XmrConnectionService; import haveno.core.api.XmrConnectionService;
import haveno.core.offer.Offer;
import haveno.core.offer.OfferDirection;
import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOffer;
import haveno.core.trade.BuyerTrade; import haveno.core.trade.BuyerTrade;
import haveno.core.trade.HavenoUtils; import haveno.core.trade.HavenoUtils;
@ -131,10 +128,10 @@ public class XmrWalletService {
private static final int NUM_MAX_WALLET_BACKUPS = 1; private static final int NUM_MAX_WALLET_BACKUPS = 1;
private static final int MONERO_LOG_LEVEL = -1; // monero library log level, -1 to disable private static final int MONERO_LOG_LEVEL = -1; // monero library log level, -1 to disable
private static final int MAX_SYNC_ATTEMPTS = 3; private static final int MAX_SYNC_ATTEMPTS = 3;
private static final boolean PRINT_STACK_TRACE = false; private static final boolean PRINT_RPC_STACK_TRACE = false;
private static final String THREAD_ID = XmrWalletService.class.getSimpleName(); private static final String THREAD_ID = XmrWalletService.class.getSimpleName();
private static final long SHUTDOWN_TIMEOUT_MS = 60000; private static final long SHUTDOWN_TIMEOUT_MS = 60000;
private static final long NUM_BLOCKS_BEHIND_WARNING = 10; private static final long NUM_BLOCKS_BEHIND_WARNING = 5;
private final User user; private final User user;
private final Preferences preferences; private final Preferences preferences;
@ -155,7 +152,7 @@ public class XmrWalletService {
private ChangeListener<? super Number> walletInitListener; private ChangeListener<? super Number> walletInitListener;
private TradeManager tradeManager; private TradeManager tradeManager;
private MoneroWallet wallet; private MoneroWallet wallet;
private Object walletLock = new Object(); public static final Object WALLET_LOCK = new Object();
private boolean wasWalletSynced = false; private boolean wasWalletSynced = false;
private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>(); private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
private boolean isClosingWallet = false; private boolean isClosingWallet = false;
@ -374,11 +371,21 @@ public class XmrWalletService {
return useNativeXmrWallet && MoneroUtils.isNativeLibraryLoaded(); return useNativeXmrWallet && MoneroUtils.isNativeLibraryLoaded();
} }
public MoneroSyncResult syncWallet() {
MoneroSyncResult result = syncWallet(wallet);
walletHeight.set(wallet.getHeight());
return result;
}
/** /**
* Sync the given wallet in a thread pool with other wallets. * Sync the given wallet in a thread pool with other wallets.
*/ */
public MoneroSyncResult syncWallet(MoneroWallet wallet) { public MoneroSyncResult syncWallet(MoneroWallet wallet) {
Callable<MoneroSyncResult> task = () -> wallet.sync(); Callable<MoneroSyncResult> task = () -> {
synchronized (HavenoUtils.getDaemonLock()) {
return wallet.sync();
}
};
Future<MoneroSyncResult> future = syncWalletThreadPool.submit(task); Future<MoneroSyncResult> future = syncWalletThreadPool.submit(task);
try { try {
return future.get(); return future.get();
@ -448,24 +455,26 @@ public class XmrWalletService {
if (name.contains(File.separator)) throw new IllegalArgumentException("Path not expected: " + name); if (name.contains(File.separator)) throw new IllegalArgumentException("Path not expected: " + name);
} }
public MoneroTxWallet createTx(MoneroTxConfig txConfig) {
synchronized (WALLET_LOCK) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
return wallet.createTx(txConfig);
}
}
}
public MoneroTxWallet createTx(List<MoneroDestination> destinations) { public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
synchronized (walletLock) { MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));;
try {
MoneroTxWallet tx = wallet.createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));
//printTxs("XmrWalletService.createTx", tx); //printTxs("XmrWalletService.createTx", tx);
requestSaveMainWallet(); requestSaveMainWallet();
return tx; return tx;
} catch (Exception e) {
throw e;
}
}
} }
/** /**
* Thaw all outputs not reserved for a trade. * Thaw all outputs not reserved for a trade.
*/ */
public void thawUnreservedOutputs() { public void thawUnreservedOutputs() {
synchronized (walletLock) { synchronized (WALLET_LOCK) {
// collect reserved outputs // collect reserved outputs
Set<String> reservedKeyImages = new HashSet<String>(); Set<String> reservedKeyImages = new HashSet<String>();
@ -505,26 +514,25 @@ public class XmrWalletService {
* @param keyImages the key images to freeze * @param keyImages the key images to freeze
*/ */
public void freezeOutputs(Collection<String> keyImages) { public void freezeOutputs(Collection<String> keyImages) {
synchronized (walletLock) { synchronized (WALLET_LOCK) {
for (String keyImage : keyImages) wallet.freezeOutput(keyImage); for (String keyImage : keyImages) wallet.freezeOutput(keyImage);
cacheWalletInfo();
requestSaveMainWallet(); requestSaveMainWallet();
doPollWallet(false);
} }
updateBalanceListeners(); // TODO (monero-java): balance listeners not notified on freeze/thaw output
} }
/** /**
* Thaw the given outputs with a lock on the wallet. * Thaw the given outputs with a lock on the wallet.
* *
* @param keyImages the key images to thaw * @param keyImages the key images to thaw (ignored if null or empty)
*/ */
public void thawOutputs(Collection<String> keyImages) { public void thawOutputs(Collection<String> keyImages) {
synchronized (walletLock) { if (keyImages == null || keyImages.isEmpty()) return;
synchronized (WALLET_LOCK) {
for (String keyImage : keyImages) wallet.thawOutput(keyImage); for (String keyImage : keyImages) wallet.thawOutput(keyImage);
cacheWalletInfo();
requestSaveMainWallet(); requestSaveMainWallet();
doPollWallet(false);
} }
updateBalanceListeners(); // TODO (monero-java): balance listeners not notified on freeze/thaw output
} }
private List<Integer> getSubaddressesWithExactInput(BigInteger amount) { private List<Integer> getSubaddressesWithExactInput(BigInteger amount) {
@ -542,40 +550,6 @@ public class XmrWalletService {
return new ArrayList<Integer>(subaddressIndices); return new ArrayList<Integer>(subaddressIndices);
} }
/**
* Create a reserve tx for an open offer and freeze its inputs.
*
* @param openOffer is the open offer to create a reserve tx for
*/
public MoneroTxWallet createReserveTx(OpenOffer openOffer) {
synchronized (walletLock) {
// collect offer data
Offer offer = openOffer.getOffer();
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 = getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
XmrAddressEntry fundingEntry = getAddressEntry(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING).orElse(null);
Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex();
// create reserve tx
MoneroTxWallet reserveTx = createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
// collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// save offer state
openOffer.setReserveTxHash(reserveTx.getHash());
openOffer.setReserveTxHex(reserveTx.getFullHex());
openOffer.setReserveTxKey(reserveTx.getKey());
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
return reserveTx;
}
}
/** /**
* Create the reserve tx and freeze its inputs. The full amount is returned * Create the reserve tx and freeze its inputs. The full amount is returned
* to the sender's payout address less the penalty and mining fees. * to the sender's payout address less the penalty and mining fees.
@ -587,15 +561,19 @@ public class XmrWalletService {
* @param returnAddress return address for reserved funds * @param returnAddress return address for reserved funds
* @param reserveExactAmount specifies to reserve the exact input amount * @param reserveExactAmount specifies to reserve the exact input amount
* @param preferredSubaddressIndex preferred source subaddress to spend from (optional) * @param preferredSubaddressIndex preferred source subaddress to spend from (optional)
* @return a transaction to reserve a trade * @return the reserve tx
*/ */
public MoneroTxWallet createReserveTx(BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String returnAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) { public MoneroTxWallet createReserveTx(BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String returnAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
synchronized (WALLET_LOCK) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress); log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress);
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
MoneroTxWallet reserveTx = createTradeTx(penaltyFee, tradeFee, sendAmount, securityDeposit, returnAddress, reserveExactAmount, preferredSubaddressIndex); MoneroTxWallet reserveTx = createTradeTx(penaltyFee, tradeFee, sendAmount, securityDeposit, returnAddress, reserveExactAmount, preferredSubaddressIndex);
log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time); log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time);
return reserveTx; return reserveTx;
} }
}
}
/** /**
* Create the multisig deposit tx and freeze its inputs. * Create the multisig deposit tx and freeze its inputs.
@ -606,28 +584,23 @@ public class XmrWalletService {
* @return MoneroTxWallet the multisig deposit tx * @return MoneroTxWallet the multisig deposit tx
*/ */
public MoneroTxWallet createDepositTx(Trade trade, boolean reserveExactAmount, Integer preferredSubaddressIndex) { public MoneroTxWallet createDepositTx(Trade trade, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
synchronized (walletLock) { synchronized (WALLET_LOCK) {
synchronized (HavenoUtils.getWalletFunctionLock()) {
// thaw reserved outputs
if (trade.getSelf().getReserveTxKeyImages() != null) {
thawOutputs(trade.getSelf().getReserveTxKeyImages());
}
// create deposit tx
String multisigAddress = trade.getProcessModel().getMultisigAddress(); String multisigAddress = trade.getProcessModel().getMultisigAddress();
BigInteger tradeFee = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee(); BigInteger tradeFee = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee();
BigInteger sendAmount = trade instanceof BuyerTrade ? BigInteger.ZERO : trade.getAmount(); BigInteger sendAmount = trade instanceof BuyerTrade ? BigInteger.ZERO : trade.getAmount();
BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee(); BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
log.info("Creating deposit tx with multisig address={}", multisigAddress); log.info("Creating deposit tx for trade {} {} with multisig address={}", trade.getClass().getSimpleName(), trade.getShortId(), multisigAddress);
MoneroTxWallet depositTx = createTradeTx(null, tradeFee, sendAmount, securityDeposit, multisigAddress, reserveExactAmount, preferredSubaddressIndex); MoneroTxWallet depositTx = createTradeTx(null, tradeFee, sendAmount, securityDeposit, multisigAddress, reserveExactAmount, preferredSubaddressIndex);
log.info("Done creating deposit tx for trade {} {} in {} ms", trade.getClass().getSimpleName(), trade.getId(), System.currentTimeMillis() - time); log.info("Done creating deposit tx for trade {} {} in {} ms", trade.getClass().getSimpleName(), trade.getShortId(), System.currentTimeMillis() - time);
return depositTx; return depositTx;
} }
} }
}
private MoneroTxWallet createTradeTx(BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, boolean reserveExactAmount, Integer preferredSubaddressIndex) { private MoneroTxWallet createTradeTx(BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
synchronized (walletLock) { synchronized (WALLET_LOCK) {
MoneroWallet wallet = getWallet(); MoneroWallet wallet = getWallet();
// create a list of subaddresses to attempt spending from in preferred order // create a list of subaddresses to attempt spending from in preferred order
@ -675,7 +648,7 @@ public class XmrWalletService {
.setSubtractFeeFrom(0) // pay fee from transfer amount .setSubtractFeeFrom(0) // pay fee from transfer amount
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY); .setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY);
if (!BigInteger.valueOf(0).equals(feeAmount)) txConfig.addDestination(HavenoUtils.getTradeFeeAddress(), feeAmount); if (!BigInteger.valueOf(0).equals(feeAmount)) txConfig.addDestination(HavenoUtils.getTradeFeeAddress(), feeAmount);
MoneroTxWallet tradeTx = wallet.createTx(txConfig); MoneroTxWallet tradeTx = createTx(txConfig);
// freeze inputs // freeze inputs
List<String> keyImages = new ArrayList<String>(); List<String> keyImages = new ArrayList<String>();
@ -872,7 +845,7 @@ public class XmrWalletService {
Runnable shutDownTask = () -> { Runnable shutDownTask = () -> {
// remove listeners // remove listeners
synchronized (walletLock) { synchronized (WALLET_LOCK) {
if (wallet != null) { if (wallet != null) {
for (MoneroWalletListenerI listener : new HashSet<>(wallet.getListeners())) { for (MoneroWalletListenerI listener : new HashSet<>(wallet.getListeners())) {
wallet.removeListener(listener); wallet.removeListener(listener);
@ -1174,7 +1147,7 @@ public class XmrWalletService {
} }
public List<MoneroTxWallet> getTxs() { public List<MoneroTxWallet> getTxs() {
return getTxs(new MoneroTxQuery()); return getTxs(new MoneroTxQuery().setIncludeOutputs(true));
} }
public List<MoneroTxWallet> getTxs(MoneroTxQuery query) { public List<MoneroTxWallet> getTxs(MoneroTxQuery query) {
@ -1242,7 +1215,7 @@ public class XmrWalletService {
// force restart main wallet if connection changed before synced // force restart main wallet if connection changed before synced
if (!wasWalletSynced) { if (!wasWalletSynced) {
if (!Boolean.TRUE.equals(connection.isConnected())) return; if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return;
ThreadUtils.submitToPool(() -> { ThreadUtils.submitToPool(() -> {
log.warn("Force restarting main wallet because connection changed before inital sync"); log.warn("Force restarting main wallet because connection changed before inital sync");
forceRestartMainWallet(); forceRestartMainWallet();
@ -1275,7 +1248,7 @@ public class XmrWalletService {
private void initMainWalletIfConnected() { private void initMainWalletIfConnected() {
ThreadUtils.execute(() -> { ThreadUtils.execute(() -> {
synchronized (walletLock) { synchronized (WALLET_LOCK) {
if (wallet == null && xmrConnectionService.downloadPercentageProperty().get() == 1 && !isShutDownStarted) { if (wallet == null && xmrConnectionService.downloadPercentageProperty().get() == 1 && !isShutDownStarted) {
maybeInitMainWallet(true); maybeInitMainWallet(true);
if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener); if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener);
@ -1295,7 +1268,7 @@ public class XmrWalletService {
} }
private void maybeInitMainWallet(boolean sync, int numAttempts) { private void maybeInitMainWallet(boolean sync, int numAttempts) {
synchronized (walletLock) { synchronized (WALLET_LOCK) {
if (isShutDownStarted) return; if (isShutDownStarted) return;
// open or create wallet main wallet // open or create wallet main wallet
@ -1304,7 +1277,7 @@ public class XmrWalletService {
log.info("Initializing main wallet with monerod=" + (daemon == null ? "null" : daemon.getRpcConnection().getUri())); log.info("Initializing main wallet with monerod=" + (daemon == null ? "null" : daemon.getRpcConnection().getUri()));
if (MoneroUtils.walletExists(xmrWalletFile.getPath())) { if (MoneroUtils.walletExists(xmrWalletFile.getPath())) {
wallet = openWallet(MONERO_WALLET_NAME, rpcBindPort, isProxyApplied(wasWalletSynced)); wallet = openWallet(MONERO_WALLET_NAME, rpcBindPort, isProxyApplied(wasWalletSynced));
} else if (xmrConnectionService.getConnection() != null && Boolean.TRUE.equals(xmrConnectionService.getConnection().isConnected())) { } else if (Boolean.TRUE.equals(xmrConnectionService.isConnected())) {
wallet = createWallet(MONERO_WALLET_NAME, rpcBindPort); wallet = createWallet(MONERO_WALLET_NAME, rpcBindPort);
// set wallet creation date to yesterday to guarantee complete restore // set wallet creation date to yesterday to guarantee complete restore
@ -1393,7 +1366,7 @@ public class XmrWalletService {
// get sync notifications from native wallet // get sync notifications from native wallet
if (wallet instanceof MoneroWalletFull) { if (wallet instanceof MoneroWalletFull) {
if (runReconnectTestOnStartup) GenUtils.waitFor(1000); // delay sync to test if (runReconnectTestOnStartup) HavenoUtils.waitFor(1000); // delay sync to test
wallet.sync(new MoneroWalletListener() { wallet.sync(new MoneroWalletListener() {
@Override @Override
public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) { public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) {
@ -1419,11 +1392,6 @@ public class XmrWalletService {
if (height < xmrConnectionService.getTargetHeight()) updateSyncProgress(height); if (height < xmrConnectionService.getTargetHeight()) updateSyncProgress(height);
else { else {
syncWithProgressLooper.stop(); syncWithProgressLooper.stop();
try {
doPollWallet(true);
} catch (Exception e) {
e.printStackTrace();
}
wasWalletSynced = true; wasWalletSynced = true;
updateSyncProgress(height); updateSyncProgress(height);
syncWithProgressLatch.countDown(); syncWithProgressLatch.countDown();
@ -1465,19 +1433,19 @@ public class XmrWalletService {
private MoneroWalletFull createWalletFull(MoneroWalletConfig config) { private MoneroWalletFull createWalletFull(MoneroWalletConfig config) {
// must be connected to daemon // must be connected to daemon
MoneroRpcConnection connection = xmrConnectionService.getConnection(); if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet");
if (connection == null || !Boolean.TRUE.equals(connection.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet");
// create wallet // create wallet
MoneroWalletFull walletFull = null; MoneroWalletFull walletFull = null;
try { try {
// create wallet // create wallet
MoneroRpcConnection connection = xmrConnectionService.getConnection();
log.info("Creating full wallet " + config.getPath() + " connected to monerod=" + connection.getUri()); log.info("Creating full wallet " + config.getPath() + " connected to monerod=" + connection.getUri());
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
config.setServer(connection); config.setServer(connection);
walletFull = MoneroWalletFull.createWallet(config); walletFull = MoneroWalletFull.createWallet(config);
walletFull.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE); walletFull.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
log.info("Done creating full wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms"); log.info("Done creating full wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms");
return walletFull; return walletFull;
} catch (Exception e) { } catch (Exception e) {
@ -1499,7 +1467,7 @@ public class XmrWalletService {
config.setNetworkType(getMoneroNetworkType()); config.setNetworkType(getMoneroNetworkType());
config.setServer(connection); config.setServer(connection);
walletFull = MoneroWalletFull.openWallet(config); walletFull = MoneroWalletFull.openWallet(config);
if (walletFull.getDaemonConnection() != null) walletFull.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE); if (walletFull.getDaemonConnection() != null) walletFull.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
log.info("Done opening full wallet " + config.getPath()); log.info("Done opening full wallet " + config.getPath());
return walletFull; return walletFull;
} catch (Exception e) { } catch (Exception e) {
@ -1512,8 +1480,7 @@ public class XmrWalletService {
private MoneroWalletRpc createWalletRpc(MoneroWalletConfig config, Integer port) { private MoneroWalletRpc createWalletRpc(MoneroWalletConfig config, Integer port) {
// must be connected to daemon // must be connected to daemon
MoneroRpcConnection connection = xmrConnectionService.getConnection(); if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet");
if (connection == null || !Boolean.TRUE.equals(connection.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet");
// create wallet // create wallet
MoneroWalletRpc walletRpc = null; MoneroWalletRpc walletRpc = null;
@ -1521,17 +1488,18 @@ public class XmrWalletService {
// start monero-wallet-rpc instance // start monero-wallet-rpc instance
walletRpc = startWalletRpcInstance(port, isProxyApplied(false)); walletRpc = startWalletRpcInstance(port, isProxyApplied(false));
walletRpc.getRpcConnection().setPrintStackTrace(PRINT_STACK_TRACE); walletRpc.getRpcConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
// prevent wallet rpc from syncing // prevent wallet rpc from syncing
walletRpc.stopSyncing(); walletRpc.stopSyncing();
// create wallet // create wallet
MoneroRpcConnection connection = xmrConnectionService.getConnection();
log.info("Creating RPC wallet " + config.getPath() + " connected to monerod=" + connection.getUri()); log.info("Creating RPC wallet " + config.getPath() + " connected to monerod=" + connection.getUri());
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
config.setServer(connection); config.setServer(connection);
walletRpc.createWallet(config); walletRpc.createWallet(config);
walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE); walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
log.info("Done creating RPC wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms"); log.info("Done creating RPC wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms");
return walletRpc; return walletRpc;
} catch (Exception e) { } catch (Exception e) {
@ -1547,7 +1515,7 @@ public class XmrWalletService {
// start monero-wallet-rpc instance // start monero-wallet-rpc instance
walletRpc = startWalletRpcInstance(port, applyProxyUri); walletRpc = startWalletRpcInstance(port, applyProxyUri);
walletRpc.getRpcConnection().setPrintStackTrace(PRINT_STACK_TRACE); walletRpc.getRpcConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
// prevent wallet rpc from syncing // prevent wallet rpc from syncing
walletRpc.stopSyncing(); walletRpc.stopSyncing();
@ -1560,7 +1528,7 @@ public class XmrWalletService {
log.info("Opening RPC wallet " + config.getPath() + " connected to daemon " + connection.getUri()); log.info("Opening RPC wallet " + config.getPath() + " connected to daemon " + connection.getUri());
config.setServer(connection); config.setServer(connection);
walletRpc.openWallet(config); walletRpc.openWallet(config);
if (walletRpc.getDaemonConnection() != null) walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE); if (walletRpc.getDaemonConnection() != null) walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
log.info("Done opening RPC wallet " + config.getPath()); log.info("Done opening RPC wallet " + config.getPath());
return walletRpc; return walletRpc;
} catch (Exception e) { } catch (Exception e) {
@ -1613,7 +1581,7 @@ public class XmrWalletService {
} }
private void onConnectionChanged(MoneroRpcConnection connection) { private void onConnectionChanged(MoneroRpcConnection connection) {
synchronized (walletLock) { synchronized (WALLET_LOCK) {
if (wallet == null || isShutDownStarted) return; if (wallet == null || isShutDownStarted) return;
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return; if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return;
String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri(); String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri();
@ -1634,7 +1602,7 @@ public class XmrWalletService {
// sync wallet on new thread // sync wallet on new thread
if (connection != null && !isShutDownStarted) { if (connection != null && !isShutDownStarted) {
wallet.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE); wallet.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
updatePollPeriod(); updatePollPeriod();
} }
@ -1673,7 +1641,7 @@ public class XmrWalletService {
private void closeMainWallet(boolean save) { private void closeMainWallet(boolean save) {
stopPolling(); stopPolling();
synchronized (walletLock) { synchronized (WALLET_LOCK) {
try { try {
if (wallet != null) { if (wallet != null) {
isClosingWallet = true; isClosingWallet = true;
@ -1697,13 +1665,13 @@ public class XmrWalletService {
private void forceRestartMainWallet() { private void forceRestartMainWallet() {
log.warn("Force restarting main wallet"); log.warn("Force restarting main wallet");
forceCloseMainWallet(); forceCloseMainWallet();
synchronized (walletLock) { synchronized (WALLET_LOCK) {
maybeInitMainWallet(true); maybeInitMainWallet(true);
} }
} }
private void startPolling() { private void startPolling() {
synchronized (walletLock) { synchronized (WALLET_LOCK) {
if (isShutDownStarted || isPollInProgress()) return; if (isShutDownStarted || isPollInProgress()) return;
log.info("Starting to poll main wallet"); log.info("Starting to poll main wallet");
updatePollPeriod(); updatePollPeriod();
@ -1733,7 +1701,7 @@ public class XmrWalletService {
} }
private void setPollPeriod(long pollPeriodMs) { private void setPollPeriod(long pollPeriodMs) {
synchronized (walletLock) { synchronized (WALLET_LOCK) {
if (this.isShutDownStarted) return; if (this.isShutDownStarted) return;
if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return; if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return;
this.pollPeriodMs = pollPeriodMs; this.pollPeriodMs = pollPeriodMs;
@ -1751,72 +1719,52 @@ public class XmrWalletService {
private void doPollWallet(boolean updateTxs) { private void doPollWallet(boolean updateTxs) {
synchronized (pollLock) { synchronized (pollLock) {
if (isShutDownStarted) return;
pollInProgress = true; pollInProgress = true;
try { try {
// log warning if wallet is too far behind daemon // switch to best connection if daemon is too far behind
MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo(); MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo();
if (lastInfo == null) { if (lastInfo == null) {
log.warn("Last daemon info is null"); log.warn("Last daemon info is null");
return; return;
} }
long walletHeight = wallet.getHeight(); if (wasWalletSynced && walletHeight.get() < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_WARNING && !Config.baseCurrencyNetwork().isTestnet()) {
if (wasWalletSynced && walletHeight < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_WARNING && !Config.baseCurrencyNetwork().isTestnet()) { log.warn("Updating connection because main wallet is {} blocks behind monerod, wallet height={}, monerod height={}", xmrConnectionService.getTargetHeight() - walletHeight.get(), walletHeight.get(), lastInfo.getHeight());
log.warn("Main wallet is {} blocks behind monerod, wallet height={}, monerod height={},", xmrConnectionService.getTargetHeight() - walletHeight, walletHeight, lastInfo.getHeight()); xmrConnectionService.switchToBestConnection();
} }
// sync wallet if behind daemon // sync wallet if behind daemon
if (wallet.getHeight() < xmrConnectionService.getTargetHeight()) wallet.sync(); if (walletHeight.get() < xmrConnectionService.getTargetHeight()) {
synchronized (WALLET_LOCK) { // avoid long sync from blocking other operations
syncWallet();
}
}
// fetch transactions from pool and store to cache // fetch transactions from pool and store to cache
// TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs? // TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs?
if (updateTxs) { if (updateTxs) {
synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations
synchronized (HavenoUtils.getDaemonLock()) {
try { try {
cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true)); cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true));
} catch (Exception e) { // fetch from pool can fail } catch (Exception e) { // fetch from pool can fail
log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage()); if (!isShutDownStarted) log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage());
}
}
} }
} }
// get basic wallet info // cache wallet info
long height = wallet.getHeight(); cacheWalletInfo();
BigInteger balance = wallet.getBalance();
BigInteger unlockedBalance = wallet.getUnlockedBalance();
cachedSubaddresses = wallet.getSubaddresses(0);
cachedOutputs = wallet.getOutputs();
// cache and notify changes
if (cachedHeight == null) {
cachedHeight = height;
cachedBalance = balance;
cachedAvailableBalance = unlockedBalance;
} else {
// notify listeners of new block
if (height != cachedHeight) {
cachedHeight = height;
onNewBlock(height);
}
// notify listeners of balance change
if (!balance.equals(cachedBalance) || !unlockedBalance.equals(cachedAvailableBalance)) {
cachedBalance = balance;
cachedAvailableBalance = unlockedBalance;
onBalancesChanged(balance, unlockedBalance);
}
}
} catch (Exception e) { } catch (Exception e) {
if (isShutDownStarted) return; if (wallet == null || isShutDownStarted) return;
boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused"); boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused");
if (isConnectionRefused && wallet != null) forceRestartMainWallet(); if (isConnectionRefused) forceRestartMainWallet();
else { else if (isWalletConnectedToDaemon()) {
boolean isWalletConnected = isWalletConnectedToDaemon();
if (!isWalletConnected) xmrConnectionService.checkConnection(); // check connection if wallet is not connected
if (wallet != null && isWalletConnected) {
log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection()); log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection());
//e.printStackTrace(); //e.printStackTrace();
} }
}
} finally { } finally {
pollInProgress = false; pollInProgress = false;
} }
@ -1824,7 +1772,7 @@ public class XmrWalletService {
} }
public boolean isWalletConnectedToDaemon() { public boolean isWalletConnectedToDaemon() {
synchronized (walletLock) { synchronized (WALLET_LOCK) {
try { try {
if (wallet == null) return false; if (wallet == null) return false;
return wallet.isConnectedToDaemon(); return wallet.isConnectedToDaemon();
@ -1841,6 +1789,33 @@ public class XmrWalletService {
}); });
} }
private void cacheWalletInfo() {
// get basic wallet info
long height = wallet.getHeight();
BigInteger balance = wallet.getBalance();
BigInteger unlockedBalance = wallet.getUnlockedBalance();
cachedSubaddresses = wallet.getSubaddresses(0);
cachedOutputs = wallet.getOutputs();
// cache and notify changes
if (cachedHeight == null) {
cachedHeight = height;
cachedBalance = balance;
cachedAvailableBalance = unlockedBalance;
onNewBlock(height);
onBalancesChanged(balance, unlockedBalance);
} else {
boolean heightChanged = height != cachedHeight;
boolean balancesChanged = !balance.equals(cachedBalance) || !unlockedBalance.equals(cachedAvailableBalance);
cachedHeight = height;
cachedBalance = balance;
cachedAvailableBalance = unlockedBalance;
if (heightChanged) onNewBlock(height);
if (balancesChanged) onBalancesChanged(balance, unlockedBalance);
}
}
private void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) { private void onBalancesChanged(BigInteger newBalance, BigInteger newUnlockedBalance) {
updateBalanceListeners(); updateBalanceListeners();
for (MoneroWalletListenerI listener : walletListeners) ThreadUtils.submitToPool(() -> listener.onBalancesChanged(newBalance, newUnlockedBalance)); for (MoneroWalletListenerI listener : walletListeners) ThreadUtils.submitToPool(() -> listener.onBalancesChanged(newBalance, newUnlockedBalance));

View File

@ -57,10 +57,10 @@ import haveno.proto.grpc.SetAutoSwitchReply;
import haveno.proto.grpc.SetAutoSwitchRequest; import haveno.proto.grpc.SetAutoSwitchRequest;
import haveno.proto.grpc.SetConnectionReply; import haveno.proto.grpc.SetConnectionReply;
import haveno.proto.grpc.SetConnectionRequest; import haveno.proto.grpc.SetConnectionRequest;
import haveno.proto.grpc.StartCheckingConnectionsReply; import haveno.proto.grpc.StartCheckingConnectionReply;
import haveno.proto.grpc.StartCheckingConnectionsRequest; import haveno.proto.grpc.StartCheckingConnectionRequest;
import haveno.proto.grpc.StopCheckingConnectionsReply; import haveno.proto.grpc.StopCheckingConnectionReply;
import haveno.proto.grpc.StopCheckingConnectionsRequest; import haveno.proto.grpc.StopCheckingConnectionRequest;
import haveno.proto.grpc.UrlConnection; import haveno.proto.grpc.UrlConnection;
import static haveno.proto.grpc.XmrConnectionsGrpc.XmrConnectionsImplBase; import static haveno.proto.grpc.XmrConnectionsGrpc.XmrConnectionsImplBase;
import static haveno.proto.grpc.XmrConnectionsGrpc.getAddConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getAddConnectionMethod;
@ -72,8 +72,8 @@ import static haveno.proto.grpc.XmrConnectionsGrpc.getGetConnectionsMethod;
import static haveno.proto.grpc.XmrConnectionsGrpc.getRemoveConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getRemoveConnectionMethod;
import static haveno.proto.grpc.XmrConnectionsGrpc.getSetAutoSwitchMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getSetAutoSwitchMethod;
import static haveno.proto.grpc.XmrConnectionsGrpc.getSetConnectionMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getSetConnectionMethod;
import static haveno.proto.grpc.XmrConnectionsGrpc.getStartCheckingConnectionsMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getStartCheckingConnectionMethod;
import static haveno.proto.grpc.XmrConnectionsGrpc.getStopCheckingConnectionsMethod; import static haveno.proto.grpc.XmrConnectionsGrpc.getStopCheckingConnectionMethod;
import io.grpc.ServerInterceptor; import io.grpc.ServerInterceptor;
import io.grpc.stub.StreamObserver; import io.grpc.stub.StreamObserver;
import java.net.MalformedURLException; import java.net.MalformedURLException;
@ -102,7 +102,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
public void addConnection(AddConnectionRequest request, public void addConnection(AddConnectionRequest request,
StreamObserver<AddConnectionReply> responseObserver) { StreamObserver<AddConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> { handleRequest(responseObserver, () -> {
coreApi.addMoneroConnection(toMoneroRpcConnection(request.getConnection())); coreApi.addXmrConnection(toMoneroRpcConnection(request.getConnection()));
return AddConnectionReply.newBuilder().build(); return AddConnectionReply.newBuilder().build();
}); });
} }
@ -111,7 +111,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
public void removeConnection(RemoveConnectionRequest request, public void removeConnection(RemoveConnectionRequest request,
StreamObserver<RemoveConnectionReply> responseObserver) { StreamObserver<RemoveConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> { handleRequest(responseObserver, () -> {
coreApi.removeMoneroConnection(validateUri(request.getUrl())); coreApi.removeXmrConnection(validateUri(request.getUrl()));
return RemoveConnectionReply.newBuilder().build(); return RemoveConnectionReply.newBuilder().build();
}); });
} }
@ -120,7 +120,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
public void getConnection(GetConnectionRequest request, public void getConnection(GetConnectionRequest request,
StreamObserver<GetConnectionReply> responseObserver) { StreamObserver<GetConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> { handleRequest(responseObserver, () -> {
UrlConnection replyConnection = toUrlConnection(coreApi.getMoneroConnection()); UrlConnection replyConnection = toUrlConnection(coreApi.getXmrConnection());
GetConnectionReply.Builder builder = GetConnectionReply.newBuilder(); GetConnectionReply.Builder builder = GetConnectionReply.newBuilder();
if (replyConnection != null) { if (replyConnection != null) {
builder.setConnection(replyConnection); builder.setConnection(replyConnection);
@ -145,10 +145,10 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
StreamObserver<SetConnectionReply> responseObserver) { StreamObserver<SetConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> { handleRequest(responseObserver, () -> {
if (request.getUrl() != null && !request.getUrl().isEmpty()) if (request.getUrl() != null && !request.getUrl().isEmpty())
coreApi.setMoneroConnection(validateUri(request.getUrl())); coreApi.setXmrConnection(validateUri(request.getUrl()));
else if (request.hasConnection()) else if (request.hasConnection())
coreApi.setMoneroConnection(toMoneroRpcConnection(request.getConnection())); coreApi.setXmrConnection(toMoneroRpcConnection(request.getConnection()));
else coreApi.setMoneroConnection((MoneroRpcConnection) null); // disconnect from client else coreApi.setXmrConnection((MoneroRpcConnection) null); // disconnect from client
return SetConnectionReply.newBuilder().build(); return SetConnectionReply.newBuilder().build();
}); });
} }
@ -157,7 +157,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
public void checkConnection(CheckConnectionRequest request, public void checkConnection(CheckConnectionRequest request,
StreamObserver<CheckConnectionReply> responseObserver) { StreamObserver<CheckConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> { handleRequest(responseObserver, () -> {
MoneroRpcConnection connection = coreApi.checkMoneroConnection(); MoneroRpcConnection connection = coreApi.checkXmrConnection();
UrlConnection replyConnection = toUrlConnection(connection); UrlConnection replyConnection = toUrlConnection(connection);
CheckConnectionReply.Builder builder = CheckConnectionReply.newBuilder(); CheckConnectionReply.Builder builder = CheckConnectionReply.newBuilder();
if (replyConnection != null) { if (replyConnection != null) {
@ -179,22 +179,22 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
} }
@Override @Override
public void startCheckingConnections(StartCheckingConnectionsRequest request, public void startCheckingConnection(StartCheckingConnectionRequest request,
StreamObserver<StartCheckingConnectionsReply> responseObserver) { StreamObserver<StartCheckingConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> { handleRequest(responseObserver, () -> {
int refreshMillis = request.getRefreshPeriod(); int refreshMillis = request.getRefreshPeriod();
Long refreshPeriod = refreshMillis == 0 ? null : (long) refreshMillis; Long refreshPeriod = refreshMillis == 0 ? null : (long) refreshMillis;
coreApi.startCheckingMoneroConnection(refreshPeriod); coreApi.startCheckingXmrConnection(refreshPeriod);
return StartCheckingConnectionsReply.newBuilder().build(); return StartCheckingConnectionReply.newBuilder().build();
}); });
} }
@Override @Override
public void stopCheckingConnections(StopCheckingConnectionsRequest request, public void stopCheckingConnection(StopCheckingConnectionRequest request,
StreamObserver<StopCheckingConnectionsReply> responseObserver) { StreamObserver<StopCheckingConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> { handleRequest(responseObserver, () -> {
coreApi.stopCheckingMoneroConnection(); coreApi.stopCheckingXmrConnection();
return StopCheckingConnectionsReply.newBuilder().build(); return StopCheckingConnectionReply.newBuilder().build();
}); });
} }
@ -202,7 +202,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
public void getBestAvailableConnection(GetBestAvailableConnectionRequest request, public void getBestAvailableConnection(GetBestAvailableConnectionRequest request,
StreamObserver<GetBestAvailableConnectionReply> responseObserver) { StreamObserver<GetBestAvailableConnectionReply> responseObserver) {
handleRequest(responseObserver, () -> { handleRequest(responseObserver, () -> {
MoneroRpcConnection connection = coreApi.getBestAvailableMoneroConnection(); MoneroRpcConnection connection = coreApi.getBestAvailableXmrConnection();
UrlConnection replyConnection = toUrlConnection(connection); UrlConnection replyConnection = toUrlConnection(connection);
GetBestAvailableConnectionReply.Builder builder = GetBestAvailableConnectionReply.newBuilder(); GetBestAvailableConnectionReply.Builder builder = GetBestAvailableConnectionReply.newBuilder();
if (replyConnection != null) { if (replyConnection != null) {
@ -216,7 +216,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
public void setAutoSwitch(SetAutoSwitchRequest request, public void setAutoSwitch(SetAutoSwitchRequest request,
StreamObserver<SetAutoSwitchReply> responseObserver) { StreamObserver<SetAutoSwitchReply> responseObserver) {
handleRequest(responseObserver, () -> { handleRequest(responseObserver, () -> {
coreApi.setMoneroConnectionAutoSwitch(request.getAutoSwitch()); coreApi.setXmrConnectionAutoSwitch(request.getAutoSwitch());
return SetAutoSwitchReply.newBuilder().build(); return SetAutoSwitchReply.newBuilder().build();
}); });
} }
@ -300,8 +300,8 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
put(getSetConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getSetConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getCheckConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getCheckConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getCheckConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getCheckConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getStartCheckingConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getStartCheckingConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getStopCheckingConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getStopCheckingConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getGetBestAvailableConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getGetBestAvailableConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
put(getSetAutoSwitchMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS)); put(getSetAutoSwitchMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
}} }}

View File

@ -91,6 +91,7 @@ import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.util.Callback; import javafx.util.Callback;
import monero.common.MoneroUtils;
import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroWalletListener; import monero.wallet.model.MoneroWalletListener;
import net.glxn.qrgen.QRCode; import net.glxn.qrgen.QRCode;
@ -365,7 +366,7 @@ public class DepositView extends ActivatableView<VBox, Void> {
@NotNull @NotNull
private String getPaymentUri() { private String getPaymentUri() {
return xmrWalletService.getWallet().getPaymentUri(new MoneroTxConfig() return MoneroUtils.getPaymentUri(new MoneroTxConfig()
.setAddress(addressTextField.getAddress()) .setAddress(addressTextField.getAddress())
.setAmount(HavenoUtils.coinToAtomicUnits(getAmount())) .setAmount(HavenoUtils.coinToAtomicUnits(getAmount()))
.setNote(paymentLabelString)); .setNote(paymentLabelString));

View File

@ -261,7 +261,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
// create tx // create tx
if (amount.compareTo(BigInteger.ZERO) <= 0) throw new RuntimeException(Res.get("portfolio.pending.step5_buyer.amountTooLow")); if (amount.compareTo(BigInteger.ZERO) <= 0) throw new RuntimeException(Res.get("portfolio.pending.step5_buyer.amountTooLow"));
MoneroTxWallet tx = xmrWalletService.getWallet().createTx(new MoneroTxConfig() MoneroTxWallet tx = xmrWalletService.createTx(new MoneroTxConfig()
.setAccountIndex(0) .setAccountIndex(0)
.setAmount(amount) .setAmount(amount)
.setAddress(withdrawToAddress) .setAddress(withdrawToAddress)

View File

@ -613,10 +613,12 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
if (offer.getState() == Offer.State.OFFER_FEE_RESERVED) errorMessage.set(errMessage + Res.get("createOffer.errorInfo")); if (offer.getState() == Offer.State.OFFER_FEE_RESERVED) errorMessage.set(errMessage + Res.get("createOffer.errorInfo"));
else errorMessage.set(errMessage); else errorMessage.set(errMessage);
UserThread.execute(() -> {
updateButtonDisableState(); updateButtonDisableState();
updateSpinnerInfo(); updateSpinnerInfo();
resultHandler.run(); resultHandler.run();
});
}); });
updateButtonDisableState(); updateButtonDisableState();

View File

@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.name.Named; import com.google.inject.name.Named;
import haveno.common.ClockWatcher; import haveno.common.ClockWatcher;
import haveno.common.UserThread;
import haveno.common.app.DevEnv; import haveno.common.app.DevEnv;
import haveno.core.account.witness.AccountAgeWitnessService; import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.network.MessageState; import haveno.core.network.MessageState;
@ -101,6 +102,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
@Getter @Getter
private final ObjectProperty<MessageState> messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); private final ObjectProperty<MessageState> messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
private Subscription tradeStateSubscription; private Subscription tradeStateSubscription;
private Subscription paymentAccountDecryptedSubscription;
private Subscription payoutStateSubscription; private Subscription payoutStateSubscription;
private Subscription messageStateSubscription; private Subscription messageStateSubscription;
@Getter @Getter
@ -146,6 +148,11 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
tradeStateSubscription = null; tradeStateSubscription = null;
} }
if (paymentAccountDecryptedSubscription != null) {
paymentAccountDecryptedSubscription.unsubscribe();
paymentAccountDecryptedSubscription = null;
}
if (payoutStateSubscription != null) { if (payoutStateSubscription != null) {
payoutStateSubscription.unsubscribe(); payoutStateSubscription.unsubscribe();
payoutStateSubscription = null; payoutStateSubscription = null;
@ -167,6 +174,10 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
buyerState.set(BuyerState.UNDEFINED); buyerState.set(BuyerState.UNDEFINED);
} }
if (paymentAccountDecryptedSubscription != null) {
paymentAccountDecryptedSubscription.unsubscribe();
}
if (payoutStateSubscription != null) { if (payoutStateSubscription != null) {
payoutStateSubscription.unsubscribe(); payoutStateSubscription.unsubscribe();
sellerState.set(SellerState.UNDEFINED); sellerState.set(SellerState.UNDEFINED);
@ -183,6 +194,9 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), state -> { tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), state -> {
onTradeStateChanged(state); onTradeStateChanged(state);
}); });
paymentAccountDecryptedSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentAccountDecryptedProperty(), decrypted -> {
refresh();
});
payoutStateSubscription = EasyBind.subscribe(trade.payoutStateProperty(), state -> { payoutStateSubscription = EasyBind.subscribe(trade.payoutStateProperty(), state -> {
onPayoutStateChanged(state); onPayoutStateChanged(state);
}); });
@ -191,6 +205,14 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
} }
} }
private void refresh() {
UserThread.execute(() -> {
sellerState.set(UNDEFINED);
buyerState.set(BuyerState.UNDEFINED);
onTradeStateChanged(trade.getState());
});
}
private void onMessageStateChanged(MessageState messageState) { private void onMessageStateChanged(MessageState messageState) {
messageStateProperty.set(messageState); messageStateProperty.set(messageState);
} }

View File

@ -221,7 +221,7 @@ public class BuyerStep2View extends TradeStepView {
PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload(); PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload();
String paymentMethodId = paymentAccountPayload != null ? paymentAccountPayload.getPaymentMethodId() : "<missing payment account payload>"; String paymentMethodId = paymentAccountPayload != null ? paymentAccountPayload.getPaymentMethodId() : "<pending>";
TitledGroupBg accountTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 4, TitledGroupBg accountTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 4,
Res.get("portfolio.pending.step2_buyer.startPaymentUsing", Res.get(paymentMethodId)), Res.get("portfolio.pending.step2_buyer.startPaymentUsing", Res.get(paymentMethodId)),
Layout.COMPACT_GROUP_DISTANCE); Layout.COMPACT_GROUP_DISTANCE);

View File

@ -93,6 +93,7 @@ import javafx.stage.StageStyle;
import javafx.util.Callback; import javafx.util.Callback;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroUtils;
import monero.daemon.model.MoneroTx; import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet; import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxConfig; import monero.wallet.model.MoneroTxConfig;
@ -686,7 +687,7 @@ public class GUIUtil {
} }
public static String getMoneroURI(String address, BigInteger amount, String label, MoneroWallet wallet) { public static String getMoneroURI(String address, BigInteger amount, String label, MoneroWallet wallet) {
return wallet.getPaymentUri(new MoneroTxConfig() return MoneroUtils.getPaymentUri(new MoneroTxConfig()
.setAddress(address) .setAddress(address)
.setAmount(amount) .setAmount(amount)
.setNote(label)); .setNote(label));

View File

@ -315,9 +315,9 @@ service XmrConnections {
} }
rpc CheckConnections(CheckConnectionsRequest) returns (CheckConnectionsReply) { rpc CheckConnections(CheckConnectionsRequest) returns (CheckConnectionsReply) {
} }
rpc StartCheckingConnections(StartCheckingConnectionsRequest) returns (StartCheckingConnectionsReply) { rpc StartCheckingConnection(StartCheckingConnectionRequest) returns (StartCheckingConnectionReply) {
} }
rpc StopCheckingConnections(StopCheckingConnectionsRequest) returns (StopCheckingConnectionsReply) { rpc StopCheckingConnection(StopCheckingConnectionRequest) returns (StopCheckingConnectionReply) {
} }
rpc GetBestAvailableConnection(GetBestAvailableConnectionRequest) returns (GetBestAvailableConnectionReply) { rpc GetBestAvailableConnection(GetBestAvailableConnectionRequest) returns (GetBestAvailableConnectionReply) {
} }
@ -388,15 +388,15 @@ message CheckConnectionsReply {
repeated UrlConnection connections = 1; repeated UrlConnection connections = 1;
} }
message StartCheckingConnectionsRequest { message StartCheckingConnectionRequest {
int32 refresh_period = 1; // milliseconds int32 refresh_period = 1; // milliseconds
} }
message StartCheckingConnectionsReply {} message StartCheckingConnectionReply {}
message StopCheckingConnectionsRequest {} message StopCheckingConnectionRequest {}
message StopCheckingConnectionsReply {} message StopCheckingConnectionReply {}
message GetBestAvailableConnectionRequest {} message GetBestAvailableConnectionRequest {}