mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-12-22 11:39:29 +00:00
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:
parent
f519ac12a5
commit
e63141279c
36 changed files with 799 additions and 568 deletions
|
@ -199,15 +199,15 @@ public class CoreApi {
|
|||
// Monero Connections
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void addMoneroConnection(MoneroRpcConnection connection) {
|
||||
public void addXmrConnection(MoneroRpcConnection connection) {
|
||||
xmrConnectionService.addConnection(connection);
|
||||
}
|
||||
|
||||
public void removeMoneroConnection(String connectionUri) {
|
||||
public void removeXmrConnection(String connectionUri) {
|
||||
xmrConnectionService.removeConnection(connectionUri);
|
||||
}
|
||||
|
||||
public MoneroRpcConnection getMoneroConnection() {
|
||||
public MoneroRpcConnection getXmrConnection() {
|
||||
return xmrConnectionService.getConnection();
|
||||
}
|
||||
|
||||
|
@ -215,15 +215,15 @@ public class CoreApi {
|
|||
return xmrConnectionService.getConnections();
|
||||
}
|
||||
|
||||
public void setMoneroConnection(String connectionUri) {
|
||||
public void setXmrConnection(String connectionUri) {
|
||||
xmrConnectionService.setConnection(connectionUri);
|
||||
}
|
||||
|
||||
public void setMoneroConnection(MoneroRpcConnection connection) {
|
||||
public void setXmrConnection(MoneroRpcConnection connection) {
|
||||
xmrConnectionService.setConnection(connection);
|
||||
}
|
||||
|
||||
public MoneroRpcConnection checkMoneroConnection() {
|
||||
public MoneroRpcConnection checkXmrConnection() {
|
||||
return xmrConnectionService.checkConnection();
|
||||
}
|
||||
|
||||
|
@ -231,19 +231,19 @@ public class CoreApi {
|
|||
return xmrConnectionService.checkConnections();
|
||||
}
|
||||
|
||||
public void startCheckingMoneroConnection(Long refreshPeriod) {
|
||||
public void startCheckingXmrConnection(Long refreshPeriod) {
|
||||
xmrConnectionService.startCheckingConnection(refreshPeriod);
|
||||
}
|
||||
|
||||
public void stopCheckingMoneroConnection() {
|
||||
public void stopCheckingXmrConnection() {
|
||||
xmrConnectionService.stopCheckingConnection();
|
||||
}
|
||||
|
||||
public MoneroRpcConnection getBestAvailableMoneroConnection() {
|
||||
public MoneroRpcConnection getBestAvailableXmrConnection() {
|
||||
return xmrConnectionService.getBestAvailableConnection();
|
||||
}
|
||||
|
||||
public void setMoneroConnectionAutoSwitch(boolean autoSwitch) {
|
||||
public void setXmrConnectionAutoSwitch(boolean autoSwitch) {
|
||||
xmrConnectionService.setAutoSwitch(autoSwitch);
|
||||
}
|
||||
|
||||
|
|
|
@ -90,6 +90,7 @@ public final class XmrConnectionService {
|
|||
private boolean isInitialized;
|
||||
private boolean pollInProgress;
|
||||
private MoneroDaemonRpc daemon;
|
||||
private Boolean isConnected = false;
|
||||
@Getter
|
||||
private MoneroDaemonInfo lastInfo;
|
||||
private Long syncStartHeight = null;
|
||||
|
@ -148,7 +149,6 @@ public final class XmrConnectionService {
|
|||
isInitialized = false;
|
||||
synchronized (lock) {
|
||||
if (daemonPollLooper != null) daemonPollLooper.stop();
|
||||
connectionManager.stopPolling();
|
||||
daemon = null;
|
||||
}
|
||||
}
|
||||
|
@ -171,7 +171,7 @@ public final class XmrConnectionService {
|
|||
}
|
||||
|
||||
public Boolean isConnected() {
|
||||
return connectionManager.isConnected();
|
||||
return isConnected;
|
||||
}
|
||||
|
||||
public void addConnection(MoneroRpcConnection connection) {
|
||||
|
@ -196,6 +196,12 @@ public final class XmrConnectionService {
|
|||
return connectionManager.getConnections();
|
||||
}
|
||||
|
||||
public void switchToBestConnection() {
|
||||
if (isFixedConnection() || !connectionManager.getAutoSwitch()) return;
|
||||
MoneroRpcConnection bestConnection = getBestAvailableConnection();
|
||||
if (bestConnection != null) setConnection(bestConnection);
|
||||
}
|
||||
|
||||
public void setConnection(String connectionUri) {
|
||||
accountService.checkAccountOpen();
|
||||
connectionManager.setConnection(connectionUri); // listener will update connection list
|
||||
|
@ -226,8 +232,8 @@ public final class XmrConnectionService {
|
|||
|
||||
public void stopCheckingConnection() {
|
||||
accountService.checkAccountOpen();
|
||||
connectionManager.stopPolling();
|
||||
connectionList.setRefreshPeriod(-1L);
|
||||
updatePolling();
|
||||
}
|
||||
|
||||
public MoneroRpcConnection getBestAvailableConnection() {
|
||||
|
@ -472,8 +478,6 @@ public final class XmrConnectionService {
|
|||
if (!isFixedConnection() && (connectionManager.getConnection() == null || connectionManager.getAutoSwitch())) {
|
||||
MoneroRpcConnection bestConnection = getBestAvailableConnection();
|
||||
if (bestConnection != null) setConnection(bestConnection);
|
||||
} else {
|
||||
checkConnection();
|
||||
}
|
||||
} else if (!isInitialized) {
|
||||
|
||||
|
@ -485,19 +489,11 @@ public final class XmrConnectionService {
|
|||
|
||||
// start local node if applicable
|
||||
maybeStartLocalNode();
|
||||
|
||||
// update connection
|
||||
checkConnection();
|
||||
}
|
||||
|
||||
// register connection listener
|
||||
connectionManager.addListener(this::onConnectionChanged);
|
||||
|
||||
// start polling after delay
|
||||
UserThread.runAfter(() -> {
|
||||
if (!isShutDownStarted) connectionManager.startPolling(getRefreshPeriodMs() * 2);
|
||||
}, getDefaultRefreshPeriodMs() * 2 / 1000);
|
||||
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
|
@ -524,7 +520,6 @@ public final class XmrConnectionService {
|
|||
|
||||
private void onConnectionChanged(MoneroRpcConnection currentConnection) {
|
||||
if (isShutDownStarted) return;
|
||||
log.info("XmrConnectionService.onConnectionChanged() uri={}, connected={}", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : currentConnection.isConnected());
|
||||
if (currentConnection == null) {
|
||||
log.warn("Setting daemon connection to null");
|
||||
Thread.dumpStack();
|
||||
|
@ -532,9 +527,11 @@ public final class XmrConnectionService {
|
|||
synchronized (lock) {
|
||||
if (currentConnection == null) {
|
||||
daemon = null;
|
||||
isConnected = false;
|
||||
connectionList.setCurrentConnectionUri(null);
|
||||
} else {
|
||||
daemon = new MoneroDaemonRpc(currentConnection);
|
||||
isConnected = currentConnection.isConnected();
|
||||
connectionList.removeConnection(currentConnection.getUri());
|
||||
connectionList.addConnection(currentConnection);
|
||||
connectionList.setCurrentConnectionUri(currentConnection.getUri());
|
||||
|
@ -546,9 +543,13 @@ public final class XmrConnectionService {
|
|||
numUpdates.set(numUpdates.get() + 1);
|
||||
});
|
||||
}
|
||||
updatePolling();
|
||||
|
||||
// update polling
|
||||
doPollDaemon();
|
||||
UserThread.runAfter(() -> updatePolling(), getRefreshPeriodMs() / 1000);
|
||||
|
||||
// notify listeners in parallel
|
||||
log.info("XmrConnectionService.onConnectionChanged() uri={}, connected={}", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : isConnected);
|
||||
synchronized (listenerLock) {
|
||||
for (MoneroConnectionManagerListener listener : listeners) {
|
||||
ThreadUtils.submitToPool(() -> listener.onConnectionChanged(currentConnection));
|
||||
|
@ -557,18 +558,14 @@ public final class XmrConnectionService {
|
|||
}
|
||||
|
||||
private void updatePolling() {
|
||||
new Thread(() -> {
|
||||
synchronized (lock) {
|
||||
stopPolling();
|
||||
if (connectionList.getRefreshPeriod() >= 0) startPolling(); // 0 means default refresh poll
|
||||
}
|
||||
}).start();
|
||||
stopPolling();
|
||||
if (connectionList.getRefreshPeriod() >= 0) startPolling(); // 0 means default refresh poll
|
||||
}
|
||||
|
||||
private void startPolling() {
|
||||
synchronized (lock) {
|
||||
if (daemonPollLooper != null) daemonPollLooper.stop();
|
||||
daemonPollLooper = new TaskLooper(() -> pollDaemonInfo());
|
||||
daemonPollLooper = new TaskLooper(() -> pollDaemon());
|
||||
daemonPollLooper.start(getRefreshPeriodMs());
|
||||
}
|
||||
}
|
||||
|
@ -582,17 +579,34 @@ public final class XmrConnectionService {
|
|||
}
|
||||
}
|
||||
|
||||
private void pollDaemonInfo() {
|
||||
private void pollDaemon() {
|
||||
if (pollInProgress) return;
|
||||
doPollDaemon();
|
||||
}
|
||||
|
||||
private void doPollDaemon() {
|
||||
synchronized (pollLock) {
|
||||
pollInProgress = true;
|
||||
if (isShutDownStarted) return;
|
||||
try {
|
||||
|
||||
// poll daemon
|
||||
log.debug("Polling daemon info");
|
||||
if (daemon == null) throw new RuntimeException("No daemon connection");
|
||||
lastInfo = daemon.getInfo();
|
||||
if (daemon == null) switchToBestConnection();
|
||||
if (daemon == null) throw new RuntimeException("No connection to Monero daemon");
|
||||
try {
|
||||
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
|
||||
UserThread.execute(() -> {
|
||||
|
@ -632,19 +646,15 @@ public final class XmrConnectionService {
|
|||
lastErrorTimestamp = null;
|
||||
}
|
||||
|
||||
// update and notify connected state
|
||||
if (!Boolean.TRUE.equals(connectionManager.isConnected())) {
|
||||
connectionManager.checkConnection();
|
||||
}
|
||||
|
||||
// clear error message
|
||||
if (Boolean.TRUE.equals(connectionManager.isConnected()) && HavenoUtils.havenoSetup != null) {
|
||||
HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(null);
|
||||
}
|
||||
if (HavenoUtils.havenoSetup != null) HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(null);
|
||||
} catch (Exception e) {
|
||||
|
||||
// skip if shut down or connected
|
||||
if (isShutDownStarted || Boolean.TRUE.equals(isConnected())) return;
|
||||
// not connected to daemon
|
||||
isConnected = false;
|
||||
|
||||
// skip if shut down
|
||||
if (isShutDownStarted) return;
|
||||
|
||||
// log error message periodically
|
||||
if ((lastErrorTimestamp == null || System.currentTimeMillis() - lastErrorTimestamp > MIN_ERROR_LOG_PERIOD_MS)) {
|
||||
|
@ -653,20 +663,8 @@ public final class XmrConnectionService {
|
|||
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
|
||||
if (!Boolean.TRUE.equals(connectionManager.isConnected()) && HavenoUtils.havenoSetup != null) {
|
||||
HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(e.getMessage());
|
||||
}
|
||||
}).start();
|
||||
// set error message
|
||||
if (HavenoUtils.havenoSetup != null) HavenoUtils.havenoSetup.getWalletServiceErrorMsg().set(e.getMessage());
|
||||
} finally {
|
||||
pollInProgress = false;
|
||||
}
|
||||
|
|
|
@ -36,7 +36,6 @@ package haveno.core.offer;
|
|||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
import common.utils.GenUtils;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.config.Config;
|
||||
import haveno.common.file.JsonFileManager;
|
||||
|
@ -46,6 +45,7 @@ import haveno.core.api.XmrConnectionService;
|
|||
import haveno.core.filter.FilterManager;
|
||||
import haveno.core.locale.Res;
|
||||
import haveno.core.provider.price.PriceFeedService;
|
||||
import haveno.core.trade.HavenoUtils;
|
||||
import haveno.core.util.JsonUtil;
|
||||
import haveno.core.xmr.wallet.XmrKeyImageListener;
|
||||
import haveno.core.xmr.wallet.XmrKeyImagePoller;
|
||||
|
@ -287,7 +287,7 @@ public class OfferBookService {
|
|||
// first poll after 20s
|
||||
// TODO: remove?
|
||||
new Thread(() -> {
|
||||
GenUtils.waitFor(20000);
|
||||
HavenoUtils.waitFor(20000);
|
||||
keyImagePoller.poll();
|
||||
}).start();
|
||||
}
|
||||
|
|
|
@ -36,7 +36,6 @@ package haveno.core.offer;
|
|||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import com.google.inject.Inject;
|
||||
import common.utils.GenUtils;
|
||||
import haveno.common.ThreadUtils;
|
||||
import haveno.common.Timer;
|
||||
import haveno.common.UserThread;
|
||||
|
@ -71,6 +70,7 @@ import haveno.core.trade.ClosedTradableManager;
|
|||
import haveno.core.trade.HavenoUtils;
|
||||
import haveno.core.trade.TradableList;
|
||||
import haveno.core.trade.handlers.TransactionResultHandler;
|
||||
import haveno.core.trade.protocol.TradeProtocol;
|
||||
import haveno.core.trade.statistics.TradeStatisticsManager;
|
||||
import haveno.core.user.Preferences;
|
||||
import haveno.core.user.User;
|
||||
|
@ -278,7 +278,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
// first poll in 5s
|
||||
// TODO: remove?
|
||||
new Thread(() -> {
|
||||
GenUtils.waitFor(5000);
|
||||
HavenoUtils.waitFor(5000);
|
||||
signedOfferKeyImagePoller.poll();
|
||||
}).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.
|
||||
// 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);
|
||||
GenUtils.waitFor(delayMs);
|
||||
HavenoUtils.waitFor(delayMs);
|
||||
}, THREAD_ID);
|
||||
} else {
|
||||
broadcaster.flush();
|
||||
|
@ -705,9 +705,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
// remove open offer which thaws its key images
|
||||
private void onCancelled(@NotNull OpenOffer openOffer) {
|
||||
Offer offer = openOffer.getOffer();
|
||||
if (offer.getOfferPayload().getReserveTxKeyImages() != null) {
|
||||
xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages());
|
||||
}
|
||||
xmrWalletService.thawOutputs(offer.getOfferPayload().getReserveTxKeyImages());
|
||||
offer.setState(Offer.State.REMOVED);
|
||||
openOffer.setState(OpenOffer.State.CANCELED);
|
||||
removeOpenOffer(openOffer);
|
||||
|
@ -1029,6 +1027,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
// handle sufficient available balance to split output
|
||||
boolean sufficientAvailableBalance = xmrWalletService.getWallet().getUnlockedBalance(0).compareTo(offerReserveAmount) >= 0;
|
||||
if (sufficientAvailableBalance) {
|
||||
log.info("Splitting and scheduling outputs for offer {} at subaddress {}", openOffer.getShortId());
|
||||
splitAndSchedule(openOffer);
|
||||
} else if (openOffer.getScheduledTxHashes() == null) {
|
||||
scheduleWithEarliestTxs(openOffers, openOffer);
|
||||
|
@ -1038,16 +1037,28 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
private MoneroTxWallet splitAndSchedule(OpenOffer openOffer) {
|
||||
BigInteger reserveAmount = openOffer.getOffer().getReserveAmount();
|
||||
xmrWalletService.swapAddressEntryToAvailable(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output(s)
|
||||
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());
|
||||
long startTime = System.currentTimeMillis();
|
||||
MoneroTxWallet splitOutputTx = xmrWalletService.getWallet().createTx(new MoneroTxConfig()
|
||||
.setAccountIndex(0)
|
||||
.setAddress(entry.getAddressString())
|
||||
.setAmount(reserveAmount)
|
||||
.setRelay(true)
|
||||
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY));
|
||||
log.info("Done creating split output tx to fund offer {} in {} ms", openOffer.getId(), System.currentTimeMillis() - startTime);
|
||||
MoneroTxWallet splitOutputTx = null;
|
||||
synchronized (XmrWalletService.WALLET_LOCK) {
|
||||
XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
|
||||
log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex());
|
||||
long startTime = System.currentTimeMillis();
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
try {
|
||||
splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig()
|
||||
.setAccountIndex(0)
|
||||
.setAddress(entry.getAddressString())
|
||||
.setAmount(reserveAmount)
|
||||
.setRelay(true)
|
||||
.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);
|
||||
}
|
||||
|
||||
// schedule txs
|
||||
openOffer.setSplitOutputTxHash(splitOutputTx.getHash());
|
||||
|
|
|
@ -133,7 +133,7 @@ public class PlaceOfferProtocol {
|
|||
stopTimeoutTimer();
|
||||
timeoutTimer = UserThread.runAfter(() -> {
|
||||
handleError(Res.get("createOffer.timeoutAtPublishing"));
|
||||
}, TradeProtocol.TRADE_TIMEOUT_SECONDS);
|
||||
}, TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
|
||||
}
|
||||
|
||||
private void stopTimeoutTimer() {
|
||||
|
|
|
@ -17,12 +17,22 @@
|
|||
|
||||
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.TaskRunner;
|
||||
import haveno.core.offer.Offer;
|
||||
import haveno.core.offer.OfferDirection;
|
||||
import haveno.core.offer.OpenOffer;
|
||||
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.wallet.XmrWalletService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.daemon.model.MoneroOutput;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
|
||||
@Slf4j
|
||||
|
@ -35,7 +45,8 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
|||
@Override
|
||||
protected void run() {
|
||||
|
||||
Offer offer = model.getOpenOffer().getOffer();
|
||||
OpenOffer openOffer = model.getOpenOffer();
|
||||
Offer offer = openOffer.getOffer();
|
||||
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
@ -44,16 +55,51 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
|||
model.getXmrWalletService().getConnectionService().verifyConnection();
|
||||
|
||||
// create reserve tx
|
||||
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(model.getOpenOffer());
|
||||
model.setReserveTx(reserveTx);
|
||||
MoneroTxWallet reserveTx = null;
|
||||
synchronized (XmrWalletService.WALLET_LOCK) {
|
||||
|
||||
// check for error in case creating reserve tx exceeded timeout // TODO: better way?
|
||||
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");
|
||||
// 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?
|
||||
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");
|
||||
}
|
||||
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
|
||||
model.getProtocol().startTimeoutTimer();
|
||||
model.setReserveTx(reserveTx);
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
offer.setErrorMessage("An error occurred.\n" +
|
||||
|
|
|
@ -524,7 +524,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
|
||||
// update multisig hex
|
||||
if (message.getUpdatedMultisigHex() != null) sender.setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
||||
if (trade.walletExists()) trade.importMultisigHex();
|
||||
|
||||
// add chat message with price info
|
||||
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
|
||||
MoneroTxWallet payoutTx = null;
|
||||
try {
|
||||
payoutTx = trade.getWallet().createTx(txConfig);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw new RuntimeException("Loser payout is too small to cover the mining fee");
|
||||
}
|
||||
MoneroTxWallet payoutTx = trade.createDisputePayoutTx(txConfig);
|
||||
|
||||
// update trade state
|
||||
if (updateState) {
|
||||
trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
|
||||
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());
|
||||
trade.setPayoutTx(payoutTx);
|
||||
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||
if (trade.getBuyer().getUpdatedMultisigHex() != null && trade.getBuyer().getUnsignedPayoutTxHex() == null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||
|
|
|
@ -36,7 +36,6 @@ package haveno.core.support.dispute.arbitration;
|
|||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import common.utils.GenUtils;
|
||||
import haveno.common.ThreadUtils;
|
||||
import haveno.common.Timer;
|
||||
import haveno.common.UserThread;
|
||||
|
@ -68,6 +67,7 @@ import haveno.core.trade.Contract;
|
|||
import haveno.core.trade.HavenoUtils;
|
||||
import haveno.core.trade.Trade;
|
||||
import haveno.core.trade.TradeManager;
|
||||
import haveno.core.trade.protocol.TradeProtocol;
|
||||
import haveno.core.xmr.wallet.TradeWalletService;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
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());
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (trade.isPayoutPublished()) break;
|
||||
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5);
|
||||
HavenoUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5);
|
||||
}
|
||||
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());
|
||||
} else {
|
||||
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 (!expectedSellerAmount.equals(actualSellerAmount)) throw new IllegalArgumentException("Unexpected seller payout: " + expectedSellerAmount + " vs " + actualSellerAmount);
|
||||
|
||||
// check wallet's daemon connection
|
||||
trade.checkAndVerifyDaemonConnection();
|
||||
// check daemon connection
|
||||
trade.verifyDaemonConnection();
|
||||
|
||||
// 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?
|
||||
|
@ -471,8 +471,17 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
}
|
||||
|
||||
// submit fully signed payout tx to the network
|
||||
List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex());
|
||||
disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
try {
|
||||
List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex());
|
||||
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
|
||||
trade.setPayoutTx(disputeTxSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
|
||||
|
|
|
@ -19,6 +19,8 @@ package haveno.core.trade;
|
|||
|
||||
import com.google.common.base.CaseFormat;
|
||||
import com.google.common.base.Charsets;
|
||||
|
||||
import common.utils.GenUtils;
|
||||
import haveno.common.config.Config;
|
||||
import haveno.common.crypto.CryptoException;
|
||||
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 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
|
||||
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;
|
||||
|
@ -108,6 +121,10 @@ public class HavenoUtils {
|
|||
return new Date().before(releaseDatePlusDays);
|
||||
}
|
||||
|
||||
public static void waitFor(long waitMs) {
|
||||
GenUtils.waitFor(waitMs);
|
||||
}
|
||||
|
||||
// ----------------------- CONVERSION UTILS -------------------------------
|
||||
|
||||
public static BigInteger coinToAtomicUnits(Coin coin) {
|
||||
|
|
|
@ -37,7 +37,6 @@ package haveno.core.trade;
|
|||
import com.google.common.base.Preconditions;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.Message;
|
||||
import common.utils.GenUtils;
|
||||
import haveno.common.ThreadUtils;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.crypto.Encryption;
|
||||
|
@ -120,7 +119,6 @@ import java.util.Arrays;
|
|||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
@ -479,7 +477,6 @@ public abstract class Trade implements Tradable, Model {
|
|||
private long payoutTxFee;
|
||||
private Long payoutHeight;
|
||||
private IdlePayoutSyncer idlePayoutSyncer;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private boolean isCompleted;
|
||||
|
@ -638,18 +635,14 @@ public abstract class Trade implements Tradable, Model {
|
|||
// handle trade state events
|
||||
tradeStateSubscription = EasyBind.subscribe(stateProperty, newValue -> {
|
||||
if (!isInitialized || isShutDownStarted) return;
|
||||
ThreadUtils.execute(() -> {
|
||||
if (newValue == Trade.State.MULTISIG_COMPLETED) {
|
||||
updatePollPeriod();
|
||||
startPolling();
|
||||
}
|
||||
}, getId());
|
||||
// no processing
|
||||
});
|
||||
|
||||
// handle trade phase events
|
||||
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
|
||||
if (!isInitialized || isShutDownStarted) return;
|
||||
ThreadUtils.execute(() -> {
|
||||
if (newValue == Trade.Phase.DEPOSIT_REQUESTED) startPolling();
|
||||
if (isDepositsPublished() && !isPayoutUnlocked()) updatePollPeriod();
|
||||
if (isPaymentReceived()) {
|
||||
UserThread.execute(() -> {
|
||||
|
@ -674,9 +667,9 @@ public abstract class Trade implements Tradable, Model {
|
|||
|
||||
// sync main wallet to update pending balance
|
||||
new Thread(() -> {
|
||||
GenUtils.waitFor(1000);
|
||||
HavenoUtils.waitFor(1000);
|
||||
if (isShutDownStarted) return;
|
||||
if (Boolean.TRUE.equals(xmrConnectionService.isConnected())) xmrWalletService.syncWallet(xmrWalletService.getWallet());
|
||||
if (xmrConnectionService.isConnected()) xmrWalletService.syncWallet();
|
||||
}).start();
|
||||
|
||||
// complete disputed trade
|
||||
|
@ -731,16 +724,17 @@ public abstract class Trade implements Tradable, Model {
|
|||
setPayoutStateUnlocked();
|
||||
return;
|
||||
} 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
|
||||
tryInitPolling();
|
||||
// start polling if deposit requested
|
||||
if (isDepositRequested()) tryInitPolling();
|
||||
}
|
||||
|
||||
public void requestPersistence() {
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
if (processModel.getTradeManager() != null) processModel.getTradeManager().requestPersistence();
|
||||
}
|
||||
|
||||
public TradeProtocol getProtocol() {
|
||||
|
@ -793,21 +787,8 @@ public abstract class Trade implements Tradable, Model {
|
|||
return MONERO_TRADE_WALLET_PREFIX + getShortId() + "_" + getShortUid();
|
||||
}
|
||||
|
||||
public void checkAndVerifyDaemonConnection() {
|
||||
|
||||
// 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 void verifyDaemonConnection() {
|
||||
if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Connection service is not connected to a Monero node");
|
||||
}
|
||||
|
||||
public boolean isWalletConnectedToDaemon() {
|
||||
|
@ -848,7 +829,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
|
||||
// reset wallet poll period after duration
|
||||
new Thread(() -> {
|
||||
GenUtils.waitFor(pollNormalDuration);
|
||||
HavenoUtils.waitFor(pollNormalDuration);
|
||||
Long pollNormalStartTimeMsCopy = pollNormalStartTimeMs; // copy to avoid race condition
|
||||
if (pollNormalStartTimeMsCopy == null) return;
|
||||
if (!isShutDown && System.currentTimeMillis() >= pollNormalStartTimeMsCopy + pollNormalDuration) {
|
||||
|
@ -860,21 +841,38 @@ public abstract class Trade implements Tradable, Model {
|
|||
|
||||
public void importMultisigHex() {
|
||||
synchronized (walletLock) {
|
||||
|
||||
// ensure wallet sees deposits confirmed
|
||||
if (!isDepositsConfirmed()) syncAndPollWallet();
|
||||
|
||||
// import multisig hexes
|
||||
List<String> multisigHexes = new ArrayList<String>();
|
||||
for (TradePeer node : getAllTradeParties()) if (node.getUpdatedMultisigHex() != null) multisigHexes.add(node.getUpdatedMultisigHex());
|
||||
if (!multisigHexes.isEmpty()) {
|
||||
log.info("Importing multisig hex for {} {}", getClass().getSimpleName(), getId());
|
||||
long startTime = System.currentTimeMillis();
|
||||
getWallet().importMultisigHex(multisigHexes.toArray(new String[0]));
|
||||
log.info("Done importing multisig hex for {} {} in {} ms", getClass().getSimpleName(), getId(), System.currentTimeMillis() - startTime);
|
||||
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
|
||||
if (!isDepositsConfirmed()) syncAndPollWallet();
|
||||
|
||||
// collect multisig hex from peers
|
||||
List<String> multisigHexes = new ArrayList<String>();
|
||||
for (TradePeer peer : getOtherPeers()) if (peer.getUpdatedMultisigHex() != null) multisigHexes.add(peer.getUpdatedMultisigHex());
|
||||
|
||||
// import multisig hex
|
||||
log.info("Importing multisig hexes for {} {}, count={}", getClass().getSimpleName(), getShortId(), multisigHexes.size());
|
||||
long startTime = System.currentTimeMillis();
|
||||
if (!multisigHexes.isEmpty()) {
|
||||
wallet.importMultisigHex(multisigHexes.toArray(new String[0]));
|
||||
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) {
|
||||
|
@ -891,10 +889,10 @@ public abstract class Trade implements Tradable, Model {
|
|||
public void saveWallet() {
|
||||
synchronized (walletLock) {
|
||||
if (!walletExists()) {
|
||||
log.warn("Cannot save wallet for {} {} because it does not exist", getClass().getSimpleName(), getId());
|
||||
log.warn("Cannot save wallet for {} {} because it does not exist", getClass().getSimpleName(), getShortId());
|
||||
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);
|
||||
maybeBackupWallet();
|
||||
}
|
||||
|
@ -953,7 +951,13 @@ public abstract class Trade implements Tradable, Model {
|
|||
|
||||
// check for balance
|
||||
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
|
||||
|
@ -1021,17 +1025,44 @@ public abstract class Trade implements Tradable, Model {
|
|||
return contract;
|
||||
}
|
||||
|
||||
public MoneroTxWallet createTx(MoneroTxConfig txConfig) {
|
||||
synchronized (walletLock) {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
return wallet.createTx(txConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
|
||||
// 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();
|
||||
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);
|
||||
|
||||
// create payout tx
|
||||
MoneroTxWallet payoutTx = multisigWallet.createTx(new MoneroTxConfig()
|
||||
MoneroTxWallet payoutTx = createTx(new MoneroTxConfig()
|
||||
.setAccountIndex(0)
|
||||
.addDestination(buyerPayoutAddress, buyerPayoutAmount)
|
||||
.addDestination(sellerPayoutAddress, sellerPayoutAmount)
|
||||
|
@ -1066,6 +1097,24 @@ public abstract class Trade implements Tradable, Model {
|
|||
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.
|
||||
*
|
||||
|
@ -1074,82 +1123,93 @@ public abstract class Trade implements Tradable, Model {
|
|||
* @param publish publishes the signed payout tx if true
|
||||
*/
|
||||
public void processPayoutTx(String payoutTxHex, boolean sign, boolean publish) {
|
||||
log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId());
|
||||
synchronized (walletLock) {
|
||||
log.info("Processing payout tx for {} {}", getClass().getSimpleName(), getId());
|
||||
|
||||
// gather relevant info
|
||||
MoneroWallet wallet = getWallet();
|
||||
Contract contract = getContract();
|
||||
BigInteger sellerDepositAmount = wallet.getTx(getSeller().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs this.getDepositTxId() necessary or avoidable?
|
||||
BigInteger buyerDepositAmount = wallet.getTx(getBuyer().getDepositTxHash()).getIncomingAmount();
|
||||
BigInteger tradeAmount = getAmount();
|
||||
|
||||
// describe payout tx
|
||||
MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
|
||||
if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack
|
||||
MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0);
|
||||
|
||||
// verify payout tx has exactly 2 destinations
|
||||
if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new IllegalArgumentException("Payout tx does not have exactly two destinations");
|
||||
|
||||
// get buyer and seller destinations (order not preserved)
|
||||
boolean buyerFirst = payoutTx.getOutgoingTransfer().getDestinations().get(0).getAddress().equals(contract.getBuyerPayoutAddressString());
|
||||
MoneroDestination buyerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 0 : 1);
|
||||
MoneroDestination sellerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 1 : 0);
|
||||
|
||||
// verify payout addresses
|
||||
if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new IllegalArgumentException("Buyer payout address does not match contract");
|
||||
if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new IllegalArgumentException("Seller payout address does not match contract");
|
||||
|
||||
// verify change address is multisig's primary address
|
||||
if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO)) log.warn("Dust left in multisig wallet for {} {}: {}", getClass().getSimpleName(), getId(), payoutTx.getChangeAmount());
|
||||
if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO) && !payoutTx.getChangeAddress().equals(wallet.getPrimaryAddress())) throw new IllegalArgumentException("Change address is not multisig wallet's primary address");
|
||||
|
||||
// verify sum of outputs = destination amounts + change amount
|
||||
if (!payoutTx.getOutputSum().equals(buyerPayoutDestination.getAmount().add(sellerPayoutDestination.getAmount()).add(payoutTx.getChangeAmount()))) throw new IllegalArgumentException("Sum of outputs != destination amounts + change amount");
|
||||
|
||||
// verify buyer destination amount is deposit amount + this amount - 1/2 tx costs
|
||||
BigInteger txCost = payoutTx.getFee().add(payoutTx.getChangeAmount());
|
||||
BigInteger txCostSplit = txCost.divide(BigInteger.valueOf(2));
|
||||
BigInteger expectedBuyerPayout = buyerDepositAmount.add(tradeAmount).subtract(txCostSplit);
|
||||
if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new IllegalArgumentException("Buyer destination amount is not deposit amount + trade amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout);
|
||||
|
||||
// verify seller destination amount is deposit amount - this amount - 1/2 tx costs
|
||||
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);
|
||||
|
||||
// check wallet connection
|
||||
if (sign || publish) checkAndVerifyDaemonConnection();
|
||||
|
||||
// handle tx signing
|
||||
if (sign) {
|
||||
|
||||
// sign tx
|
||||
MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex);
|
||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx");
|
||||
payoutTxHex = result.getSignedMultisigTxHex();
|
||||
|
||||
// describe result
|
||||
describedTxSet = wallet.describeMultisigTxSet(payoutTxHex);
|
||||
payoutTx = describedTxSet.getTxs().get(0);
|
||||
|
||||
// verify fee is within tolerance by recreating payout tx
|
||||
// TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated?
|
||||
MoneroTxWallet feeEstimateTx = createPayoutTx();
|
||||
BigInteger feeEstimate = feeEstimateTx.getFee();
|
||||
double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
|
||||
if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + payoutTx.getFee());
|
||||
log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff);
|
||||
}
|
||||
|
||||
// update trade state
|
||||
setPayoutTx(payoutTx);
|
||||
setPayoutTxHex(payoutTxHex);
|
||||
|
||||
// submit payout tx
|
||||
if (publish) {
|
||||
wallet.submitMultisigTxHex(payoutTxHex);
|
||||
pollWallet();
|
||||
// gather relevant info
|
||||
MoneroWallet wallet = getWallet();
|
||||
Contract contract = getContract();
|
||||
BigInteger sellerDepositAmount = wallet.getTx(getSeller().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs this.getDepositTxId() necessary or avoidable?
|
||||
BigInteger buyerDepositAmount = wallet.getTx(getBuyer().getDepositTxHash()).getIncomingAmount();
|
||||
BigInteger tradeAmount = getAmount();
|
||||
|
||||
// describe payout tx
|
||||
MoneroTxSet describedTxSet = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
|
||||
if (describedTxSet.getTxs() == null || describedTxSet.getTxs().size() != 1) throw new IllegalArgumentException("Bad payout tx"); // TODO (woodser): test nack
|
||||
MoneroTxWallet payoutTx = describedTxSet.getTxs().get(0);
|
||||
|
||||
// verify payout tx has exactly 2 destinations
|
||||
if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new IllegalArgumentException("Payout tx does not have exactly two destinations");
|
||||
|
||||
// get buyer and seller destinations (order not preserved)
|
||||
boolean buyerFirst = payoutTx.getOutgoingTransfer().getDestinations().get(0).getAddress().equals(contract.getBuyerPayoutAddressString());
|
||||
MoneroDestination buyerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 0 : 1);
|
||||
MoneroDestination sellerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 1 : 0);
|
||||
|
||||
// verify payout addresses
|
||||
if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new IllegalArgumentException("Buyer payout address does not match contract");
|
||||
if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new IllegalArgumentException("Seller payout address does not match contract");
|
||||
|
||||
// verify change address is multisig's primary address
|
||||
if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO)) log.warn("Dust left in multisig wallet for {} {}: {}", getClass().getSimpleName(), getId(), payoutTx.getChangeAmount());
|
||||
if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO) && !payoutTx.getChangeAddress().equals(wallet.getPrimaryAddress())) throw new IllegalArgumentException("Change address is not multisig wallet's primary address");
|
||||
|
||||
// verify sum of outputs = destination amounts + change amount
|
||||
if (!payoutTx.getOutputSum().equals(buyerPayoutDestination.getAmount().add(sellerPayoutDestination.getAmount()).add(payoutTx.getChangeAmount()))) throw new IllegalArgumentException("Sum of outputs != destination amounts + change amount");
|
||||
|
||||
// verify buyer destination amount is deposit amount + this amount - 1/2 tx costs
|
||||
BigInteger txCost = payoutTx.getFee().add(payoutTx.getChangeAmount());
|
||||
BigInteger txCostSplit = txCost.divide(BigInteger.valueOf(2));
|
||||
BigInteger expectedBuyerPayout = buyerDepositAmount.add(tradeAmount).subtract(txCostSplit);
|
||||
if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new IllegalArgumentException("Buyer destination amount is not deposit amount + trade amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout);
|
||||
|
||||
// verify seller destination amount is deposit amount - this amount - 1/2 tx costs
|
||||
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);
|
||||
|
||||
// check connection
|
||||
if (sign || publish) verifyDaemonConnection();
|
||||
|
||||
// handle tx signing
|
||||
if (sign) {
|
||||
|
||||
// sign tx
|
||||
MoneroMultisigSignResult result = wallet.signMultisigTxHex(payoutTxHex);
|
||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx");
|
||||
payoutTxHex = result.getSignedMultisigTxHex();
|
||||
|
||||
// describe result
|
||||
describedTxSet = wallet.describeMultisigTxSet(payoutTxHex);
|
||||
payoutTx = describedTxSet.getTxs().get(0);
|
||||
|
||||
// verify fee is within tolerance by recreating payout tx
|
||||
// TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated?
|
||||
MoneroTxWallet feeEstimateTx = createPayoutTx();
|
||||
BigInteger feeEstimate = feeEstimateTx.getFee();
|
||||
double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
|
||||
if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + payoutTx.getFee());
|
||||
log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff);
|
||||
}
|
||||
|
||||
// update trade state
|
||||
setPayoutTx(payoutTx);
|
||||
setPayoutTxHex(payoutTxHex);
|
||||
|
||||
// submit payout tx
|
||||
if (publish) {
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
try {
|
||||
wallet.submitMultisigTxHex(payoutTxHex);
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1173,6 +1233,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
|
||||
// set payment account payload
|
||||
getTradePeer().setPaymentAccountPayload(paymentAccountPayload);
|
||||
processModel.getPaymentAccountDecryptedProperty().set(true);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
@ -1220,6 +1281,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
public void clearAndShutDown() {
|
||||
ThreadUtils.execute(() -> {
|
||||
clearProcessData();
|
||||
onShutDownStarted();
|
||||
ThreadUtils.submitToPool(() -> shutDown()); // run off trade thread
|
||||
}, getId());
|
||||
}
|
||||
|
@ -1237,7 +1299,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
|
||||
// TODO: clear other process data
|
||||
setPayoutTxHex(null);
|
||||
for (TradePeer peer : getAllTradeParties()) {
|
||||
for (TradePeer peer : getAllPeers()) {
|
||||
peer.setUnsignedPayoutTxHex(null);
|
||||
peer.setUpdatedMultisigHex(null);
|
||||
peer.setDisputeClosedMessage(null);
|
||||
|
@ -1294,7 +1356,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
// repeatedly acquire lock to clear tasks
|
||||
for (int i = 0; i < 20; i++) {
|
||||
synchronized (this) {
|
||||
GenUtils.waitFor(10);
|
||||
HavenoUtils.waitFor(10);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1390,6 +1452,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
}
|
||||
|
||||
this.state = state;
|
||||
requestPersistence();
|
||||
UserThread.await(() -> {
|
||||
stateProperty.set(state);
|
||||
phaseProperty.set(state.getPhase());
|
||||
|
@ -1421,6 +1484,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
}
|
||||
|
||||
this.payoutState = payoutState;
|
||||
requestPersistence();
|
||||
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");
|
||||
}
|
||||
|
||||
private List<TradePeer> getPeers() {
|
||||
List<TradePeer> peers = getAllTradeParties();
|
||||
private List<TradePeer> getOtherPeers() {
|
||||
List<TradePeer> peers = getAllPeers();
|
||||
if (!peers.remove(getSelf())) throw new IllegalStateException("Failed to remove self from list of peers");
|
||||
return peers;
|
||||
}
|
||||
|
||||
private List<TradePeer> getAllTradeParties() {
|
||||
private List<TradePeer> getAllPeers() {
|
||||
List<TradePeer> peers = new ArrayList<TradePeer>();
|
||||
peers.add(getMaker());
|
||||
peers.add(getTaker());
|
||||
|
@ -1765,7 +1829,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
if (this instanceof BuyerTrade) {
|
||||
return getArbitrator().isDepositsConfirmedMessageAcked();
|
||||
} else {
|
||||
for (TradePeer peer : getPeers()) if (!peer.isDepositsConfirmedMessageAcked()) return false;
|
||||
for (TradePeer peer : getOtherPeers()) if (!peer.isDepositsConfirmedMessageAcked()) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -1982,13 +2046,19 @@ public abstract class Trade implements Tradable, Model {
|
|||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
private void tryInitPolling() {
|
||||
if (isShutDownStarted) return;
|
||||
|
||||
// set known deposit txs
|
||||
List<MoneroTxWallet> depositTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true).setInTxPool(false));
|
||||
setDepositTxs(depositTxs);
|
||||
|
||||
// start polling
|
||||
if (!isIdling()) {
|
||||
tryInitPollingAux();
|
||||
} else {
|
||||
|
@ -2023,9 +2093,12 @@ public abstract class Trade implements Tradable, Model {
|
|||
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().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());
|
||||
xmrWalletService.syncWallet(getWallet());
|
||||
log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
||||
if (isWalletBehind()) {
|
||||
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getShortId());
|
||||
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
|
||||
if (!wasWalletSynced) {
|
||||
|
@ -2063,6 +2136,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
private void startPolling() {
|
||||
synchronized (walletLock) {
|
||||
if (isShutDownStarted || isPollInProgress()) return;
|
||||
updatePollPeriod();
|
||||
log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId());
|
||||
pollLooper = new TaskLooper(() -> pollWallet());
|
||||
pollLooper.start(pollPeriodMs);
|
||||
|
@ -2110,7 +2184,15 @@ public abstract class Trade implements Tradable, Model {
|
|||
MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true);
|
||||
Boolean updatePool = !isDepositsConfirmed() && (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null);
|
||||
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);
|
||||
if (getMaker().getDepositTx() == null || getTaker().getDepositTx() == null) return; // skip if either deposit tx not seen
|
||||
setStateDepositsSeen();
|
||||
|
@ -2142,7 +2224,7 @@ public abstract class Trade implements Tradable, Model {
|
|||
if (isPayoutExpected || isPayoutPublished()) syncWalletIfBehind();
|
||||
|
||||
// rescan spent outputs to detect unconfirmed payout tx
|
||||
if (isPayoutExpected && !isPayoutPublished()) {
|
||||
if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) {
|
||||
try {
|
||||
wallet.rescanSpent();
|
||||
} catch (Exception e) {
|
||||
|
@ -2154,7 +2236,15 @@ public abstract class Trade implements Tradable, Model {
|
|||
MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true);
|
||||
boolean updatePool = isPayoutExpected && !isPayoutConfirmed();
|
||||
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);
|
||||
|
||||
// check if any outputs spent (observed on payout published)
|
||||
|
@ -2191,7 +2281,15 @@ public abstract class Trade implements Tradable, Model {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
@ -2278,9 +2376,8 @@ public abstract class Trade implements Tradable, Model {
|
|||
processing = false;
|
||||
} catch (Exception e) {
|
||||
processing = false;
|
||||
boolean isWalletConnected = isWalletConnectedToDaemon();
|
||||
if (!isWalletConnected) xmrConnectionService.checkConnection(); // check connection if wallet is not connected
|
||||
if (isInitialized &&!isShutDownStarted && isWalletConnected) {
|
||||
if (!isInitialized || isShutDownStarted) return;
|
||||
if (isWalletConnectedToDaemon()) {
|
||||
e.printStackTrace();
|
||||
log.warn("Error polling idle trade for {} {}: {}. Monerod={}", getClass().getSimpleName(), getId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection());
|
||||
};
|
||||
|
|
|
@ -38,7 +38,6 @@ import static com.google.common.base.Preconditions.checkArgument;
|
|||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.inject.Inject;
|
||||
import common.utils.GenUtils;
|
||||
import haveno.common.ClockWatcher;
|
||||
import haveno.common.ThreadUtils;
|
||||
import haveno.common.UserThread;
|
||||
|
@ -512,7 +511,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
}).start();
|
||||
|
||||
// allow execution to start
|
||||
GenUtils.waitFor(100);
|
||||
HavenoUtils.waitFor(100);
|
||||
}
|
||||
|
||||
private void initPersistedTrade(Trade trade) {
|
||||
|
@ -1249,27 +1248,20 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
}
|
||||
|
||||
private void addTrade(Trade trade) {
|
||||
UserThread.execute(() -> {
|
||||
synchronized (tradableList) {
|
||||
if (tradableList.add(trade)) {
|
||||
requestPersistence();
|
||||
}
|
||||
synchronized (tradableList) {
|
||||
if (tradableList.add(trade)) {
|
||||
requestPersistence();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void removeTrade(Trade trade) {
|
||||
log.info("TradeManager.removeTrade() " + trade.getId());
|
||||
synchronized (tradableList) {
|
||||
if (!tradableList.contains(trade)) return;
|
||||
}
|
||||
|
||||
|
||||
// remove trade
|
||||
UserThread.execute(() -> {
|
||||
synchronized (tradableList) {
|
||||
tradableList.remove(trade);
|
||||
}
|
||||
});
|
||||
synchronized (tradableList) {
|
||||
if (!tradableList.remove(trade)) return;
|
||||
}
|
||||
|
||||
// unregister and persist
|
||||
p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade));
|
||||
|
@ -1277,30 +1269,26 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
}
|
||||
|
||||
private void maybeRemoveTradeOnError(Trade trade) {
|
||||
synchronized (tradableList) {
|
||||
if (trade.isDepositRequested() && !trade.isDepositFailed()) {
|
||||
listenForCleanup(trade);
|
||||
} else {
|
||||
removeTradeOnError(trade);
|
||||
}
|
||||
if (trade.isDepositRequested() && !trade.isDepositFailed()) {
|
||||
listenForCleanup(trade);
|
||||
} else {
|
||||
removeTradeOnError(trade);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeTradeOnError(Trade trade) {
|
||||
log.warn("TradeManager.removeTradeOnError() trade={}, tradeId={}, state={}", trade.getClass().getSimpleName(), trade.getShortId(), trade.getState());
|
||||
synchronized (tradableList) {
|
||||
|
||||
// unreserve taker key images
|
||||
if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) {
|
||||
xmrWalletService.thawOutputs(trade.getSelf().getReserveTxKeyImages());
|
||||
trade.getSelf().setReserveTxKeyImages(null);
|
||||
}
|
||||
// unreserve taker key images
|
||||
if (trade instanceof TakerTrade) {
|
||||
xmrWalletService.thawOutputs(trade.getSelf().getReserveTxKeyImages());
|
||||
trade.getSelf().setReserveTxKeyImages(null);
|
||||
}
|
||||
|
||||
// unreserve open offer
|
||||
Optional<OpenOffer> openOffer = openOfferManager.getOpenOfferById(trade.getId());
|
||||
if (trade instanceof MakerTrade && openOffer.isPresent()) {
|
||||
openOfferManager.unreserveOpenOffer(openOffer.get());
|
||||
}
|
||||
// unreserve open offer
|
||||
Optional<OpenOffer> openOffer = openOfferManager.getOpenOfferById(trade.getId());
|
||||
if (trade instanceof MakerTrade && openOffer.isPresent()) {
|
||||
openOfferManager.unreserveOpenOffer(openOffer.get());
|
||||
}
|
||||
|
||||
// clear and shut down trade
|
||||
|
@ -1358,7 +1346,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||
new Thread(() -> {
|
||||
|
||||
// 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
|
||||
MoneroTx makerDepositTx = trade.getMaker().getDepositTxHash() == null ? null : xmrWalletService.getDaemon().getTx(trade.getMaker().getDepositTxHash());
|
||||
|
|
|
@ -59,13 +59,13 @@ public class ArbitratorProtocol extends DisputeProtocol {
|
|||
ArbitratorSendInitTradeOrMultisigRequests.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
startTimeout(TRADE_TIMEOUT_SECONDS);
|
||||
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
},
|
||||
errorMessage -> {
|
||||
handleTaskRunnerFault(peer, message, errorMessage);
|
||||
}))
|
||||
.withTimeout(TRADE_TIMEOUT_SECONDS))
|
||||
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
|
@ -100,7 +100,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
|
|||
errorMessage -> {
|
||||
handleTaskRunnerFault(sender, request, errorMessage);
|
||||
}))
|
||||
.withTimeout(TRADE_TIMEOUT_SECONDS))
|
||||
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
|
|
|
@ -74,13 +74,13 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
|
|||
MakerSendInitTradeRequest.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
startTimeout(TRADE_TIMEOUT_SECONDS);
|
||||
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
},
|
||||
errorMessage -> {
|
||||
handleTaskRunnerFault(peer, message, errorMessage);
|
||||
}))
|
||||
.withTimeout(TRADE_TIMEOUT_SECONDS))
|
||||
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
|
|
|
@ -79,13 +79,13 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
|
|||
TakerSendInitTradeRequestToArbitrator.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
startTimeout(TRADE_TIMEOUT_SECONDS);
|
||||
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
|
||||
unlatchTrade();
|
||||
},
|
||||
errorMessage -> {
|
||||
handleError(errorMessage);
|
||||
}))
|
||||
.withTimeout(TRADE_TIMEOUT_SECONDS))
|
||||
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ import haveno.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage;
|
|||
import haveno.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToArbitrator;
|
||||
import haveno.core.trade.protocol.tasks.BuyerSendPaymentSentMessageToSeller;
|
||||
import haveno.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator;
|
||||
import haveno.core.trade.protocol.tasks.SendDepositsConfirmedMessageToSeller;
|
||||
import haveno.core.trade.protocol.tasks.TradeTask;
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
@ -158,6 +159,6 @@ public class BuyerProtocol extends DisputeProtocol {
|
|||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Class<? extends TradeTask>[] getDepositsConfirmedTasks() {
|
||||
return new Class[] { SendDepositsConfirmedMessageToArbitrator.class };
|
||||
return new Class[] { SendDepositsConfirmedMessageToSeller.class, SendDepositsConfirmedMessageToArbitrator.class };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -163,6 +163,7 @@ public class ProcessModel implements Model, PersistablePayload {
|
|||
private ObjectProperty<MessageState> paymentSentMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
|
||||
@Setter
|
||||
private ObjectProperty<MessageState> paymentSentMessageStatePropertyArbitrator = new SimpleObjectProperty<>(MessageState.UNDEFINED);
|
||||
private ObjectProperty<Boolean> paymentAccountDecryptedProperty = new SimpleObjectProperty<>(false);
|
||||
|
||||
public ProcessModel(String offerId, String accountId, PubKeyRing pubKeyRing) {
|
||||
this(offerId, accountId, pubKeyRing, new TradePeer(), new TradePeer(), new TradePeer());
|
||||
|
|
|
@ -79,13 +79,13 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
|
|||
MakerSendInitTradeRequest.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
startTimeout(TRADE_TIMEOUT_SECONDS);
|
||||
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
},
|
||||
errorMessage -> {
|
||||
handleTaskRunnerFault(peer, message, errorMessage);
|
||||
}))
|
||||
.withTimeout(TRADE_TIMEOUT_SECONDS))
|
||||
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
|
|
|
@ -80,13 +80,13 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
|
|||
TakerSendInitTradeRequestToArbitrator.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
startTimeout(TRADE_TIMEOUT_SECONDS);
|
||||
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
|
||||
unlatchTrade();
|
||||
},
|
||||
errorMessage -> {
|
||||
handleError(errorMessage);
|
||||
}))
|
||||
.withTimeout(TRADE_TIMEOUT_SECONDS))
|
||||
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ package haveno.core.trade.protocol;
|
|||
import haveno.common.ThreadUtils;
|
||||
import haveno.common.Timer;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.config.Config;
|
||||
import haveno.common.crypto.PubKeyRing;
|
||||
import haveno.common.handlers.ErrorMessageHandler;
|
||||
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.ProcessPaymentSentMessage;
|
||||
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.SellerPublishTradeStatistics;
|
||||
import haveno.core.trade.protocol.tasks.MaybeResendDisputeClosedMessageWithPayout;
|
||||
|
@ -93,8 +94,10 @@ import java.util.concurrent.CountDownLatch;
|
|||
@Slf4j
|
||||
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.";
|
||||
public static final int MAX_ATTEMPTS = 3;
|
||||
public static final long REPROCESS_DELAY_MS = 5000;
|
||||
|
||||
protected final ProcessModel processModel;
|
||||
protected final Trade trade;
|
||||
|
@ -104,6 +107,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
protected TradeResultHandler tradeResultHandler;
|
||||
protected ErrorMessageHandler errorMessageHandler;
|
||||
|
||||
private boolean depositsConfirmedTasksCalled;
|
||||
private int reprocessPaymentReceivedMessageCount;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -251,14 +255,14 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
}
|
||||
|
||||
// send deposits confirmed message if applicable
|
||||
maybeSendDepositsConfirmedMessages();
|
||||
EasyBind.subscribe(trade.stateProperty(), state -> maybeSendDepositsConfirmedMessages());
|
||||
}
|
||||
|
||||
public void maybeSendDepositsConfirmedMessages() {
|
||||
if (!trade.isInitialized() || trade.isShutDownStarted()) return;
|
||||
ThreadUtils.execute(() -> {
|
||||
if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished()) return;
|
||||
if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished() || depositsConfirmedTasksCalled) return;
|
||||
depositsConfirmedTasksCalled = true;
|
||||
synchronized (trade) {
|
||||
if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down
|
||||
latchTrade();
|
||||
|
@ -316,13 +320,13 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
MaybeSendSignContractRequest.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
startTimeout(TRADE_TIMEOUT_SECONDS);
|
||||
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
|
||||
handleTaskRunnerSuccess(sender, request);
|
||||
},
|
||||
errorMessage -> {
|
||||
handleTaskRunnerFault(sender, request, errorMessage);
|
||||
}))
|
||||
.withTimeout(TRADE_TIMEOUT_SECONDS))
|
||||
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
|
@ -354,13 +358,13 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
ProcessSignContractRequest.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
startTimeout(TRADE_TIMEOUT_SECONDS);
|
||||
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
|
||||
handleTaskRunnerSuccess(sender, message);
|
||||
},
|
||||
errorMessage -> {
|
||||
handleTaskRunnerFault(sender, message, errorMessage);
|
||||
}))
|
||||
.withTimeout(TRADE_TIMEOUT_SECONDS)) // extend timeout
|
||||
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) // extend timeout
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
} else {
|
||||
|
@ -396,16 +400,16 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
.from(sender))
|
||||
.setup(tasks(
|
||||
// TODO (woodser): validate request
|
||||
ProcessSignContractResponse.class)
|
||||
SendDepositRequest.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
startTimeout(TRADE_TIMEOUT_SECONDS);
|
||||
startTimeout(TRADE_STEP_TIMEOUT_SECONDS);
|
||||
handleTaskRunnerSuccess(sender, message);
|
||||
},
|
||||
errorMessage -> {
|
||||
handleTaskRunnerFault(sender, message, errorMessage);
|
||||
}))
|
||||
.withTimeout(TRADE_TIMEOUT_SECONDS)) // extend timeout
|
||||
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS)) // extend timeout
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
} else {
|
||||
|
@ -451,7 +455,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||
errorMessage -> {
|
||||
handleTaskRunnerFault(sender, response, errorMessage);
|
||||
}))
|
||||
.withTimeout(TRADE_TIMEOUT_SECONDS))
|
||||
.withTimeout(TRADE_STEP_TIMEOUT_SECONDS))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
|
|||
|
||||
// skip if payout tx already created
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
|
|||
trade.importMultisigHex();
|
||||
|
||||
// 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();
|
||||
trade.setPayoutTx(payoutTx);
|
||||
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||
|
|
|
@ -28,6 +28,7 @@ import haveno.core.trade.Trade.State;
|
|||
import haveno.core.trade.messages.SignContractRequest;
|
||||
import haveno.core.trade.protocol.TradeProtocol;
|
||||
import haveno.core.xmr.model.XmrAddressEntry;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
import haveno.network.p2p.SendDirectMessageListener;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.daemon.model.MoneroOutput;
|
||||
|
@ -78,37 +79,70 @@ public class MaybeSendSignContractRequest extends TradeTask {
|
|||
trade.addInitProgressStep();
|
||||
|
||||
// create deposit tx and freeze inputs
|
||||
Integer subaddressIndex = null;
|
||||
boolean reserveExactAmount = false;
|
||||
if (trade instanceof MakerTrade) {
|
||||
reserveExactAmount = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isReserveExactAmount();
|
||||
if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex();
|
||||
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;
|
||||
boolean reserveExactAmount = false;
|
||||
if (trade instanceof MakerTrade) {
|
||||
reserveExactAmount = processModel.getOpenOfferManager().getOpenOfferById(trade.getId()).get().isReserveExactAmount();
|
||||
if (reserveExactAmount) subaddressIndex = model.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.OFFER_FUNDING).get().getSubaddressIndex();
|
||||
}
|
||||
|
||||
// thaw reserved outputs
|
||||
if (trade.getSelf().getReserveTxKeyImages() != null) {
|
||||
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
|
||||
trade.getProtocol().startTimeout(TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS);
|
||||
|
||||
// collect reserved key images
|
||||
List<String> reservedKeyImages = new ArrayList<String>();
|
||||
for (MoneroOutput input : depositTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
|
||||
|
||||
// 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().setDepositTxHash(depositTx.getHash());
|
||||
trade.getSelf().setDepositTxFee(depositTx.getFee());
|
||||
trade.getSelf().setReserveTxKeyImages(reservedKeyImages);
|
||||
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()));
|
||||
}
|
||||
MoneroTxWallet depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
|
||||
|
||||
// check if trade still exists
|
||||
if (!processModel.getTradeManager().hasOpenTrade(trade)) {
|
||||
throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getId());
|
||||
}
|
||||
|
||||
// reset protocol timeout
|
||||
trade.getProtocol().startTimeout(TradeProtocol.TRADE_TIMEOUT_SECONDS);
|
||||
|
||||
// collect reserved key images
|
||||
List<String> reservedKeyImages = new ArrayList<String>();
|
||||
for (MoneroOutput input : depositTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
|
||||
|
||||
// save process state
|
||||
trade.getSelf().setDepositTx(depositTx);
|
||||
trade.getSelf().setDepositTxHash(depositTx.getHash());
|
||||
trade.getSelf().setDepositTxFee(depositTx.getFee());
|
||||
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().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
|
||||
byte[] sig = null;
|
||||
|
@ -170,4 +204,8 @@ public class MaybeSendSignContractRequest extends TradeTask {
|
|||
processModel.getTradeManager().requestPersistence();
|
||||
complete();
|
||||
}
|
||||
|
||||
private boolean isTimedOut() {
|
||||
return !processModel.getTradeManager().hasOpenTrade(trade);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
package haveno.core.trade.protocol.tasks;
|
||||
|
||||
|
||||
import haveno.common.ThreadUtils;
|
||||
import haveno.common.taskrunner.TaskRunner;
|
||||
import haveno.core.trade.Trade;
|
||||
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.getArbitrator().getNodeAddress()) && sender != trade.getArbitrator()) trade.getArbitrator().setNodeAddress(null);
|
||||
|
||||
// update multisig hex
|
||||
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
|
||||
|
||||
// decrypt seller payment account payload if key given
|
||||
if (request.getSellerPaymentAccountKey() != null && trade.getTradePeer().getPaymentAccountPayload() == null) {
|
||||
log.info(trade.getClass().getSimpleName() + " decrypting using seller payment account key");
|
||||
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
|
||||
}
|
||||
|
||||
// persist
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
// update multisig hex
|
||||
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
|
||||
|
||||
// try to import multisig hex (retry later)
|
||||
try {
|
||||
trade.importMultisigHex();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
ThreadUtils.submitToPool(() -> {
|
||||
try {
|
||||
trade.importMultisigHex();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
|
||||
// persist
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
|
|
|
@ -34,7 +34,6 @@
|
|||
|
||||
package haveno.core.trade.protocol.tasks;
|
||||
|
||||
import common.utils.GenUtils;
|
||||
import haveno.common.taskrunner.TaskRunner;
|
||||
import haveno.core.account.sign.SignedWitness;
|
||||
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());
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (trade.isPayoutPublished()) break;
|
||||
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5);
|
||||
HavenoUtils.waitFor(Trade.DEFER_PUBLISH_MS / 5);
|
||||
}
|
||||
if (!trade.isPayoutPublished()) trade.syncAndPollWallet();
|
||||
}
|
||||
|
|
|
@ -60,15 +60,6 @@ public class ProcessPaymentSentMessage extends TradeTask {
|
|||
// if seller, decrypt buyer's payment account payload
|
||||
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
|
||||
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
|
||||
trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
|
||||
|
|
|
@ -37,7 +37,7 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
|
|||
runInterceptHook();
|
||||
|
||||
// check connection
|
||||
trade.checkAndVerifyDaemonConnection();
|
||||
trade.verifyDaemonConnection();
|
||||
|
||||
// handle first time preparation
|
||||
if (trade.getArbitrator().getPaymentReceivedMessage() == null) {
|
||||
|
|
|
@ -29,14 +29,16 @@ import haveno.core.trade.protocol.TradePeer;
|
|||
import haveno.network.p2p.SendDirectMessageListener;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
public class ProcessSignContractResponse extends TradeTask {
|
||||
public class SendDepositRequest extends TradeTask {
|
||||
|
||||
@SuppressWarnings({"unused"})
|
||||
public ProcessSignContractResponse(TaskRunner taskHandler, Trade trade) {
|
||||
public SendDepositRequest(TaskRunner taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
|
@ -107,7 +109,11 @@ public class ProcessSignContractResponse extends TradeTask {
|
|||
}
|
||||
});
|
||||
} 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
|
||||
}
|
||||
} catch (Throwable t) {
|
|
@ -23,6 +23,8 @@ import haveno.core.trade.HavenoUtils;
|
|||
import haveno.core.trade.Trade;
|
||||
import haveno.core.trade.protocol.TradeProtocol;
|
||||
import haveno.core.xmr.model.XmrAddressEntry;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.daemon.model.MoneroOutput;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
|
||||
|
@ -30,6 +32,7 @@ import java.math.BigInteger;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
public class TakerReserveTradeFunds extends TradeTask {
|
||||
|
||||
public TakerReserveTradeFunds(TaskRunner taskHandler, Trade trade) {
|
||||
|
@ -42,28 +45,49 @@ public class TakerReserveTradeFunds extends TradeTask {
|
|||
runInterceptHook();
|
||||
|
||||
// create reserve tx
|
||||
BigInteger penaltyFee = HavenoUtils.multiply(trade.getAmount(), trade.getOffer().getPenaltyFeePct());
|
||||
BigInteger takerFee = trade.getTakerFee();
|
||||
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO;
|
||||
BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getSellerSecurityDepositBeforeMiningFee() : trade.getBuyerSecurityDepositBeforeMiningFee();
|
||||
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
|
||||
MoneroTxWallet reserveTx = null;
|
||||
synchronized (XmrWalletService.WALLET_LOCK) {
|
||||
|
||||
// check if trade still exists
|
||||
if (!processModel.getTradeManager().hasOpenTrade(trade)) {
|
||||
throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getId());
|
||||
// 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 takerFee = trade.getTakerFee();
|
||||
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getAmount() : BigInteger.ZERO;
|
||||
BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getSellerSecurityDepositBeforeMiningFee() : trade.getBuyerSecurityDepositBeforeMiningFee();
|
||||
String returnAddress = trade.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||
|
||||
// attempt creating reserve tx
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
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
|
||||
List<String> reservedKeyImages = new ArrayList<String>();
|
||||
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
|
||||
|
||||
// update trade state
|
||||
trade.getTaker().setReserveTxKeyImages(reservedKeyImages);
|
||||
}
|
||||
|
||||
// collect reserved key images
|
||||
List<String> reservedKeyImages = new ArrayList<String>();
|
||||
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
|
||||
|
||||
// reset protocol timeout
|
||||
trade.getProtocol().startTimeout(TradeProtocol.TRADE_TIMEOUT_SECONDS);
|
||||
|
||||
// save process state
|
||||
processModel.setReserveTx(reserveTx);
|
||||
processModel.getTaker().setReserveTxKeyImages(reservedKeyImages);
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
trade.addInitProgressStep();
|
||||
complete();
|
||||
|
@ -74,4 +98,8 @@ public class TakerReserveTradeFunds extends TradeTask {
|
|||
failed(t);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTimedOut() {
|
||||
return !processModel.getTradeManager().hasOpenTrade(trade);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ import com.google.common.util.concurrent.Service.State;
|
|||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
|
||||
import common.utils.GenUtils;
|
||||
import common.utils.JsonUtils;
|
||||
import haveno.common.ThreadUtils;
|
||||
import haveno.common.UserThread;
|
||||
|
@ -33,8 +32,6 @@ import haveno.common.util.Utilities;
|
|||
import haveno.core.api.AccountServiceListener;
|
||||
import haveno.core.api.CoreAccountService;
|
||||
import haveno.core.api.XmrConnectionService;
|
||||
import haveno.core.offer.Offer;
|
||||
import haveno.core.offer.OfferDirection;
|
||||
import haveno.core.offer.OpenOffer;
|
||||
import haveno.core.trade.BuyerTrade;
|
||||
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 MONERO_LOG_LEVEL = -1; // monero library log level, -1 to disable
|
||||
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 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 Preferences preferences;
|
||||
|
@ -155,7 +152,7 @@ public class XmrWalletService {
|
|||
private ChangeListener<? super Number> walletInitListener;
|
||||
private TradeManager tradeManager;
|
||||
private MoneroWallet wallet;
|
||||
private Object walletLock = new Object();
|
||||
public static final Object WALLET_LOCK = new Object();
|
||||
private boolean wasWalletSynced = false;
|
||||
private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
|
||||
private boolean isClosingWallet = false;
|
||||
|
@ -374,11 +371,21 @@ public class XmrWalletService {
|
|||
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.
|
||||
*/
|
||||
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);
|
||||
try {
|
||||
return future.get();
|
||||
|
@ -448,24 +455,26 @@ public class XmrWalletService {
|
|||
if (name.contains(File.separator)) throw new IllegalArgumentException("Path not expected: " + name);
|
||||
}
|
||||
|
||||
public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
|
||||
synchronized (walletLock) {
|
||||
try {
|
||||
MoneroTxWallet tx = wallet.createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));
|
||||
//printTxs("XmrWalletService.createTx", tx);
|
||||
requestSaveMainWallet();
|
||||
return tx;
|
||||
} catch (Exception e) {
|
||||
throw e;
|
||||
public MoneroTxWallet createTx(MoneroTxConfig txConfig) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
return wallet.createTx(txConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
|
||||
MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));;
|
||||
//printTxs("XmrWalletService.createTx", tx);
|
||||
requestSaveMainWallet();
|
||||
return tx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Thaw all outputs not reserved for a trade.
|
||||
*/
|
||||
public void thawUnreservedOutputs() {
|
||||
synchronized (walletLock) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
|
||||
// collect reserved outputs
|
||||
Set<String> reservedKeyImages = new HashSet<String>();
|
||||
|
@ -505,26 +514,25 @@ public class XmrWalletService {
|
|||
* @param keyImages the key images to freeze
|
||||
*/
|
||||
public void freezeOutputs(Collection<String> keyImages) {
|
||||
synchronized (walletLock) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
for (String keyImage : keyImages) wallet.freezeOutput(keyImage);
|
||||
cacheWalletInfo();
|
||||
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.
|
||||
*
|
||||
* @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) {
|
||||
synchronized (walletLock) {
|
||||
if (keyImages == null || keyImages.isEmpty()) return;
|
||||
synchronized (WALLET_LOCK) {
|
||||
for (String keyImage : keyImages) wallet.thawOutput(keyImage);
|
||||
cacheWalletInfo();
|
||||
requestSaveMainWallet();
|
||||
doPollWallet(false);
|
||||
}
|
||||
updateBalanceListeners(); // TODO (monero-java): balance listeners not notified on freeze/thaw output
|
||||
}
|
||||
|
||||
private List<Integer> getSubaddressesWithExactInput(BigInteger amount) {
|
||||
|
@ -542,40 +550,6 @@ public class XmrWalletService {
|
|||
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
|
||||
* to the sender's payout address less the penalty and mining fees.
|
||||
|
@ -587,14 +561,18 @@ public class XmrWalletService {
|
|||
* @param returnAddress return address for reserved funds
|
||||
* @param reserveExactAmount specifies to reserve the exact input amount
|
||||
* @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) {
|
||||
log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress);
|
||||
long time = System.currentTimeMillis();
|
||||
MoneroTxWallet reserveTx = createTradeTx(penaltyFee, tradeFee, sendAmount, securityDeposit, returnAddress, reserveExactAmount, preferredSubaddressIndex);
|
||||
log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time);
|
||||
return reserveTx;
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress);
|
||||
long time = System.currentTimeMillis();
|
||||
MoneroTxWallet reserveTx = createTradeTx(penaltyFee, tradeFee, sendAmount, securityDeposit, returnAddress, reserveExactAmount, preferredSubaddressIndex);
|
||||
log.info("Done creating reserve tx in {} ms", System.currentTimeMillis() - time);
|
||||
return reserveTx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -606,28 +584,23 @@ public class XmrWalletService {
|
|||
* @return MoneroTxWallet the multisig deposit tx
|
||||
*/
|
||||
public MoneroTxWallet createDepositTx(Trade trade, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
|
||||
synchronized (walletLock) {
|
||||
|
||||
// thaw reserved outputs
|
||||
if (trade.getSelf().getReserveTxKeyImages() != null) {
|
||||
thawOutputs(trade.getSelf().getReserveTxKeyImages());
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
String multisigAddress = trade.getProcessModel().getMultisigAddress();
|
||||
BigInteger tradeFee = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee();
|
||||
BigInteger sendAmount = trade instanceof BuyerTrade ? BigInteger.ZERO : trade.getAmount();
|
||||
BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
|
||||
long time = System.currentTimeMillis();
|
||||
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);
|
||||
log.info("Done creating deposit tx for trade {} {} in {} ms", trade.getClass().getSimpleName(), trade.getShortId(), System.currentTimeMillis() - time);
|
||||
return depositTx;
|
||||
}
|
||||
|
||||
// create deposit tx
|
||||
String multisigAddress = trade.getProcessModel().getMultisigAddress();
|
||||
BigInteger tradeFee = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee();
|
||||
BigInteger sendAmount = trade instanceof BuyerTrade ? BigInteger.ZERO : trade.getAmount();
|
||||
BigInteger securityDeposit = trade instanceof BuyerTrade ? trade.getBuyerSecurityDepositBeforeMiningFee() : trade.getSellerSecurityDepositBeforeMiningFee();
|
||||
long time = System.currentTimeMillis();
|
||||
log.info("Creating deposit tx with multisig address={}", multisigAddress);
|
||||
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);
|
||||
return depositTx;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// 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
|
||||
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY);
|
||||
if (!BigInteger.valueOf(0).equals(feeAmount)) txConfig.addDestination(HavenoUtils.getTradeFeeAddress(), feeAmount);
|
||||
MoneroTxWallet tradeTx = wallet.createTx(txConfig);
|
||||
MoneroTxWallet tradeTx = createTx(txConfig);
|
||||
|
||||
// freeze inputs
|
||||
List<String> keyImages = new ArrayList<String>();
|
||||
|
@ -872,7 +845,7 @@ public class XmrWalletService {
|
|||
Runnable shutDownTask = () -> {
|
||||
|
||||
// remove listeners
|
||||
synchronized (walletLock) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
if (wallet != null) {
|
||||
for (MoneroWalletListenerI listener : new HashSet<>(wallet.getListeners())) {
|
||||
wallet.removeListener(listener);
|
||||
|
@ -1174,7 +1147,7 @@ public class XmrWalletService {
|
|||
}
|
||||
|
||||
public List<MoneroTxWallet> getTxs() {
|
||||
return getTxs(new MoneroTxQuery());
|
||||
return getTxs(new MoneroTxQuery().setIncludeOutputs(true));
|
||||
}
|
||||
|
||||
public List<MoneroTxWallet> getTxs(MoneroTxQuery query) {
|
||||
|
@ -1242,7 +1215,7 @@ public class XmrWalletService {
|
|||
|
||||
// force restart main wallet if connection changed before synced
|
||||
if (!wasWalletSynced) {
|
||||
if (!Boolean.TRUE.equals(connection.isConnected())) return;
|
||||
if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return;
|
||||
ThreadUtils.submitToPool(() -> {
|
||||
log.warn("Force restarting main wallet because connection changed before inital sync");
|
||||
forceRestartMainWallet();
|
||||
|
@ -1275,7 +1248,7 @@ public class XmrWalletService {
|
|||
|
||||
private void initMainWalletIfConnected() {
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (walletLock) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
if (wallet == null && xmrConnectionService.downloadPercentageProperty().get() == 1 && !isShutDownStarted) {
|
||||
maybeInitMainWallet(true);
|
||||
if (walletInitListener != null) xmrConnectionService.downloadPercentageProperty().removeListener(walletInitListener);
|
||||
|
@ -1295,7 +1268,7 @@ public class XmrWalletService {
|
|||
}
|
||||
|
||||
private void maybeInitMainWallet(boolean sync, int numAttempts) {
|
||||
synchronized (walletLock) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
if (isShutDownStarted) return;
|
||||
|
||||
// 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()));
|
||||
if (MoneroUtils.walletExists(xmrWalletFile.getPath())) {
|
||||
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);
|
||||
|
||||
// set wallet creation date to yesterday to guarantee complete restore
|
||||
|
@ -1393,7 +1366,7 @@ public class XmrWalletService {
|
|||
|
||||
// get sync notifications from native wallet
|
||||
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() {
|
||||
@Override
|
||||
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);
|
||||
else {
|
||||
syncWithProgressLooper.stop();
|
||||
try {
|
||||
doPollWallet(true);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
wasWalletSynced = true;
|
||||
updateSyncProgress(height);
|
||||
syncWithProgressLatch.countDown();
|
||||
|
@ -1465,19 +1433,19 @@ public class XmrWalletService {
|
|||
private MoneroWalletFull createWalletFull(MoneroWalletConfig config) {
|
||||
|
||||
// must be connected to daemon
|
||||
MoneroRpcConnection connection = xmrConnectionService.getConnection();
|
||||
if (connection == null || !Boolean.TRUE.equals(connection.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet");
|
||||
if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet");
|
||||
|
||||
// create wallet
|
||||
MoneroWalletFull walletFull = null;
|
||||
try {
|
||||
|
||||
// create wallet
|
||||
MoneroRpcConnection connection = xmrConnectionService.getConnection();
|
||||
log.info("Creating full wallet " + config.getPath() + " connected to monerod=" + connection.getUri());
|
||||
long time = System.currentTimeMillis();
|
||||
config.setServer(connection);
|
||||
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");
|
||||
return walletFull;
|
||||
} catch (Exception e) {
|
||||
|
@ -1499,7 +1467,7 @@ public class XmrWalletService {
|
|||
config.setNetworkType(getMoneroNetworkType());
|
||||
config.setServer(connection);
|
||||
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());
|
||||
return walletFull;
|
||||
} catch (Exception e) {
|
||||
|
@ -1512,8 +1480,7 @@ public class XmrWalletService {
|
|||
private MoneroWalletRpc createWalletRpc(MoneroWalletConfig config, Integer port) {
|
||||
|
||||
// must be connected to daemon
|
||||
MoneroRpcConnection connection = xmrConnectionService.getConnection();
|
||||
if (connection == null || !Boolean.TRUE.equals(connection.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet");
|
||||
if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) throw new RuntimeException("Must be connected to daemon before creating wallet");
|
||||
|
||||
// create wallet
|
||||
MoneroWalletRpc walletRpc = null;
|
||||
|
@ -1521,17 +1488,18 @@ public class XmrWalletService {
|
|||
|
||||
// start monero-wallet-rpc instance
|
||||
walletRpc = startWalletRpcInstance(port, isProxyApplied(false));
|
||||
walletRpc.getRpcConnection().setPrintStackTrace(PRINT_STACK_TRACE);
|
||||
walletRpc.getRpcConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
|
||||
|
||||
// prevent wallet rpc from syncing
|
||||
walletRpc.stopSyncing();
|
||||
|
||||
// create wallet
|
||||
MoneroRpcConnection connection = xmrConnectionService.getConnection();
|
||||
log.info("Creating RPC wallet " + config.getPath() + " connected to monerod=" + connection.getUri());
|
||||
long time = System.currentTimeMillis();
|
||||
config.setServer(connection);
|
||||
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");
|
||||
return walletRpc;
|
||||
} catch (Exception e) {
|
||||
|
@ -1547,7 +1515,7 @@ public class XmrWalletService {
|
|||
|
||||
// start monero-wallet-rpc instance
|
||||
walletRpc = startWalletRpcInstance(port, applyProxyUri);
|
||||
walletRpc.getRpcConnection().setPrintStackTrace(PRINT_STACK_TRACE);
|
||||
walletRpc.getRpcConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
|
||||
|
||||
// prevent wallet rpc from syncing
|
||||
walletRpc.stopSyncing();
|
||||
|
@ -1560,7 +1528,7 @@ public class XmrWalletService {
|
|||
log.info("Opening RPC wallet " + config.getPath() + " connected to daemon " + connection.getUri());
|
||||
config.setServer(connection);
|
||||
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());
|
||||
return walletRpc;
|
||||
} catch (Exception e) {
|
||||
|
@ -1613,7 +1581,7 @@ public class XmrWalletService {
|
|||
}
|
||||
|
||||
private void onConnectionChanged(MoneroRpcConnection connection) {
|
||||
synchronized (walletLock) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
if (wallet == null || isShutDownStarted) return;
|
||||
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return;
|
||||
String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri();
|
||||
|
@ -1634,7 +1602,7 @@ public class XmrWalletService {
|
|||
|
||||
// sync wallet on new thread
|
||||
if (connection != null && !isShutDownStarted) {
|
||||
wallet.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE);
|
||||
wallet.getDaemonConnection().setPrintStackTrace(PRINT_RPC_STACK_TRACE);
|
||||
updatePollPeriod();
|
||||
}
|
||||
|
||||
|
@ -1673,7 +1641,7 @@ public class XmrWalletService {
|
|||
|
||||
private void closeMainWallet(boolean save) {
|
||||
stopPolling();
|
||||
synchronized (walletLock) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
try {
|
||||
if (wallet != null) {
|
||||
isClosingWallet = true;
|
||||
|
@ -1697,13 +1665,13 @@ public class XmrWalletService {
|
|||
private void forceRestartMainWallet() {
|
||||
log.warn("Force restarting main wallet");
|
||||
forceCloseMainWallet();
|
||||
synchronized (walletLock) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
maybeInitMainWallet(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void startPolling() {
|
||||
synchronized (walletLock) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
if (isShutDownStarted || isPollInProgress()) return;
|
||||
log.info("Starting to poll main wallet");
|
||||
updatePollPeriod();
|
||||
|
@ -1733,7 +1701,7 @@ public class XmrWalletService {
|
|||
}
|
||||
|
||||
private void setPollPeriod(long pollPeriodMs) {
|
||||
synchronized (walletLock) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
if (this.isShutDownStarted) return;
|
||||
if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return;
|
||||
this.pollPeriodMs = pollPeriodMs;
|
||||
|
@ -1751,71 +1719,51 @@ public class XmrWalletService {
|
|||
|
||||
private void doPollWallet(boolean updateTxs) {
|
||||
synchronized (pollLock) {
|
||||
if (isShutDownStarted) return;
|
||||
pollInProgress = true;
|
||||
try {
|
||||
|
||||
// log warning if wallet is too far behind daemon
|
||||
// switch to best connection if daemon is too far behind
|
||||
MoneroDaemonInfo lastInfo = xmrConnectionService.getLastInfo();
|
||||
if (lastInfo == null) {
|
||||
log.warn("Last daemon info is null");
|
||||
return;
|
||||
}
|
||||
long walletHeight = wallet.getHeight();
|
||||
if (wasWalletSynced && walletHeight < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_WARNING && !Config.baseCurrencyNetwork().isTestnet()) {
|
||||
log.warn("Main wallet is {} blocks behind monerod, wallet height={}, monerod height={},", xmrConnectionService.getTargetHeight() - walletHeight, walletHeight, lastInfo.getHeight());
|
||||
if (wasWalletSynced && walletHeight.get() < 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());
|
||||
xmrConnectionService.switchToBestConnection();
|
||||
}
|
||||
|
||||
// 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
|
||||
// TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs?
|
||||
if (updateTxs) {
|
||||
try {
|
||||
cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true));
|
||||
} catch (Exception e) { // fetch from pool can fail
|
||||
log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage());
|
||||
synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations
|
||||
synchronized (HavenoUtils.getDaemonLock()) {
|
||||
try {
|
||||
cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true));
|
||||
} catch (Exception e) { // fetch from pool can fail
|
||||
if (!isShutDownStarted) log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
// cache wallet info
|
||||
cacheWalletInfo();
|
||||
} catch (Exception e) {
|
||||
if (isShutDownStarted) return;
|
||||
if (wallet == null || isShutDownStarted) return;
|
||||
boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused");
|
||||
if (isConnectionRefused && wallet != null) forceRestartMainWallet();
|
||||
else {
|
||||
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());
|
||||
//e.printStackTrace();
|
||||
}
|
||||
if (isConnectionRefused) forceRestartMainWallet();
|
||||
else if (isWalletConnectedToDaemon()) {
|
||||
log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection());
|
||||
//e.printStackTrace();
|
||||
}
|
||||
} finally {
|
||||
pollInProgress = false;
|
||||
|
@ -1824,7 +1772,7 @@ public class XmrWalletService {
|
|||
}
|
||||
|
||||
public boolean isWalletConnectedToDaemon() {
|
||||
synchronized (walletLock) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
try {
|
||||
if (wallet == null) return false;
|
||||
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) {
|
||||
updateBalanceListeners();
|
||||
for (MoneroWalletListenerI listener : walletListeners) ThreadUtils.submitToPool(() -> listener.onBalancesChanged(newBalance, newUnlockedBalance));
|
||||
|
|
|
@ -57,10 +57,10 @@ import haveno.proto.grpc.SetAutoSwitchReply;
|
|||
import haveno.proto.grpc.SetAutoSwitchRequest;
|
||||
import haveno.proto.grpc.SetConnectionReply;
|
||||
import haveno.proto.grpc.SetConnectionRequest;
|
||||
import haveno.proto.grpc.StartCheckingConnectionsReply;
|
||||
import haveno.proto.grpc.StartCheckingConnectionsRequest;
|
||||
import haveno.proto.grpc.StopCheckingConnectionsReply;
|
||||
import haveno.proto.grpc.StopCheckingConnectionsRequest;
|
||||
import haveno.proto.grpc.StartCheckingConnectionReply;
|
||||
import haveno.proto.grpc.StartCheckingConnectionRequest;
|
||||
import haveno.proto.grpc.StopCheckingConnectionReply;
|
||||
import haveno.proto.grpc.StopCheckingConnectionRequest;
|
||||
import haveno.proto.grpc.UrlConnection;
|
||||
import static haveno.proto.grpc.XmrConnectionsGrpc.XmrConnectionsImplBase;
|
||||
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.getSetAutoSwitchMethod;
|
||||
import static haveno.proto.grpc.XmrConnectionsGrpc.getSetConnectionMethod;
|
||||
import static haveno.proto.grpc.XmrConnectionsGrpc.getStartCheckingConnectionsMethod;
|
||||
import static haveno.proto.grpc.XmrConnectionsGrpc.getStopCheckingConnectionsMethod;
|
||||
import static haveno.proto.grpc.XmrConnectionsGrpc.getStartCheckingConnectionMethod;
|
||||
import static haveno.proto.grpc.XmrConnectionsGrpc.getStopCheckingConnectionMethod;
|
||||
import io.grpc.ServerInterceptor;
|
||||
import io.grpc.stub.StreamObserver;
|
||||
import java.net.MalformedURLException;
|
||||
|
@ -102,7 +102,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
|
|||
public void addConnection(AddConnectionRequest request,
|
||||
StreamObserver<AddConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
coreApi.addMoneroConnection(toMoneroRpcConnection(request.getConnection()));
|
||||
coreApi.addXmrConnection(toMoneroRpcConnection(request.getConnection()));
|
||||
return AddConnectionReply.newBuilder().build();
|
||||
});
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
|
|||
public void removeConnection(RemoveConnectionRequest request,
|
||||
StreamObserver<RemoveConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
coreApi.removeMoneroConnection(validateUri(request.getUrl()));
|
||||
coreApi.removeXmrConnection(validateUri(request.getUrl()));
|
||||
return RemoveConnectionReply.newBuilder().build();
|
||||
});
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
|
|||
public void getConnection(GetConnectionRequest request,
|
||||
StreamObserver<GetConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
UrlConnection replyConnection = toUrlConnection(coreApi.getMoneroConnection());
|
||||
UrlConnection replyConnection = toUrlConnection(coreApi.getXmrConnection());
|
||||
GetConnectionReply.Builder builder = GetConnectionReply.newBuilder();
|
||||
if (replyConnection != null) {
|
||||
builder.setConnection(replyConnection);
|
||||
|
@ -145,10 +145,10 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
|
|||
StreamObserver<SetConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
if (request.getUrl() != null && !request.getUrl().isEmpty())
|
||||
coreApi.setMoneroConnection(validateUri(request.getUrl()));
|
||||
coreApi.setXmrConnection(validateUri(request.getUrl()));
|
||||
else if (request.hasConnection())
|
||||
coreApi.setMoneroConnection(toMoneroRpcConnection(request.getConnection()));
|
||||
else coreApi.setMoneroConnection((MoneroRpcConnection) null); // disconnect from client
|
||||
coreApi.setXmrConnection(toMoneroRpcConnection(request.getConnection()));
|
||||
else coreApi.setXmrConnection((MoneroRpcConnection) null); // disconnect from client
|
||||
return SetConnectionReply.newBuilder().build();
|
||||
});
|
||||
}
|
||||
|
@ -157,7 +157,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
|
|||
public void checkConnection(CheckConnectionRequest request,
|
||||
StreamObserver<CheckConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
MoneroRpcConnection connection = coreApi.checkMoneroConnection();
|
||||
MoneroRpcConnection connection = coreApi.checkXmrConnection();
|
||||
UrlConnection replyConnection = toUrlConnection(connection);
|
||||
CheckConnectionReply.Builder builder = CheckConnectionReply.newBuilder();
|
||||
if (replyConnection != null) {
|
||||
|
@ -179,22 +179,22 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void startCheckingConnections(StartCheckingConnectionsRequest request,
|
||||
StreamObserver<StartCheckingConnectionsReply> responseObserver) {
|
||||
public void startCheckingConnection(StartCheckingConnectionRequest request,
|
||||
StreamObserver<StartCheckingConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
int refreshMillis = request.getRefreshPeriod();
|
||||
Long refreshPeriod = refreshMillis == 0 ? null : (long) refreshMillis;
|
||||
coreApi.startCheckingMoneroConnection(refreshPeriod);
|
||||
return StartCheckingConnectionsReply.newBuilder().build();
|
||||
coreApi.startCheckingXmrConnection(refreshPeriod);
|
||||
return StartCheckingConnectionReply.newBuilder().build();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopCheckingConnections(StopCheckingConnectionsRequest request,
|
||||
StreamObserver<StopCheckingConnectionsReply> responseObserver) {
|
||||
public void stopCheckingConnection(StopCheckingConnectionRequest request,
|
||||
StreamObserver<StopCheckingConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
coreApi.stopCheckingMoneroConnection();
|
||||
return StopCheckingConnectionsReply.newBuilder().build();
|
||||
coreApi.stopCheckingXmrConnection();
|
||||
return StopCheckingConnectionReply.newBuilder().build();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -202,7 +202,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
|
|||
public void getBestAvailableConnection(GetBestAvailableConnectionRequest request,
|
||||
StreamObserver<GetBestAvailableConnectionReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
MoneroRpcConnection connection = coreApi.getBestAvailableMoneroConnection();
|
||||
MoneroRpcConnection connection = coreApi.getBestAvailableXmrConnection();
|
||||
UrlConnection replyConnection = toUrlConnection(connection);
|
||||
GetBestAvailableConnectionReply.Builder builder = GetBestAvailableConnectionReply.newBuilder();
|
||||
if (replyConnection != null) {
|
||||
|
@ -216,7 +216,7 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
|
|||
public void setAutoSwitch(SetAutoSwitchRequest request,
|
||||
StreamObserver<SetAutoSwitchReply> responseObserver) {
|
||||
handleRequest(responseObserver, () -> {
|
||||
coreApi.setMoneroConnectionAutoSwitch(request.getAutoSwitch());
|
||||
coreApi.setXmrConnectionAutoSwitch(request.getAutoSwitch());
|
||||
return SetAutoSwitchReply.newBuilder().build();
|
||||
});
|
||||
}
|
||||
|
@ -300,8 +300,8 @@ class GrpcXmrConnectionService extends XmrConnectionsImplBase {
|
|||
put(getSetConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
|
||||
put(getCheckConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
|
||||
put(getCheckConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
|
||||
put(getStartCheckingConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
|
||||
put(getStopCheckingConnectionsMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
|
||||
put(getStartCheckingConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
|
||||
put(getStopCheckingConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
|
||||
put(getGetBestAvailableConnectionMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
|
||||
put(getSetAutoSwitchMethod().getFullMethodName(), new GrpcCallRateMeter(allowedCallsPerTimeWindow, SECONDS));
|
||||
}}
|
||||
|
|
|
@ -91,6 +91,7 @@ import javafx.scene.layout.GridPane;
|
|||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.Callback;
|
||||
import monero.common.MoneroUtils;
|
||||
import monero.wallet.model.MoneroTxConfig;
|
||||
import monero.wallet.model.MoneroWalletListener;
|
||||
import net.glxn.qrgen.QRCode;
|
||||
|
@ -365,7 +366,7 @@ public class DepositView extends ActivatableView<VBox, Void> {
|
|||
|
||||
@NotNull
|
||||
private String getPaymentUri() {
|
||||
return xmrWalletService.getWallet().getPaymentUri(new MoneroTxConfig()
|
||||
return MoneroUtils.getPaymentUri(new MoneroTxConfig()
|
||||
.setAddress(addressTextField.getAddress())
|
||||
.setAmount(HavenoUtils.coinToAtomicUnits(getAmount()))
|
||||
.setNote(paymentLabelString));
|
||||
|
|
|
@ -261,7 +261,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
|
|||
|
||||
// create tx
|
||||
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)
|
||||
.setAmount(amount)
|
||||
.setAddress(withdrawToAddress)
|
||||
|
|
|
@ -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"));
|
||||
else errorMessage.set(errMessage);
|
||||
|
||||
updateButtonDisableState();
|
||||
updateSpinnerInfo();
|
||||
UserThread.execute(() -> {
|
||||
updateButtonDisableState();
|
||||
updateSpinnerInfo();
|
||||
resultHandler.run();
|
||||
|
||||
resultHandler.run();
|
||||
});
|
||||
});
|
||||
|
||||
updateButtonDisableState();
|
||||
|
|
|
@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
|||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
import haveno.common.ClockWatcher;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.app.DevEnv;
|
||||
import haveno.core.account.witness.AccountAgeWitnessService;
|
||||
import haveno.core.network.MessageState;
|
||||
|
@ -101,6 +102,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
|||
@Getter
|
||||
private final ObjectProperty<MessageState> messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
|
||||
private Subscription tradeStateSubscription;
|
||||
private Subscription paymentAccountDecryptedSubscription;
|
||||
private Subscription payoutStateSubscription;
|
||||
private Subscription messageStateSubscription;
|
||||
@Getter
|
||||
|
@ -146,6 +148,11 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
|||
tradeStateSubscription = null;
|
||||
}
|
||||
|
||||
if (paymentAccountDecryptedSubscription != null) {
|
||||
paymentAccountDecryptedSubscription.unsubscribe();
|
||||
paymentAccountDecryptedSubscription = null;
|
||||
}
|
||||
|
||||
if (payoutStateSubscription != null) {
|
||||
payoutStateSubscription.unsubscribe();
|
||||
payoutStateSubscription = null;
|
||||
|
@ -167,6 +174,10 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
|||
buyerState.set(BuyerState.UNDEFINED);
|
||||
}
|
||||
|
||||
if (paymentAccountDecryptedSubscription != null) {
|
||||
paymentAccountDecryptedSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
if (payoutStateSubscription != null) {
|
||||
payoutStateSubscription.unsubscribe();
|
||||
sellerState.set(SellerState.UNDEFINED);
|
||||
|
@ -183,6 +194,9 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
|||
tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), state -> {
|
||||
onTradeStateChanged(state);
|
||||
});
|
||||
paymentAccountDecryptedSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentAccountDecryptedProperty(), decrypted -> {
|
||||
refresh();
|
||||
});
|
||||
payoutStateSubscription = EasyBind.subscribe(trade.payoutStateProperty(), 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) {
|
||||
messageStateProperty.set(messageState);
|
||||
}
|
||||
|
|
|
@ -221,7 +221,7 @@ public class BuyerStep2View extends TradeStepView {
|
|||
|
||||
|
||||
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,
|
||||
Res.get("portfolio.pending.step2_buyer.startPaymentUsing", Res.get(paymentMethodId)),
|
||||
Layout.COMPACT_GROUP_DISTANCE);
|
||||
|
|
|
@ -93,6 +93,7 @@ import javafx.stage.StageStyle;
|
|||
import javafx.util.Callback;
|
||||
import javafx.util.StringConverter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.common.MoneroUtils;
|
||||
import monero.daemon.model.MoneroTx;
|
||||
import monero.wallet.MoneroWallet;
|
||||
import monero.wallet.model.MoneroTxConfig;
|
||||
|
@ -686,7 +687,7 @@ public class GUIUtil {
|
|||
}
|
||||
|
||||
public static String getMoneroURI(String address, BigInteger amount, String label, MoneroWallet wallet) {
|
||||
return wallet.getPaymentUri(new MoneroTxConfig()
|
||||
return MoneroUtils.getPaymentUri(new MoneroTxConfig()
|
||||
.setAddress(address)
|
||||
.setAmount(amount)
|
||||
.setNote(label));
|
||||
|
|
|
@ -315,9 +315,9 @@ service XmrConnections {
|
|||
}
|
||||
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) {
|
||||
}
|
||||
|
@ -388,15 +388,15 @@ message CheckConnectionsReply {
|
|||
repeated UrlConnection connections = 1;
|
||||
}
|
||||
|
||||
message StartCheckingConnectionsRequest {
|
||||
message StartCheckingConnectionRequest {
|
||||
int32 refresh_period = 1; // milliseconds
|
||||
}
|
||||
|
||||
message StartCheckingConnectionsReply {}
|
||||
message StartCheckingConnectionReply {}
|
||||
|
||||
message StopCheckingConnectionsRequest {}
|
||||
message StopCheckingConnectionRequest {}
|
||||
|
||||
message StopCheckingConnectionsReply {}
|
||||
message StopCheckingConnectionReply {}
|
||||
|
||||
message GetBestAvailableConnectionRequest {}
|
||||
|
||||
|
|
Loading…
Reference in a new issue