improve stability on tor, refactor startup and shut down
refactor startup sequence to improve message reliability refactor shut down sequence to finish processing messages reduce monerod requests to improve slow tor connections refactor trade wallet polling monero node service uses default data directory unless local connections service checks connection by polling daemon connections service supports getRefreshPeriodMs and shutting down add make config: monerod-stagenet-custom fix bugs in key image polling force stop wallet on deletion trade manager initializes persisted trades on data received support hardcoding monero log level and request stack traces remove xmrAddress from Arbitrator model fix formatting of MoneroWalletRpcManager
This commit is contained in:
parent
5e364f7e7e
commit
2afa5d761d
9
Makefile
9
Makefile
@ -210,6 +210,15 @@ monerod-stagenet:
|
|||||||
--bootstrap-daemon-address auto \
|
--bootstrap-daemon-address auto \
|
||||||
--rpc-access-control-origins http://localhost:8080 \
|
--rpc-access-control-origins http://localhost:8080 \
|
||||||
|
|
||||||
|
monerod-stagenet-custom:
|
||||||
|
./.localnet/monerod \
|
||||||
|
--stagenet \
|
||||||
|
--no-zmq \
|
||||||
|
--p2p-bind-port 39080 \
|
||||||
|
--rpc-bind-port 39081 \
|
||||||
|
--bootstrap-daemon-address auto \
|
||||||
|
--rpc-access-control-origins http://localhost:8080 \
|
||||||
|
|
||||||
seednode-stagenet:
|
seednode-stagenet:
|
||||||
./haveno-seednode$(APP_EXT) \
|
./haveno-seednode$(APP_EXT) \
|
||||||
--baseCurrencyNetwork=XMR_STAGENET \
|
--baseCurrencyNetwork=XMR_STAGENET \
|
||||||
|
@ -98,7 +98,9 @@ public class CoreAccountService {
|
|||||||
if (accountExists()) throw new IllegalStateException("Cannot create account if account already exists");
|
if (accountExists()) throw new IllegalStateException("Cannot create account if account already exists");
|
||||||
keyRing.generateKeys(password);
|
keyRing.generateKeys(password);
|
||||||
this.password = password;
|
this.password = password;
|
||||||
for (AccountServiceListener listener : new ArrayList<AccountServiceListener>(listeners)) listener.onAccountCreated();
|
synchronized (listeners) {
|
||||||
|
for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onAccountCreated();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void openAccount(String password) throws IncorrectPasswordException {
|
public void openAccount(String password) throws IncorrectPasswordException {
|
||||||
@ -106,7 +108,7 @@ public class CoreAccountService {
|
|||||||
if (keyRing.unlockKeys(password, false)) {
|
if (keyRing.unlockKeys(password, false)) {
|
||||||
this.password = password;
|
this.password = password;
|
||||||
synchronized (listeners) {
|
synchronized (listeners) {
|
||||||
for (AccountServiceListener listener : listeners) listener.onAccountOpened();
|
for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onAccountOpened();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException("keyRing.unlockKeys() returned false, that should never happen");
|
throw new IllegalStateException("keyRing.unlockKeys() returned false, that should never happen");
|
||||||
@ -121,7 +123,7 @@ public class CoreAccountService {
|
|||||||
keyStorage.saveKeyRing(keyRing, oldPassword, newPassword);
|
keyStorage.saveKeyRing(keyRing, oldPassword, newPassword);
|
||||||
this.password = newPassword;
|
this.password = newPassword;
|
||||||
synchronized (listeners) {
|
synchronized (listeners) {
|
||||||
for (AccountServiceListener listener : listeners) listener.onPasswordChanged(oldPassword, newPassword);
|
for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onPasswordChanged(oldPassword, newPassword);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,7 +137,7 @@ public class CoreAccountService {
|
|||||||
if (!isAccountOpen()) throw new IllegalStateException("Cannot close unopened account");
|
if (!isAccountOpen()) throw new IllegalStateException("Cannot close unopened account");
|
||||||
keyRing.lockKeys(); // closed account means the keys are locked
|
keyRing.lockKeys(); // closed account means the keys are locked
|
||||||
synchronized (listeners) {
|
synchronized (listeners) {
|
||||||
for (AccountServiceListener listener : listeners) listener.onAccountClosed();
|
for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onAccountClosed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,7 +170,7 @@ public class CoreAccountService {
|
|||||||
File dataDir = new File(config.appDataDir.getPath());
|
File dataDir = new File(config.appDataDir.getPath());
|
||||||
ZipUtils.unzipToDir(dataDir, inputStream, bufferSize);
|
ZipUtils.unzipToDir(dataDir, inputStream, bufferSize);
|
||||||
synchronized (listeners) {
|
synchronized (listeners) {
|
||||||
for (AccountServiceListener listener : listeners) listener.onAccountRestored(onShutdown);
|
for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onAccountRestored(onShutdown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,7 +178,7 @@ public class CoreAccountService {
|
|||||||
try {
|
try {
|
||||||
if (isAccountOpen()) closeAccount();
|
if (isAccountOpen()) closeAccount();
|
||||||
synchronized (listeners) {
|
synchronized (listeners) {
|
||||||
for (AccountServiceListener listener : listeners) listener.onAccountDeleted(onShutdown);
|
for (AccountServiceListener listener : new ArrayList<>(listeners)) listener.onAccountDeleted(onShutdown);
|
||||||
}
|
}
|
||||||
File dataDir = new File(config.appDataDir.getPath()); // TODO (woodser): deleting directory after gracefulShutdown() so services don't throw when they try to persist (e.g. XmrTxProofService), but gracefulShutdown() should honor read-only shutdown
|
File dataDir = new File(config.appDataDir.getPath()); // TODO (woodser): deleting directory after gracefulShutdown() so services don't throw when they try to persist (e.g. XmrTxProofService), but gracefulShutdown() should honor read-only shutdown
|
||||||
FileUtil.deleteDirectory(dataDir, null, false);
|
FileUtil.deleteDirectory(dataDir, null, false);
|
||||||
|
@ -171,7 +171,6 @@ class CoreDisputeAgentsService {
|
|||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
Arbitrator arbitrator = new Arbitrator(
|
Arbitrator arbitrator = new Arbitrator(
|
||||||
p2PService.getAddress(),
|
p2PService.getAddress(),
|
||||||
xmrWalletService.getWallet().getPrimaryAddress(), // TODO: how is this used?
|
|
||||||
keyRing.getPubKeyRing(),
|
keyRing.getPubKeyRing(),
|
||||||
new ArrayList<>(languageCodes),
|
new ArrayList<>(languageCodes),
|
||||||
new Date().getTime(),
|
new Date().getTime(),
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package haveno.core.api;
|
package haveno.core.api;
|
||||||
|
|
||||||
import haveno.common.UserThread;
|
|
||||||
import haveno.common.app.DevEnv;
|
import haveno.common.app.DevEnv;
|
||||||
import haveno.common.config.BaseCurrencyNetwork;
|
import haveno.common.config.BaseCurrencyNetwork;
|
||||||
import haveno.common.config.Config;
|
import haveno.common.config.Config;
|
||||||
@ -9,6 +8,8 @@ import haveno.core.xmr.model.EncryptedConnectionList;
|
|||||||
import haveno.core.xmr.setup.DownloadListener;
|
import haveno.core.xmr.setup.DownloadListener;
|
||||||
import haveno.core.xmr.setup.WalletsSetup;
|
import haveno.core.xmr.setup.WalletsSetup;
|
||||||
import haveno.network.Socks5ProxyProvider;
|
import haveno.network.Socks5ProxyProvider;
|
||||||
|
import haveno.network.p2p.P2PService;
|
||||||
|
import haveno.network.p2p.P2PServiceListener;
|
||||||
import javafx.beans.property.IntegerProperty;
|
import javafx.beans.property.IntegerProperty;
|
||||||
import javafx.beans.property.LongProperty;
|
import javafx.beans.property.LongProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
@ -57,8 +58,9 @@ public final class CoreMoneroConnectionsService {
|
|||||||
new MoneroRpcConnection("http://127.0.0.1:28081").setPriority(1)
|
new MoneroRpcConnection("http://127.0.0.1:28081").setPriority(1)
|
||||||
));
|
));
|
||||||
DEFAULT_CONNECTIONS.put(BaseCurrencyNetwork.XMR_STAGENET, Arrays.asList(
|
DEFAULT_CONNECTIONS.put(BaseCurrencyNetwork.XMR_STAGENET, Arrays.asList(
|
||||||
new MoneroRpcConnection("http://127.0.0.1:38081").setPriority(1), // localhost is first priority, use loopback address to match url generated by local node service
|
new MoneroRpcConnection("http://127.0.0.1:38081").setPriority(1), // localhost is first priority, use loopback address 127.0.0.1 to match url used by local node service
|
||||||
new MoneroRpcConnection("http://45.63.8.26:38081").setPriority(2),
|
new MoneroRpcConnection("http://127.0.0.1:39081").setPriority(2), // from makefile: `monerod-stagenet-custom`
|
||||||
|
new MoneroRpcConnection("http://45.63.8.26:38081").setPriority(2), // hosted by haveno
|
||||||
new MoneroRpcConnection("http://stagenet.community.rino.io:38081").setPriority(2),
|
new MoneroRpcConnection("http://stagenet.community.rino.io:38081").setPriority(2),
|
||||||
new MoneroRpcConnection("http://stagenet.melo.tools:38081").setPriority(2),
|
new MoneroRpcConnection("http://stagenet.melo.tools:38081").setPriority(2),
|
||||||
new MoneroRpcConnection("http://node.sethforprivacy.com:38089").setPriority(2),
|
new MoneroRpcConnection("http://node.sethforprivacy.com:38089").setPriority(2),
|
||||||
@ -89,14 +91,17 @@ public final class CoreMoneroConnectionsService {
|
|||||||
private final DownloadListener downloadListener = new DownloadListener();
|
private final DownloadListener downloadListener = new DownloadListener();
|
||||||
private Socks5ProxyProvider socks5ProxyProvider;
|
private Socks5ProxyProvider socks5ProxyProvider;
|
||||||
|
|
||||||
|
private boolean isInitialized;
|
||||||
private MoneroDaemonRpc daemon;
|
private MoneroDaemonRpc daemon;
|
||||||
@Getter
|
@Getter
|
||||||
private MoneroDaemonInfo lastInfo;
|
private MoneroDaemonInfo lastInfo;
|
||||||
private boolean isInitialized = false;
|
private TaskLooper daemonPollLooper;
|
||||||
private TaskLooper updateDaemonLooper;;
|
private boolean isShutDownStarted;
|
||||||
|
private List<MoneroConnectionManagerListener> listeners = new ArrayList<>();
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public CoreMoneroConnectionsService(Config config,
|
public CoreMoneroConnectionsService(P2PService p2PService,
|
||||||
|
Config config,
|
||||||
CoreContext coreContext,
|
CoreContext coreContext,
|
||||||
WalletsSetup walletsSetup,
|
WalletsSetup walletsSetup,
|
||||||
CoreAccountService accountService,
|
CoreAccountService accountService,
|
||||||
@ -112,33 +117,41 @@ public final class CoreMoneroConnectionsService {
|
|||||||
this.connectionList = connectionList;
|
this.connectionList = connectionList;
|
||||||
this.socks5ProxyProvider = socks5ProxyProvider;
|
this.socks5ProxyProvider = socks5ProxyProvider;
|
||||||
|
|
||||||
// initialize after account open and basic setup
|
// initialize when connected to p2p network
|
||||||
walletsSetup.addSetupTaskHandler(() -> { // TODO: use something better than legacy WalletSetup for notification to initialize
|
p2PService.addP2PServiceListener(new P2PServiceListener() {
|
||||||
|
|
||||||
// initialize from connections read from disk
|
|
||||||
initialize();
|
|
||||||
|
|
||||||
// listen for account to be opened or password changed
|
|
||||||
accountService.addListener(new AccountServiceListener() {
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAccountOpened() {
|
public void onTorNodeReady() {
|
||||||
try {
|
|
||||||
log.info(getClass() + ".onAccountOpened() called");
|
|
||||||
initialize();
|
initialize();
|
||||||
} catch (Exception e) {
|
}
|
||||||
e.printStackTrace();
|
@Override
|
||||||
throw new RuntimeException(e);
|
public void onHiddenServicePublished() {}
|
||||||
|
@Override
|
||||||
|
public void onDataReceived() {}
|
||||||
|
@Override
|
||||||
|
public void onNoSeedNodeAvailable() {}
|
||||||
|
@Override
|
||||||
|
public void onNoPeersAvailable() {}
|
||||||
|
@Override
|
||||||
|
public void onUpdatedDataReceived() {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onShutDownStarted() {
|
||||||
|
log.info("{}.onShutDownStarted()", getClass().getSimpleName());
|
||||||
|
isShutDownStarted = true;
|
||||||
|
synchronized (this) {
|
||||||
|
// ensures request not in progress
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public void shutDown() {
|
||||||
public void onPasswordChanged(String oldPassword, String newPassword) {
|
log.info("Shutting down started for {}", getClass().getSimpleName());
|
||||||
log.info(getClass() + ".onPasswordChanged({}, {}) called", oldPassword, newPassword);
|
synchronized (lock) {
|
||||||
connectionList.changePassword(oldPassword, newPassword);
|
isInitialized = false;
|
||||||
|
if (daemonPollLooper != null) daemonPollLooper.stop();
|
||||||
|
connectionManager.stopCheckingConnection();
|
||||||
|
daemon = null;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------ CONNECTION MANAGEMENT -------------------------
|
// ------------------------ CONNECTION MANAGEMENT -------------------------
|
||||||
@ -154,11 +167,11 @@ public final class CoreMoneroConnectionsService {
|
|||||||
|
|
||||||
public void addListener(MoneroConnectionManagerListener listener) {
|
public void addListener(MoneroConnectionManagerListener listener) {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
connectionManager.addListener(listener);
|
listeners.add(listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isConnected() {
|
public Boolean isConnected() {
|
||||||
return connectionManager.isConnected();
|
return connectionManager.isConnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,8 +238,8 @@ public final class CoreMoneroConnectionsService {
|
|||||||
public void startCheckingConnection(Long refreshPeriod) {
|
public void startCheckingConnection(Long refreshPeriod) {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
accountService.checkAccountOpen();
|
accountService.checkAccountOpen();
|
||||||
connectionManager.startCheckingConnection(refreshPeriod == null ? getDefaultRefreshPeriodMs() : refreshPeriod);
|
|
||||||
connectionList.setRefreshPeriod(refreshPeriod);
|
connectionList.setRefreshPeriod(refreshPeriod);
|
||||||
|
updatePolling();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,17 +270,11 @@ public final class CoreMoneroConnectionsService {
|
|||||||
return getConnection() != null && HavenoUtils.isLocalHost(getConnection().getUri());
|
return getConnection() != null && HavenoUtils.isLocalHost(getConnection().getUri());
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getDefaultRefreshPeriodMs() {
|
public long getRefreshPeriodMs() {
|
||||||
if (daemon == null) return REFRESH_PERIOD_LOCAL_MS;
|
if (connectionList.getRefreshPeriod() < 0 || connectionList.getRefreshPeriod() > 0) {
|
||||||
else {
|
return connectionList.getRefreshPeriod();
|
||||||
if (isConnectionLocal()) {
|
|
||||||
if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_HTTP_MS; // refresh slower if syncing or bootstrapped
|
|
||||||
else return REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing
|
|
||||||
} else if (getConnection().isOnion()) {
|
|
||||||
return REFRESH_PERIOD_ONION_MS;
|
|
||||||
} else {
|
} else {
|
||||||
return REFRESH_PERIOD_HTTP_MS;
|
return getDefaultRefreshPeriodMs();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,7 +336,48 @@ public final class CoreMoneroConnectionsService {
|
|||||||
|
|
||||||
// ------------------------------- HELPERS --------------------------------
|
// ------------------------------- HELPERS --------------------------------
|
||||||
|
|
||||||
|
private long getDefaultRefreshPeriodMs() {
|
||||||
|
if (daemon == null) return REFRESH_PERIOD_LOCAL_MS;
|
||||||
|
else {
|
||||||
|
if (isConnectionLocal()) {
|
||||||
|
if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_HTTP_MS; // refresh slower if syncing or bootstrapped
|
||||||
|
else return REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing
|
||||||
|
} else if (getConnection().isOnion()) {
|
||||||
|
return REFRESH_PERIOD_ONION_MS;
|
||||||
|
} else {
|
||||||
|
return REFRESH_PERIOD_HTTP_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void initialize() {
|
private void initialize() {
|
||||||
|
|
||||||
|
// initialize connections
|
||||||
|
initializeConnections();
|
||||||
|
|
||||||
|
// listen for account to be opened or password changed
|
||||||
|
accountService.addListener(new AccountServiceListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAccountOpened() {
|
||||||
|
try {
|
||||||
|
log.info(getClass() + ".onAccountOpened() called");
|
||||||
|
initialize();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPasswordChanged(String oldPassword, String newPassword) {
|
||||||
|
log.info(getClass() + ".onPasswordChanged({}, {}) called", oldPassword, newPassword);
|
||||||
|
connectionList.changePassword(oldPassword, newPassword);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeConnections() {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
|
|
||||||
// reset connection manager
|
// reset connection manager
|
||||||
@ -365,12 +413,11 @@ public final class CoreMoneroConnectionsService {
|
|||||||
currentConnectionUri = Optional.of(connection.getUri());
|
currentConnectionUri = Optional.of(connection.getUri());
|
||||||
}
|
}
|
||||||
|
|
||||||
// restore configuration and check connection
|
// restore configuration
|
||||||
if ("".equals(config.xmrNode)) connectionManager.setAutoSwitch(connectionList.getAutoSwitch());
|
if ("".equals(config.xmrNode)) connectionManager.setAutoSwitch(connectionList.getAutoSwitch());
|
||||||
long refreshPeriod = connectionList.getRefreshPeriod();
|
|
||||||
if (refreshPeriod > 0) connectionManager.startCheckingConnection(refreshPeriod);
|
// check connection
|
||||||
else if (refreshPeriod == 0) connectionManager.startCheckingConnection();
|
checkConnection();
|
||||||
else checkConnection();
|
|
||||||
|
|
||||||
// run once
|
// run once
|
||||||
if (!isInitialized) {
|
if (!isInitialized) {
|
||||||
@ -392,14 +439,16 @@ public final class CoreMoneroConnectionsService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// if offline and last connection is local, start local node if offline
|
// if offline and last connection is local node, start local node if it's offline
|
||||||
currentConnectionUri.ifPresent(uri -> {
|
currentConnectionUri.ifPresent(uri -> {
|
||||||
try {
|
try {
|
||||||
if (!connectionManager.isConnected() && HavenoUtils.isLocalHost(uri) && !nodeService.isOnline()) {
|
if (!connectionManager.isConnected() && nodeService.equalsUri(uri) && !nodeService.isOnline()) {
|
||||||
|
log.info("Starting local node");
|
||||||
nodeService.startMoneroNode();
|
nodeService.startMoneroNode();
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Unable to start local monero node: " + e.getMessage());
|
log.warn("Unable to start local monero node: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -417,22 +466,23 @@ public final class CoreMoneroConnectionsService {
|
|||||||
// if using legacy desktop app, connect to best available connection
|
// if using legacy desktop app, connect to best available connection
|
||||||
if (!coreContext.isApiUser() && "".equals(config.xmrNode)) {
|
if (!coreContext.isApiUser() && "".equals(config.xmrNode)) {
|
||||||
connectionManager.setAutoSwitch(true);
|
connectionManager.setAutoSwitch(true);
|
||||||
connectionManager.setConnection(connectionManager.getBestAvailableConnection());
|
MoneroRpcConnection bestConnection = connectionManager.getBestAvailableConnection();
|
||||||
|
log.info("Setting best available connection for monerod: " + (bestConnection == null ? null : bestConnection.getUri()));
|
||||||
|
connectionManager.setConnection(bestConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// register connection change listener
|
// register connection change listener
|
||||||
if (!isInitialized) {
|
|
||||||
connectionManager.addListener(this::onConnectionChanged);
|
connectionManager.addListener(this::onConnectionChanged);
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
}
|
|
||||||
|
|
||||||
// announce connection
|
// update connection state
|
||||||
onConnectionChanged(connectionManager.getConnection());
|
onConnectionChanged(connectionManager.getConnection());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onConnectionChanged(MoneroRpcConnection currentConnection) {
|
private void onConnectionChanged(MoneroRpcConnection currentConnection) {
|
||||||
// TODO: ignore if shutdown
|
log.info("CoreMoneroConnetionsService.onConnectionChanged() uri={}, connected=", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : currentConnection.isConnected());
|
||||||
|
if (isShutDownStarted) return;
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
if (currentConnection == null) {
|
if (currentConnection == null) {
|
||||||
daemon = null;
|
daemon = null;
|
||||||
@ -443,31 +493,46 @@ public final class CoreMoneroConnectionsService {
|
|||||||
connectionList.addConnection(currentConnection);
|
connectionList.addConnection(currentConnection);
|
||||||
connectionList.setCurrentConnectionUri(currentConnection.getUri());
|
connectionList.setCurrentConnectionUri(currentConnection.getUri());
|
||||||
}
|
}
|
||||||
startPollingDaemon();
|
}
|
||||||
|
updatePolling();
|
||||||
|
|
||||||
|
// notify listeners
|
||||||
|
synchronized (lock) {
|
||||||
|
for (MoneroConnectionManagerListener listener : listeners) listener.onConnectionChanged(currentConnection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startPollingDaemon() {
|
private void updatePolling() {
|
||||||
|
new Thread(() -> {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
updateDaemonInfo();
|
stopPolling();
|
||||||
if (updateDaemonLooper != null) updateDaemonLooper.stop();
|
if (getRefreshPeriodMs() > 0) startPolling();
|
||||||
UserThread.runAfter(() -> {
|
|
||||||
synchronized (lock) {
|
|
||||||
if (updateDaemonLooper != null) updateDaemonLooper.stop();
|
|
||||||
updateDaemonLooper = new TaskLooper(() -> updateDaemonInfo());
|
|
||||||
updateDaemonLooper.start(getDefaultRefreshPeriodMs());
|
|
||||||
}
|
}
|
||||||
}, getDefaultRefreshPeriodMs() / 1000);
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startPolling() {
|
||||||
|
synchronized (lock) {
|
||||||
|
if (daemonPollLooper != null) daemonPollLooper.stop();
|
||||||
|
daemonPollLooper = new TaskLooper(() -> pollDaemonInfo());
|
||||||
|
daemonPollLooper.start(getRefreshPeriodMs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateDaemonInfo() {
|
private void stopPolling() {
|
||||||
|
synchronized (lock) {
|
||||||
|
if (daemonPollLooper != null) daemonPollLooper.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pollDaemonInfo() {
|
||||||
|
if (isShutDownStarted) return;
|
||||||
try {
|
try {
|
||||||
log.trace("Updating daemon info");
|
log.debug("Polling daemon info");
|
||||||
if (daemon == null) throw new RuntimeException("No daemon connection");
|
if (daemon == null) throw new RuntimeException("No daemon connection");
|
||||||
|
synchronized (this) {
|
||||||
lastInfo = daemon.getInfo();
|
lastInfo = daemon.getInfo();
|
||||||
//System.out.println(JsonUtils.serialize(lastInfo));
|
}
|
||||||
//System.out.println(JsonUtils.serialize(daemon.getSyncInfo()));
|
|
||||||
chainHeight.set(lastInfo.getTargetHeight() == 0 ? lastInfo.getHeight() : lastInfo.getTargetHeight());
|
chainHeight.set(lastInfo.getTargetHeight() == 0 ? lastInfo.getHeight() : lastInfo.getTargetHeight());
|
||||||
|
|
||||||
// set peer connections
|
// set peer connections
|
||||||
@ -481,17 +546,30 @@ public final class CoreMoneroConnectionsService {
|
|||||||
numPeers.set(lastInfo.getNumOutgoingConnections() + lastInfo.getNumIncomingConnections());
|
numPeers.set(lastInfo.getNumOutgoingConnections() + lastInfo.getNumIncomingConnections());
|
||||||
peers.set(new ArrayList<MoneroPeer>());
|
peers.set(new ArrayList<MoneroPeer>());
|
||||||
|
|
||||||
|
// log recovery message
|
||||||
if (lastErrorTimestamp != null) {
|
if (lastErrorTimestamp != null) {
|
||||||
log.info("Successfully fetched daemon info after previous error");
|
log.info("Successfully fetched daemon info after previous error");
|
||||||
lastErrorTimestamp = null;
|
lastErrorTimestamp = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update and notify connected state
|
||||||
|
if (!Boolean.TRUE.equals(connectionManager.isConnected())) {
|
||||||
|
connectionManager.checkConnection();
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (lastErrorTimestamp == null || System.currentTimeMillis() - lastErrorTimestamp > MIN_ERROR_LOG_PERIOD_MS) {
|
|
||||||
|
// log error message periodically
|
||||||
|
if ((lastErrorTimestamp == null || System.currentTimeMillis() - lastErrorTimestamp > MIN_ERROR_LOG_PERIOD_MS)) {
|
||||||
lastErrorTimestamp = System.currentTimeMillis();
|
lastErrorTimestamp = System.currentTimeMillis();
|
||||||
log.warn("Could not update daemon info: " + e.getMessage());
|
log.warn("Could not update daemon info: " + e.getMessage());
|
||||||
if (DevEnv.isDevMode()) e.printStackTrace();
|
if (DevEnv.isDevMode()) e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check connection which notifies of changes
|
||||||
|
synchronized (this) {
|
||||||
if (connectionManager.getAutoSwitch()) connectionManager.setConnection(connectionManager.getBestAvailableConnection());
|
if (connectionManager.getAutoSwitch()) connectionManager.setConnection(connectionManager.getBestAvailableConnection());
|
||||||
|
else connectionManager.checkConnection();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import haveno.core.trade.HavenoUtils;
|
|||||||
import haveno.core.user.Preferences;
|
import haveno.core.user.Preferences;
|
||||||
import haveno.core.xmr.MoneroNodeSettings;
|
import haveno.core.xmr.MoneroNodeSettings;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import monero.common.MoneroUtils;
|
||||||
import monero.daemon.MoneroDaemonRpc;
|
import monero.daemon.MoneroDaemonRpc;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@ -44,7 +45,7 @@ public class CoreMoneroNodeService {
|
|||||||
public static final String MONEROD_DIR = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? System.getProperty("user.dir") + File.separator + ".localnet" : Config.appDataDir().getAbsolutePath();
|
public static final String MONEROD_DIR = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? System.getProperty("user.dir") + File.separator + ".localnet" : Config.appDataDir().getAbsolutePath();
|
||||||
public static final String MONEROD_NAME = Utilities.isWindows() ? "monerod.exe" : "monerod";
|
public static final String MONEROD_NAME = Utilities.isWindows() ? "monerod.exe" : "monerod";
|
||||||
public static final String MONEROD_PATH = MONEROD_DIR + File.separator + MONEROD_NAME;
|
public static final String MONEROD_PATH = MONEROD_DIR + File.separator + MONEROD_NAME;
|
||||||
private static final String MONEROD_DATADIR = MONEROD_DIR + File.separator + Config.baseCurrencyNetwork().toString().toLowerCase() + File.separator + "node1";
|
private static final String MONEROD_DATADIR = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? MONEROD_DIR + File.separator + Config.baseCurrencyNetwork().toString().toLowerCase() + File.separator + "node1" : null; // use default directory unless local
|
||||||
|
|
||||||
private final Preferences preferences;
|
private final Preferences preferences;
|
||||||
private final List<MoneroNodeServiceListener> listeners = new ArrayList<>();
|
private final List<MoneroNodeServiceListener> listeners = new ArrayList<>();
|
||||||
@ -59,15 +60,17 @@ public class CoreMoneroNodeService {
|
|||||||
|
|
||||||
// client to the local Monero node
|
// client to the local Monero node
|
||||||
private MoneroDaemonRpc daemon;
|
private MoneroDaemonRpc daemon;
|
||||||
|
private static Integer rpcPort;
|
||||||
@Inject
|
static {
|
||||||
public CoreMoneroNodeService(Preferences preferences) {
|
|
||||||
this.preferences = preferences;
|
|
||||||
Integer rpcPort = null;
|
|
||||||
if (Config.baseCurrencyNetwork().isMainnet()) rpcPort = 18081;
|
if (Config.baseCurrencyNetwork().isMainnet()) rpcPort = 18081;
|
||||||
else if (Config.baseCurrencyNetwork().isTestnet()) rpcPort = 28081;
|
else if (Config.baseCurrencyNetwork().isTestnet()) rpcPort = 28081;
|
||||||
else if (Config.baseCurrencyNetwork().isStagenet()) rpcPort = 38081;
|
else if (Config.baseCurrencyNetwork().isStagenet()) rpcPort = 38081;
|
||||||
else throw new RuntimeException("Base network is not local testnet, stagenet, or mainnet");
|
else throw new RuntimeException("Base network is not local testnet, stagenet, or mainnet");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public CoreMoneroNodeService(Preferences preferences) {
|
||||||
|
this.preferences = preferences;
|
||||||
this.daemon = new MoneroDaemonRpc("http://" + HavenoUtils.LOOPBACK_HOST + ":" + rpcPort);
|
this.daemon = new MoneroDaemonRpc("http://" + HavenoUtils.LOOPBACK_HOST + ":" + rpcPort);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +93,10 @@ public class CoreMoneroNodeService {
|
|||||||
return daemon.getRpcConnection().checkConnection(5000);
|
return daemon.getRpcConnection().checkConnection(5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean equalsUri(String uri) {
|
||||||
|
return HavenoUtils.isLocalHost(uri) && MoneroUtils.parseUri(uri).getPort() == rpcPort;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if local Monero node is online.
|
* Check if local Monero node is online.
|
||||||
*/
|
*/
|
||||||
|
@ -34,25 +34,28 @@ import haveno.common.setup.UncaughtExceptionHandler;
|
|||||||
import haveno.common.util.Utilities;
|
import haveno.common.util.Utilities;
|
||||||
import haveno.core.api.AccountServiceListener;
|
import haveno.core.api.AccountServiceListener;
|
||||||
import haveno.core.api.CoreAccountService;
|
import haveno.core.api.CoreAccountService;
|
||||||
|
import haveno.core.api.CoreMoneroConnectionsService;
|
||||||
|
import haveno.core.offer.OfferBookService;
|
||||||
import haveno.core.offer.OpenOfferManager;
|
import haveno.core.offer.OpenOfferManager;
|
||||||
import haveno.core.provider.price.PriceFeedService;
|
import haveno.core.provider.price.PriceFeedService;
|
||||||
import haveno.core.setup.CorePersistedDataHost;
|
import haveno.core.setup.CorePersistedDataHost;
|
||||||
import haveno.core.setup.CoreSetup;
|
import haveno.core.setup.CoreSetup;
|
||||||
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||||
import haveno.core.trade.HavenoUtils;
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.trade.TradeManager;
|
|
||||||
import haveno.core.trade.statistics.TradeStatisticsManager;
|
import haveno.core.trade.statistics.TradeStatisticsManager;
|
||||||
import haveno.core.trade.txproof.xmr.XmrTxProofService;
|
import haveno.core.trade.txproof.xmr.XmrTxProofService;
|
||||||
import haveno.core.xmr.setup.WalletsSetup;
|
import haveno.core.xmr.setup.WalletsSetup;
|
||||||
import haveno.core.xmr.wallet.BtcWalletService;
|
import haveno.core.xmr.wallet.BtcWalletService;
|
||||||
import haveno.core.xmr.wallet.XmrWalletService;
|
import haveno.core.xmr.wallet.XmrWalletService;
|
||||||
import haveno.network.p2p.P2PService;
|
import haveno.network.p2p.P2PService;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import java.io.Console;
|
import java.io.Console;
|
||||||
import java.util.Arrays;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -74,7 +77,8 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
|||||||
protected Injector injector;
|
protected Injector injector;
|
||||||
protected AppModule module;
|
protected AppModule module;
|
||||||
protected Config config;
|
protected Config config;
|
||||||
private boolean isShutdownInProgress;
|
@Getter
|
||||||
|
protected boolean isShutdownInProgress;
|
||||||
private boolean isReadOnly;
|
private boolean isReadOnly;
|
||||||
private Thread keepRunningThread;
|
private Thread keepRunningThread;
|
||||||
private AtomicInteger keepRunningResult = new AtomicInteger(EXIT_SUCCESS);
|
private AtomicInteger keepRunningResult = new AtomicInteger(EXIT_SUCCESS);
|
||||||
@ -303,12 +307,13 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
|||||||
// This might need to be overwritten in case the application is not using all modules
|
// This might need to be overwritten in case the application is not using all modules
|
||||||
@Override
|
@Override
|
||||||
public void gracefulShutDown(ResultHandler onShutdown, boolean systemExit) {
|
public void gracefulShutDown(ResultHandler onShutdown, boolean systemExit) {
|
||||||
log.info("Start graceful shutDown");
|
log.info("Starting graceful shut down of {}", getClass().getSimpleName());
|
||||||
|
|
||||||
|
// ignore if shut down in progress
|
||||||
if (isShutdownInProgress) {
|
if (isShutdownInProgress) {
|
||||||
log.info("Ignoring call to gracefulShutDown, already in progress");
|
log.info("Ignoring call to gracefulShutDown, already in progress");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isShutdownInProgress = true;
|
isShutdownInProgress = true;
|
||||||
|
|
||||||
ResultHandler resultHandler;
|
ResultHandler resultHandler;
|
||||||
@ -328,33 +333,41 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
// notify trade protocols and wallets to prepare for shut down before shutting down
|
||||||
|
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||||
|
tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted());
|
||||||
|
tasks.add(() -> injector.getInstance(CoreMoneroConnectionsService.class).onShutDownStarted());
|
||||||
|
HavenoUtils.executeTasks(tasks); // notify in parallel
|
||||||
|
|
||||||
injector.getInstance(PriceFeedService.class).shutDown();
|
injector.getInstance(PriceFeedService.class).shutDown();
|
||||||
injector.getInstance(ArbitratorManager.class).shutDown();
|
injector.getInstance(ArbitratorManager.class).shutDown();
|
||||||
injector.getInstance(TradeStatisticsManager.class).shutDown();
|
injector.getInstance(TradeStatisticsManager.class).shutDown();
|
||||||
injector.getInstance(XmrTxProofService.class).shutDown();
|
injector.getInstance(XmrTxProofService.class).shutDown();
|
||||||
injector.getInstance(AvoidStandbyModeService.class).shutDown();
|
injector.getInstance(AvoidStandbyModeService.class).shutDown();
|
||||||
log.info("TradeManager and XmrWalletService shutdown started");
|
|
||||||
HavenoUtils.executeTasks(Arrays.asList( // shut down trade and main wallets at same time
|
// shut down open offer manager
|
||||||
() -> injector.getInstance(TradeManager.class).shutDown(),
|
log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService");
|
||||||
() -> injector.getInstance(XmrWalletService.class).shutDown(!isReadOnly)));
|
|
||||||
log.info("OpenOfferManager shutdown started");
|
|
||||||
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
|
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
|
||||||
log.info("OpenOfferManager shutdown completed");
|
|
||||||
|
|
||||||
injector.getInstance(BtcWalletService.class).shutDown();
|
// shut down offer book service
|
||||||
|
injector.getInstance(OfferBookService.class).shutDown();
|
||||||
// We need to shutdown BitcoinJ before the P2PService as it uses Tor.
|
|
||||||
WalletsSetup walletsSetup = injector.getInstance(WalletsSetup.class);
|
|
||||||
walletsSetup.shutDownComplete.addListener((ov, o, n) -> {
|
|
||||||
log.info("WalletsSetup shutdown completed");
|
|
||||||
|
|
||||||
|
// shut down p2p service
|
||||||
injector.getInstance(P2PService.class).shutDown(() -> {
|
injector.getInstance(P2PService.class).shutDown(() -> {
|
||||||
log.info("P2PService shutdown completed");
|
log.info("Done shutting down OpenOfferManager, OfferBookService, and P2PService");
|
||||||
|
|
||||||
|
// shut down monero wallets and connections
|
||||||
|
injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
|
||||||
|
log.info("Graceful shutdown completed. Exiting now.");
|
||||||
module.close(injector);
|
module.close(injector);
|
||||||
completeShutdown(resultHandler, EXIT_SUCCESS, systemExit);
|
completeShutdown(resultHandler, EXIT_SUCCESS, systemExit);
|
||||||
});
|
});
|
||||||
|
injector.getInstance(BtcWalletService.class).shutDown();
|
||||||
|
injector.getInstance(XmrWalletService.class).shutDown();
|
||||||
|
injector.getInstance(CoreMoneroConnectionsService.class).shutDown();
|
||||||
|
injector.getInstance(WalletsSetup.class).shutDown();
|
||||||
});
|
});
|
||||||
walletsSetup.shutDown();
|
|
||||||
});
|
});
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
log.error("App shutdown failed with exception {}", t.toString());
|
log.error("App shutdown failed with exception {}", t.toString());
|
||||||
|
@ -44,6 +44,7 @@ import haveno.core.support.dispute.Dispute;
|
|||||||
import haveno.core.support.dispute.arbitration.ArbitrationManager;
|
import haveno.core.support.dispute.arbitration.ArbitrationManager;
|
||||||
import haveno.core.support.dispute.mediation.MediationManager;
|
import haveno.core.support.dispute.mediation.MediationManager;
|
||||||
import haveno.core.support.dispute.refund.RefundManager;
|
import haveno.core.support.dispute.refund.RefundManager;
|
||||||
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.trade.TradeManager;
|
import haveno.core.trade.TradeManager;
|
||||||
import haveno.core.trade.TradeTxException;
|
import haveno.core.trade.TradeTxException;
|
||||||
import haveno.core.user.Preferences;
|
import haveno.core.user.Preferences;
|
||||||
@ -246,7 +247,7 @@ public class HavenoSetup {
|
|||||||
this.refundManager = refundManager;
|
this.refundManager = refundManager;
|
||||||
this.arbitrationManager = arbitrationManager;
|
this.arbitrationManager = arbitrationManager;
|
||||||
|
|
||||||
xmrWalletService.setHavenoSetup(this);
|
HavenoUtils.havenoSetup = this;
|
||||||
|
|
||||||
MemPoolSpaceTxBroadcaster.init(socks5ProxyProvider, preferences, localBitcoinNode);
|
MemPoolSpaceTxBroadcaster.init(socks5ProxyProvider, preferences, localBitcoinNode);
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@ public class P2PNetworkSetup {
|
|||||||
this.preferences = preferences;
|
this.preferences = preferences;
|
||||||
}
|
}
|
||||||
|
|
||||||
BooleanProperty init(Runnable initWalletServiceHandler, @Nullable Consumer<Boolean> displayTorNetworkSettingsHandler) {
|
BooleanProperty init(Runnable onReadyHandler, @Nullable Consumer<Boolean> displayTorNetworkSettingsHandler) {
|
||||||
StringProperty bootstrapState = new SimpleStringProperty();
|
StringProperty bootstrapState = new SimpleStringProperty();
|
||||||
StringProperty bootstrapWarning = new SimpleStringProperty();
|
StringProperty bootstrapWarning = new SimpleStringProperty();
|
||||||
BooleanProperty hiddenServicePublished = new SimpleBooleanProperty();
|
BooleanProperty hiddenServicePublished = new SimpleBooleanProperty();
|
||||||
@ -146,8 +146,8 @@ public class P2PNetworkSetup {
|
|||||||
priceFeedService.setCurrencyCodeOnInit();
|
priceFeedService.setCurrencyCodeOnInit();
|
||||||
priceFeedService.requestPrices();
|
priceFeedService.requestPrices();
|
||||||
|
|
||||||
// invoke handler to initialize wallet
|
// invoke handler when network ready
|
||||||
initWalletServiceHandler.run();
|
onReadyHandler.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -40,6 +40,8 @@ import javafx.beans.property.SimpleStringProperty;
|
|||||||
import javafx.beans.property.StringProperty;
|
import javafx.beans.property.StringProperty;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import monero.daemon.model.MoneroDaemonInfo;
|
||||||
|
|
||||||
import org.bitcoinj.core.RejectMessage;
|
import org.bitcoinj.core.RejectMessage;
|
||||||
import org.bitcoinj.core.VersionMessage;
|
import org.bitcoinj.core.VersionMessage;
|
||||||
import org.bitcoinj.store.BlockStoreException;
|
import org.bitcoinj.store.BlockStoreException;
|
||||||
@ -115,7 +117,8 @@ public class WalletAppSetup {
|
|||||||
if (exception == null) {
|
if (exception == null) {
|
||||||
double percentage = (double) downloadPercentage;
|
double percentage = (double) downloadPercentage;
|
||||||
btcSyncProgress.set(percentage);
|
btcSyncProgress.set(percentage);
|
||||||
Long bestChainHeight = connectionService.getDaemon() == null ? null : connectionService.getDaemon().getInfo().getHeight();
|
MoneroDaemonInfo lastInfo = connectionService.getLastInfo();
|
||||||
|
Long bestChainHeight = lastInfo == null ? null : lastInfo.getHeight();
|
||||||
String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ?
|
String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ?
|
||||||
String.valueOf(bestChainHeight) :
|
String.valueOf(bestChainHeight) :
|
||||||
"";
|
"";
|
||||||
|
@ -26,9 +26,12 @@ import haveno.common.handlers.ResultHandler;
|
|||||||
import haveno.common.persistence.PersistenceManager;
|
import haveno.common.persistence.PersistenceManager;
|
||||||
import haveno.common.setup.GracefulShutDownHandler;
|
import haveno.common.setup.GracefulShutDownHandler;
|
||||||
import haveno.common.util.Profiler;
|
import haveno.common.util.Profiler;
|
||||||
|
import haveno.core.api.CoreMoneroConnectionsService;
|
||||||
import haveno.core.app.HavenoExecutable;
|
import haveno.core.app.HavenoExecutable;
|
||||||
|
import haveno.core.offer.OfferBookService;
|
||||||
import haveno.core.offer.OpenOfferManager;
|
import haveno.core.offer.OpenOfferManager;
|
||||||
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||||
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.xmr.setup.WalletsSetup;
|
import haveno.core.xmr.setup.WalletsSetup;
|
||||||
import haveno.core.xmr.wallet.BtcWalletService;
|
import haveno.core.xmr.wallet.BtcWalletService;
|
||||||
import haveno.core.xmr.wallet.XmrWalletService;
|
import haveno.core.xmr.wallet.XmrWalletService;
|
||||||
@ -42,7 +45,9 @@ import java.time.ZoneId;
|
|||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.ThreadFactory;
|
import java.util.concurrent.ThreadFactory;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -76,25 +81,54 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
|
|||||||
// We don't use the gracefulShutDown implementation of the super class as we have a limited set of modules
|
// We don't use the gracefulShutDown implementation of the super class as we have a limited set of modules
|
||||||
@Override
|
@Override
|
||||||
public void gracefulShutDown(ResultHandler resultHandler) {
|
public void gracefulShutDown(ResultHandler resultHandler) {
|
||||||
log.info("gracefulShutDown");
|
log.info("Starting graceful shut down of {}", getClass().getSimpleName());
|
||||||
|
|
||||||
|
// ignore if shut down in progress
|
||||||
|
if (isShutdownInProgress) {
|
||||||
|
log.info("Ignoring call to gracefulShutDown, already in progress");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isShutdownInProgress = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (injector != null) {
|
if (injector != null) {
|
||||||
|
|
||||||
|
// notify trade protocols and wallets to prepare for shut down before shutting down
|
||||||
|
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||||
|
tasks.add(() -> injector.getInstance(XmrWalletService.class).onShutDownStarted());
|
||||||
|
tasks.add(() -> injector.getInstance(CoreMoneroConnectionsService.class).onShutDownStarted());
|
||||||
|
HavenoUtils.executeTasks(tasks); // notify in parallel
|
||||||
|
|
||||||
JsonFileManager.shutDownAllInstances();
|
JsonFileManager.shutDownAllInstances();
|
||||||
injector.getInstance(ArbitratorManager.class).shutDown();
|
injector.getInstance(ArbitratorManager.class).shutDown();
|
||||||
injector.getInstance(XmrWalletService.class).shutDown(true);
|
|
||||||
injector.getInstance(OpenOfferManager.class).shutDown(() -> injector.getInstance(P2PService.class).shutDown(() -> {
|
// shut down open offer manager
|
||||||
|
log.info("Shutting down OpenOfferManager, OfferBookService, and P2PService");
|
||||||
|
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
|
||||||
|
|
||||||
|
// shut down offer book service
|
||||||
|
injector.getInstance(OfferBookService.class).shutDown();
|
||||||
|
|
||||||
|
// shut down p2p service
|
||||||
|
injector.getInstance(P2PService.class).shutDown(() -> {
|
||||||
|
log.info("Done shutting down OpenOfferManager, OfferBookService, and P2PService");
|
||||||
|
|
||||||
|
// shut down monero wallets and connections
|
||||||
injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
|
injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
|
||||||
module.close(injector);
|
module.close(injector);
|
||||||
|
|
||||||
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
|
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
|
||||||
resultHandler.handleResult();
|
resultHandler.handleResult();
|
||||||
log.info("Graceful shutdown completed. Exiting now.");
|
log.info("Graceful shutdown completed. Exiting now.");
|
||||||
UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1);
|
UserThread.runAfter(() -> System.exit(HavenoExecutable.EXIT_SUCCESS), 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
injector.getInstance(WalletsSetup.class).shutDown();
|
|
||||||
injector.getInstance(BtcWalletService.class).shutDown();
|
injector.getInstance(BtcWalletService.class).shutDown();
|
||||||
}));
|
injector.getInstance(XmrWalletService.class).shutDown();
|
||||||
|
injector.getInstance(CoreMoneroConnectionsService.class).shutDown();
|
||||||
|
injector.getInstance(WalletsSetup.class).shutDown();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// we wait max 5 sec.
|
// we wait max 5 sec.
|
||||||
UserThread.runAfter(() -> {
|
UserThread.runAfter(() -> {
|
||||||
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
|
PersistenceManager.flushAllDataToDiskAtShutdown(() -> {
|
||||||
|
@ -97,7 +97,7 @@ public class OfferBookService {
|
|||||||
connectionsService.addListener(new MoneroConnectionManagerListener() {
|
connectionsService.addListener(new MoneroConnectionManagerListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onConnectionChanged(MoneroRpcConnection connection) {
|
public void onConnectionChanged(MoneroRpcConnection connection) {
|
||||||
if (keyImagePoller == null) return;
|
maybeInitializeKeyImagePoller();
|
||||||
keyImagePoller.setDaemon(connectionsService.getDaemon());
|
keyImagePoller.setDaemon(connectionsService.getDaemon());
|
||||||
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
|
keyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
|
||||||
}
|
}
|
||||||
@ -111,8 +111,8 @@ public class OfferBookService {
|
|||||||
synchronized (offerBookChangedListeners) {
|
synchronized (offerBookChangedListeners) {
|
||||||
offerBookChangedListeners.forEach(listener -> {
|
offerBookChangedListeners.forEach(listener -> {
|
||||||
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
||||||
maybeInitializeKeyImagePoller();
|
|
||||||
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
||||||
|
maybeInitializeKeyImagePoller();
|
||||||
keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages());
|
keyImagePoller.addKeyImages(offerPayload.getReserveTxKeyImages());
|
||||||
Offer offer = new Offer(offerPayload);
|
Offer offer = new Offer(offerPayload);
|
||||||
offer.setPriceFeedService(priceFeedService);
|
offer.setPriceFeedService(priceFeedService);
|
||||||
@ -130,8 +130,8 @@ public class OfferBookService {
|
|||||||
synchronized (offerBookChangedListeners) {
|
synchronized (offerBookChangedListeners) {
|
||||||
offerBookChangedListeners.forEach(listener -> {
|
offerBookChangedListeners.forEach(listener -> {
|
||||||
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
if (protectedStorageEntry.getProtectedStoragePayload() instanceof OfferPayload) {
|
||||||
maybeInitializeKeyImagePoller();
|
|
||||||
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
OfferPayload offerPayload = (OfferPayload) protectedStorageEntry.getProtectedStoragePayload();
|
||||||
|
maybeInitializeKeyImagePoller();
|
||||||
keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages());
|
keyImagePoller.removeKeyImages(offerPayload.getReserveTxKeyImages());
|
||||||
Offer offer = new Offer(offerPayload);
|
Offer offer = new Offer(offerPayload);
|
||||||
offer.setPriceFeedService(priceFeedService);
|
offer.setPriceFeedService(priceFeedService);
|
||||||
@ -257,6 +257,10 @@ public class OfferBookService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void shutDown() {
|
||||||
|
if (keyImagePoller != null) keyImagePoller.clearKeyImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Private
|
// Private
|
||||||
@ -276,11 +280,12 @@ public class OfferBookService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// first poll after 5s
|
// first poll after 20s
|
||||||
|
// TODO: remove?
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
GenUtils.waitFor(5000);
|
GenUtils.waitFor(20000);
|
||||||
keyImagePoller.poll();
|
keyImagePoller.poll();
|
||||||
});
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getKeyImageRefreshPeriodMs() {
|
private long getKeyImageRefreshPeriodMs() {
|
||||||
|
@ -202,8 +202,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
@Override
|
@Override
|
||||||
public void onConnectionChanged(MoneroRpcConnection connection) {
|
public void onConnectionChanged(MoneroRpcConnection connection) {
|
||||||
maybeInitializeKeyImagePoller();
|
maybeInitializeKeyImagePoller();
|
||||||
signedOfferKeyImagePoller.setDaemon(connectionsService.getDaemon());
|
|
||||||
signedOfferKeyImagePoller.setRefreshPeriodMs(getKeyImageRefreshPeriodMs());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -258,10 +256,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
});
|
});
|
||||||
|
|
||||||
// first poll in 5s
|
// first poll in 5s
|
||||||
|
// TODO: remove?
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
GenUtils.waitFor(5000);
|
GenUtils.waitFor(5000);
|
||||||
signedOfferKeyImagePoller.poll();
|
signedOfferKeyImagePoller.poll();
|
||||||
});
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getKeyImageRefreshPeriodMs() {
|
private long getKeyImageRefreshPeriodMs() {
|
||||||
@ -334,6 +333,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
stopped = true;
|
stopped = true;
|
||||||
p2PService.getPeerManager().removeListener(this);
|
p2PService.getPeerManager().removeListener(this);
|
||||||
p2PService.removeDecryptedDirectMessageListener(this);
|
p2PService.removeDecryptedDirectMessageListener(this);
|
||||||
|
signedOfferKeyImagePoller.clearKeyImages();
|
||||||
|
|
||||||
stopPeriodicRefreshOffersTimer();
|
stopPeriodicRefreshOffersTimer();
|
||||||
stopPeriodicRepublishOffersTimer();
|
stopPeriodicRepublishOffersTimer();
|
||||||
|
@ -53,12 +53,12 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
|||||||
BigInteger makerFee = offer.getMakerFee();
|
BigInteger makerFee = offer.getMakerFee();
|
||||||
BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.valueOf(0) : offer.getAmount();
|
BigInteger sendAmount = offer.getDirection() == OfferDirection.BUY ? BigInteger.valueOf(0) : offer.getAmount();
|
||||||
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit();
|
BigInteger securityDeposit = offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit();
|
||||||
String returnAddress = model.getXmrWalletService().getNewAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
String returnAddress = model.getXmrWalletService().getNewAddressEntry(offer.getId(), XmrAddressEntry.Context.RESERVED_FOR_TRADE).getAddressString();
|
||||||
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress);
|
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, sendAmount, securityDeposit, returnAddress);
|
||||||
|
|
||||||
// check for error in case creating reserve tx exceeded timeout
|
// check for error in case creating reserve tx exceeded timeout
|
||||||
// TODO: better way?
|
// TODO: better way?
|
||||||
if (!model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).isPresent()) {
|
if (!model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.RESERVED_FOR_TRADE).isPresent()) {
|
||||||
throw new RuntimeException("An error has occurred posting offer " + offer.getId() + " causing its subaddress entry to be deleted");
|
throw new RuntimeException("An error has occurred posting offer " + offer.getId() + " causing its subaddress entry to be deleted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ public class MakerSendSignOfferRequest extends Task<PlaceOfferModel> {
|
|||||||
runInterceptHook();
|
runInterceptHook();
|
||||||
|
|
||||||
// create request for arbitrator to sign offer
|
// create request for arbitrator to sign offer
|
||||||
String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString();
|
String returnAddress = model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.RESERVED_FOR_TRADE).get().getAddressString();
|
||||||
SignOfferRequest request = new SignOfferRequest(
|
SignOfferRequest request = new SignOfferRequest(
|
||||||
model.getOffer().getId(),
|
model.getOffer().getId(),
|
||||||
P2PService.getMyNodeAddress(),
|
P2PService.getMyNodeAddress(),
|
||||||
|
@ -465,13 +465,21 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
DisputeValidation.validateDisputeData(dispute);
|
DisputeValidation.validateDisputeData(dispute);
|
||||||
DisputeValidation.validateNodeAddresses(dispute, config);
|
DisputeValidation.validateNodeAddresses(dispute, config);
|
||||||
DisputeValidation.validateSenderNodeAddress(dispute, message.getSenderNodeAddress());
|
DisputeValidation.validateSenderNodeAddress(dispute, message.getSenderNodeAddress());
|
||||||
DisputeValidation.validatePaymentAccountPayload(dispute);
|
|
||||||
//DisputeValidation.testIfDisputeTriesReplay(dispute, disputeList.getList());
|
//DisputeValidation.testIfDisputeTriesReplay(dispute, disputeList.getList());
|
||||||
} catch (DisputeValidation.ValidationException e) {
|
} catch (DisputeValidation.ValidationException e) {
|
||||||
validationExceptions.add(e);
|
validationExceptions.add(e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// try to validate payment account
|
||||||
|
// TODO: add field to dispute details: valid, invalid, missing
|
||||||
|
try {
|
||||||
|
DisputeValidation.validatePaymentAccountPayload(dispute);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(e.getMessage());
|
||||||
|
trade.prependErrorMessage(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// get sender
|
// get sender
|
||||||
senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
|
senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
|
||||||
TradePeer sender = trade.getTradePeer(senderPubKeyRing);
|
TradePeer sender = trade.getTradePeer(senderPubKeyRing);
|
||||||
|
@ -37,10 +37,8 @@ import java.util.Optional;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Getter
|
@Getter
|
||||||
public final class Arbitrator extends DisputeAgent {
|
public final class Arbitrator extends DisputeAgent {
|
||||||
private final String xmrAddress;
|
|
||||||
|
|
||||||
public Arbitrator(NodeAddress nodeAddress,
|
public Arbitrator(NodeAddress nodeAddress,
|
||||||
String xmrAddress,
|
|
||||||
PubKeyRing pubKeyRing,
|
PubKeyRing pubKeyRing,
|
||||||
List<String> languageCodes,
|
List<String> languageCodes,
|
||||||
long registrationDate,
|
long registrationDate,
|
||||||
@ -59,8 +57,6 @@ public final class Arbitrator extends DisputeAgent {
|
|||||||
emailAddress,
|
emailAddress,
|
||||||
info,
|
info,
|
||||||
extraDataMap);
|
extraDataMap);
|
||||||
|
|
||||||
this.xmrAddress = xmrAddress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -71,7 +67,6 @@ public final class Arbitrator extends DisputeAgent {
|
|||||||
public protobuf.StoragePayload toProtoMessage() {
|
public protobuf.StoragePayload toProtoMessage() {
|
||||||
protobuf.Arbitrator.Builder builder = protobuf.Arbitrator.newBuilder()
|
protobuf.Arbitrator.Builder builder = protobuf.Arbitrator.newBuilder()
|
||||||
.setNodeAddress(nodeAddress.toProtoMessage())
|
.setNodeAddress(nodeAddress.toProtoMessage())
|
||||||
.setXmrAddress(xmrAddress)
|
|
||||||
.setPubKeyRing(pubKeyRing.toProtoMessage())
|
.setPubKeyRing(pubKeyRing.toProtoMessage())
|
||||||
.addAllLanguageCodes(languageCodes)
|
.addAllLanguageCodes(languageCodes)
|
||||||
.setRegistrationDate(registrationDate)
|
.setRegistrationDate(registrationDate)
|
||||||
@ -85,7 +80,6 @@ public final class Arbitrator extends DisputeAgent {
|
|||||||
|
|
||||||
public static Arbitrator fromProto(protobuf.Arbitrator proto) {
|
public static Arbitrator fromProto(protobuf.Arbitrator proto) {
|
||||||
return new Arbitrator(NodeAddress.fromProto(proto.getNodeAddress()),
|
return new Arbitrator(NodeAddress.fromProto(proto.getNodeAddress()),
|
||||||
proto.getXmrAddress(),
|
|
||||||
PubKeyRing.fromProto(proto.getPubKeyRing()),
|
PubKeyRing.fromProto(proto.getPubKeyRing()),
|
||||||
new ArrayList<>(proto.getLanguageCodesList()),
|
new ArrayList<>(proto.getLanguageCodesList()),
|
||||||
proto.getRegistrationDate(),
|
proto.getRegistrationDate(),
|
||||||
@ -103,8 +97,6 @@ public final class Arbitrator extends DisputeAgent {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Arbitrator{" +
|
return "Arbitrator{} " + super.toString();
|
||||||
",\n xmrAddress='" + xmrAddress + '\'' +
|
|
||||||
"\n} " + super.toString();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import haveno.common.crypto.KeyRing;
|
|||||||
import haveno.common.crypto.PubKeyRing;
|
import haveno.common.crypto.PubKeyRing;
|
||||||
import haveno.common.crypto.Sig;
|
import haveno.common.crypto.Sig;
|
||||||
import haveno.common.util.Utilities;
|
import haveno.common.util.Utilities;
|
||||||
|
import haveno.core.app.HavenoSetup;
|
||||||
import haveno.core.offer.Offer;
|
import haveno.core.offer.Offer;
|
||||||
import haveno.core.offer.OfferPayload;
|
import haveno.core.offer.OfferPayload;
|
||||||
import haveno.core.support.dispute.arbitration.ArbitrationManager;
|
import haveno.core.support.dispute.arbitration.ArbitrationManager;
|
||||||
@ -35,6 +36,8 @@ import haveno.core.trade.messages.PaymentSentMessage;
|
|||||||
import haveno.core.util.JsonUtil;
|
import haveno.core.util.JsonUtil;
|
||||||
import haveno.network.p2p.NodeAddress;
|
import haveno.network.p2p.NodeAddress;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import monero.common.MoneroRpcConnection;
|
||||||
|
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
|
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
@ -69,8 +72,9 @@ public class HavenoUtils {
|
|||||||
private static final int POOL_SIZE = 10;
|
private static final int POOL_SIZE = 10;
|
||||||
private static final ExecutorService POOL = Executors.newFixedThreadPool(POOL_SIZE);
|
private static final ExecutorService POOL = Executors.newFixedThreadPool(POOL_SIZE);
|
||||||
|
|
||||||
public static ArbitrationManager arbitrationManager; // TODO: better way to share reference?
|
// TODO: better way to share refernces?
|
||||||
|
public static ArbitrationManager arbitrationManager;
|
||||||
|
public static HavenoSetup havenoSetup;
|
||||||
|
|
||||||
// ----------------------- CONVERSION UTILS -------------------------------
|
// ----------------------- CONVERSION UTILS -------------------------------
|
||||||
|
|
||||||
@ -502,4 +506,10 @@ public class HavenoUtils {
|
|||||||
public static String toCamelCase(String underscore) {
|
public static String toCamelCase(String underscore) {
|
||||||
return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, underscore);
|
return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, underscore);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean connectionConfigsEqual(MoneroRpcConnection c1, MoneroRpcConnection c2) {
|
||||||
|
if (c1 == c2) return true;
|
||||||
|
if (c1 == null) return false;
|
||||||
|
return c1.equals(c2); // equality considers uri, username, and password
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ package haveno.core.trade;
|
|||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.protobuf.ByteString;
|
import com.google.protobuf.ByteString;
|
||||||
import com.google.protobuf.Message;
|
import com.google.protobuf.Message;
|
||||||
|
|
||||||
import haveno.common.UserThread;
|
import haveno.common.UserThread;
|
||||||
import haveno.common.crypto.Encryption;
|
import haveno.common.crypto.Encryption;
|
||||||
import haveno.common.crypto.PubKeyRing;
|
import haveno.common.crypto.PubKeyRing;
|
||||||
@ -72,6 +73,7 @@ import monero.common.TaskLooper;
|
|||||||
import monero.daemon.MoneroDaemon;
|
import monero.daemon.MoneroDaemon;
|
||||||
import monero.daemon.model.MoneroTx;
|
import monero.daemon.model.MoneroTx;
|
||||||
import monero.wallet.MoneroWallet;
|
import monero.wallet.MoneroWallet;
|
||||||
|
import monero.wallet.MoneroWalletRpc;
|
||||||
import monero.wallet.model.MoneroDestination;
|
import monero.wallet.model.MoneroDestination;
|
||||||
import monero.wallet.model.MoneroMultisigSignResult;
|
import monero.wallet.model.MoneroMultisigSignResult;
|
||||||
import monero.wallet.model.MoneroOutputWallet;
|
import monero.wallet.model.MoneroOutputWallet;
|
||||||
@ -80,6 +82,8 @@ import monero.wallet.model.MoneroTxQuery;
|
|||||||
import monero.wallet.model.MoneroTxSet;
|
import monero.wallet.model.MoneroTxSet;
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
import monero.wallet.model.MoneroWalletListener;
|
import monero.wallet.model.MoneroWalletListener;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
import org.fxmisc.easybind.EasyBind;
|
import org.fxmisc.easybind.EasyBind;
|
||||||
import org.fxmisc.easybind.Subscription;
|
import org.fxmisc.easybind.Subscription;
|
||||||
@ -94,7 +98,6 @@ import java.util.Arrays;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@ -385,6 +388,8 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
@Getter
|
@Getter
|
||||||
transient private boolean isInitialized;
|
transient private boolean isInitialized;
|
||||||
@Getter
|
@Getter
|
||||||
|
transient private boolean isShutDownStarted;
|
||||||
|
@Getter
|
||||||
transient private boolean isShutDown;
|
transient private boolean isShutDown;
|
||||||
|
|
||||||
// Added in v1.2.0
|
// Added in v1.2.0
|
||||||
@ -572,12 +577,15 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public void initialize(ProcessModelServiceProvider serviceProvider) {
|
public void initialize(ProcessModelServiceProvider serviceProvider) {
|
||||||
|
if (isInitialized) throw new IllegalStateException(getClass().getSimpleName() + " " + getId() + " is already initialized");
|
||||||
|
|
||||||
|
// set arbitrator pub key ring once known
|
||||||
serviceProvider.getArbitratorManager().getDisputeAgentByNodeAddress(getArbitratorNodeAddress()).ifPresent(arbitrator -> {
|
serviceProvider.getArbitratorManager().getDisputeAgentByNodeAddress(getArbitratorNodeAddress()).ifPresent(arbitrator -> {
|
||||||
getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing());
|
getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing());
|
||||||
});
|
});
|
||||||
|
|
||||||
// listen to daemon connection
|
// listen to daemon connection
|
||||||
xmrWalletService.getConnectionsService().addListener(newConnection -> setDaemonConnection(newConnection));
|
xmrWalletService.getConnectionsService().addListener(newConnection -> onConnectionChanged(newConnection));
|
||||||
|
|
||||||
// check if done
|
// check if done
|
||||||
if (isPayoutUnlocked()) {
|
if (isPayoutUnlocked()) {
|
||||||
@ -585,19 +593,19 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset payment sent state if no ack receive
|
// reset buyer's payment sent state if no ack receive
|
||||||
if (getState().ordinal() >= Trade.State.BUYER_CONFIRMED_IN_UI_PAYMENT_SENT.ordinal() && getState().ordinal() < Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG.ordinal()) {
|
if (this instanceof BuyerTrade && getState().ordinal() >= Trade.State.BUYER_CONFIRMED_IN_UI_PAYMENT_SENT.ordinal() && getState().ordinal() < Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG.ordinal()) {
|
||||||
log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
|
log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
|
||||||
setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
|
setState(Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset payment received state if no ack receive
|
// reset seller's payment received state if no ack receive
|
||||||
if (getState().ordinal() >= Trade.State.SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT.ordinal() && getState().ordinal() < Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG.ordinal()) {
|
if (this instanceof SellerTrade && getState().ordinal() >= Trade.State.SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT.ordinal() && getState().ordinal() < Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG.ordinal()) {
|
||||||
log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
|
log.warn("Resetting state of {} {} from {} to {} because no ack was received", getClass().getSimpleName(), getId(), getState(), Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
|
||||||
setState(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
|
setState(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle trade state events
|
// handle trade phase events
|
||||||
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
|
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
|
||||||
if (isDepositsPublished() && !isPayoutUnlocked()) updateWalletRefreshPeriod();
|
if (isDepositsPublished() && !isPayoutUnlocked()) updateWalletRefreshPeriod();
|
||||||
if (isCompleted()) {
|
if (isCompleted()) {
|
||||||
@ -652,18 +660,28 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
xmrWalletService.addWalletListener(idlePayoutSyncer);
|
xmrWalletService.addWalletListener(idlePayoutSyncer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDepositRequested()) {
|
// send deposit confirmed message on startup or event
|
||||||
|
if (isDepositsConfirmed()) {
|
||||||
// start syncing and polling trade wallet
|
new Thread(() -> getProtocol().maybeSendDepositsConfirmedMessages()).start();
|
||||||
updateSyncing();
|
} else {
|
||||||
|
EasyBind.subscribe(stateProperty(), state -> {
|
||||||
// allow state notifications to process before returning
|
if (isDepositsConfirmed()) {
|
||||||
CountDownLatch latch = new CountDownLatch(1);
|
new Thread(() -> getProtocol().maybeSendDepositsConfirmedMessages()).start();
|
||||||
UserThread.execute(() -> latch.countDown());
|
}
|
||||||
HavenoUtils.awaitLatch(latch);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reprocess pending payout messages
|
||||||
|
this.getProtocol().maybeReprocessPaymentReceivedMessage(false);
|
||||||
|
HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(this, false);
|
||||||
|
|
||||||
|
// trade is initialized but not synced
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
|
|
||||||
|
// sync wallet if applicable
|
||||||
|
if (!isDepositRequested() || isPayoutUnlocked()) return;
|
||||||
|
if (xmrWalletService.getConnectionsService().getConnection() == null || Boolean.FALSE.equals(xmrWalletService.getConnectionsService().isConnected())) return;
|
||||||
|
updateSyncing();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void requestPersistence() {
|
public void requestPersistence() {
|
||||||
@ -710,8 +728,8 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
synchronized (walletLock) {
|
synchronized (walletLock) {
|
||||||
if (wallet != null) return wallet;
|
if (wallet != null) return wallet;
|
||||||
if (!walletExists()) return null;
|
if (!walletExists()) return null;
|
||||||
if (isShutDown) throw new RuntimeException("Cannot open wallet for " + getClass().getSimpleName() + " " + getId() + " because trade is shut down");
|
if (isShutDownStarted) throw new RuntimeException("Cannot open wallet for " + getClass().getSimpleName() + " " + getId() + " because shut down is started");
|
||||||
if (!isShutDown) wallet = xmrWalletService.openWallet(getWalletName());
|
else wallet = xmrWalletService.openWallet(getWalletName());
|
||||||
return wallet;
|
return wallet;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -744,7 +762,7 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId());
|
if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId());
|
||||||
if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId());
|
if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId());
|
||||||
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
||||||
getWallet().sync();
|
xmrWalletService.syncWallet(getWallet());
|
||||||
pollWallet();
|
pollWallet();
|
||||||
log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
||||||
}
|
}
|
||||||
@ -753,7 +771,7 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
try {
|
try {
|
||||||
syncWallet();
|
syncWallet();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (!isShutDown) {
|
if (!isShutDown && walletExists()) {
|
||||||
log.warn("Error syncing trade wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage());
|
log.warn("Error syncing trade wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -761,7 +779,7 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
|
|
||||||
public void syncWalletNormallyForMs(long syncNormalDuration) {
|
public void syncWalletNormallyForMs(long syncNormalDuration) {
|
||||||
syncNormalStartTime = System.currentTimeMillis();
|
syncNormalStartTime = System.currentTimeMillis();
|
||||||
setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs());
|
setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getRefreshPeriodMs());
|
||||||
UserThread.runAfter(() -> {
|
UserThread.runAfter(() -> {
|
||||||
if (!isShutDown && System.currentTimeMillis() >= syncNormalStartTime + syncNormalDuration) updateWalletRefreshPeriod();
|
if (!isShutDown && System.currentTimeMillis() >= syncNormalStartTime + syncNormalDuration) updateWalletRefreshPeriod();
|
||||||
}, syncNormalDuration);
|
}, syncNormalDuration);
|
||||||
@ -772,7 +790,11 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
if (getBuyer().getUpdatedMultisigHex() != null) multisigHexes.add(getBuyer().getUpdatedMultisigHex());
|
if (getBuyer().getUpdatedMultisigHex() != null) multisigHexes.add(getBuyer().getUpdatedMultisigHex());
|
||||||
if (getSeller().getUpdatedMultisigHex() != null) multisigHexes.add(getSeller().getUpdatedMultisigHex());
|
if (getSeller().getUpdatedMultisigHex() != null) multisigHexes.add(getSeller().getUpdatedMultisigHex());
|
||||||
if (getArbitrator().getUpdatedMultisigHex() != null) multisigHexes.add(getArbitrator().getUpdatedMultisigHex());
|
if (getArbitrator().getUpdatedMultisigHex() != null) multisigHexes.add(getArbitrator().getUpdatedMultisigHex());
|
||||||
if (!multisigHexes.isEmpty()) getWallet().importMultisigHex(multisigHexes.toArray(new String[0]));
|
if (!multisigHexes.isEmpty()) {
|
||||||
|
log.info("Importing multisig hex for {} {}", getClass().getSimpleName(), getId());
|
||||||
|
getWallet().importMultisigHex(multisigHexes.toArray(new String[0]));
|
||||||
|
log.info("Done importing multisig hex for {} {}", getClass().getSimpleName(), getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void changeWalletPassword(String oldPassword, String newPassword) {
|
public void changeWalletPassword(String oldPassword, String newPassword) {
|
||||||
@ -791,13 +813,22 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
|
|
||||||
private void closeWallet() {
|
private void closeWallet() {
|
||||||
synchronized (walletLock) {
|
synchronized (walletLock) {
|
||||||
if (wallet == null) throw new RuntimeException("Trade wallet to close was not previously opened for trade " + getId());
|
if (wallet == null) throw new RuntimeException("Trade wallet to close is not open for trade " + getId());
|
||||||
stopPolling();
|
stopPolling();
|
||||||
xmrWalletService.closeWallet(wallet, true);
|
xmrWalletService.closeWallet(wallet, true);
|
||||||
wallet = null;
|
wallet = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void stopWallet() {
|
||||||
|
synchronized (walletLock) {
|
||||||
|
if (wallet == null) throw new RuntimeException("Trade wallet to close is not open for trade " + getId());
|
||||||
|
stopPolling();
|
||||||
|
xmrWalletService.stopWallet(wallet, wallet.getPath(), true);
|
||||||
|
wallet = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void deleteWallet() {
|
public void deleteWallet() {
|
||||||
synchronized (walletLock) {
|
synchronized (walletLock) {
|
||||||
if (walletExists()) {
|
if (walletExists()) {
|
||||||
@ -808,14 +839,10 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
throw new RuntimeException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because the deposit txs have been published but payout tx has not unlocked");
|
throw new RuntimeException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because the deposit txs have been published but payout tx has not unlocked");
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if wallet balance > dust
|
// force stop the wallet
|
||||||
BigInteger maxBalance = isDepositsPublished() ? getMakerDepositTx().getFee().min(getTakerDepositTx().getFee()) : BigInteger.ZERO;
|
if (wallet != null) stopWallet();
|
||||||
if (getWallet().getBalance().compareTo(maxBalance) > 0) {
|
|
||||||
throw new RuntimeException("Refusing to delete wallet for " + getClass().getSimpleName() + " " + getId() + " because its balance is more than dust");
|
|
||||||
}
|
|
||||||
|
|
||||||
// close and delete trade wallet
|
// delete wallet
|
||||||
if (wallet != null) closeWallet();
|
|
||||||
log.info("Deleting wallet for {} {}", getClass().getSimpleName(), getId());
|
log.info("Deleting wallet for {} {}", getClass().getSimpleName(), getId());
|
||||||
xmrWalletService.deleteWallet(getWalletName());
|
xmrWalletService.deleteWallet(getWalletName());
|
||||||
|
|
||||||
@ -882,8 +909,7 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
// check connection to monero daemon
|
// check connection to monero daemon
|
||||||
checkWalletConnection();
|
checkWalletConnection();
|
||||||
|
|
||||||
// import multisig hex
|
// check multisig import
|
||||||
importMultisigHex();
|
|
||||||
if (getWallet().isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed");
|
if (getWallet().isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed");
|
||||||
|
|
||||||
// gather info
|
// gather info
|
||||||
@ -979,8 +1005,8 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2)));
|
BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2)));
|
||||||
if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout);
|
if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new IllegalArgumentException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout);
|
||||||
|
|
||||||
// check wallet's daemon connection
|
// check wallet connection
|
||||||
checkWalletConnection();
|
if (sign || publish) checkWalletConnection();
|
||||||
|
|
||||||
// handle tx signing
|
// handle tx signing
|
||||||
if (sign) {
|
if (sign) {
|
||||||
@ -1005,19 +1031,12 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
|
|
||||||
// verify fee is within tolerance by recreating payout tx
|
// 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?
|
// 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 = null;
|
MoneroTxWallet feeEstimateTx = createPayoutTx();;
|
||||||
try {
|
|
||||||
feeEstimateTx = createPayoutTx();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Could not recreate payout tx to verify fee: " + e.getMessage());
|
|
||||||
}
|
|
||||||
if (feeEstimateTx != null) {
|
|
||||||
BigInteger feeEstimate = feeEstimateTx.getFee();
|
BigInteger feeEstimate = feeEstimateTx.getFee();
|
||||||
double feeDiff = payoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
|
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());
|
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);
|
log.info("Payout tx fee {} is within tolerance, diff %={}", payoutTx.getFee(), feeDiff);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// update trade state
|
// update trade state
|
||||||
setPayoutTx(payoutTx);
|
setPayoutTx(payoutTx);
|
||||||
@ -1072,7 +1091,7 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
if (depositId == null) return null;
|
if (depositId == null) return null;
|
||||||
try {
|
try {
|
||||||
if (trader.getDepositTx() == null || !trader.getDepositTx().isConfirmed()) {
|
if (trader.getDepositTx() == null || !trader.getDepositTx().isConfirmed()) {
|
||||||
trader.setDepositTx(getTxFromWalletOrDaemon(depositId));
|
trader.setDepositTx(getDepositTxFromWalletOrDaemon(depositId));
|
||||||
}
|
}
|
||||||
return trader.getDepositTx();
|
return trader.getDepositTx();
|
||||||
} catch (MoneroError e) {
|
} catch (MoneroError e) {
|
||||||
@ -1081,12 +1100,18 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private MoneroTx getTxFromWalletOrDaemon(String txId) {
|
private MoneroTx getDepositTxFromWalletOrDaemon(String txId) {
|
||||||
MoneroTx tx = null;
|
MoneroTx tx = null;
|
||||||
|
|
||||||
|
// first check wallet
|
||||||
if (getWallet() != null) {
|
if (getWallet() != null) {
|
||||||
try { tx = getWallet().getTx(txId); } // TODO monero-java: return null if tx not found
|
List<MoneroTxWallet> filteredTxs = getWallet().getTxs(new MoneroTxQuery()
|
||||||
catch (Exception e) { }
|
.setHash(txId)
|
||||||
|
.setIsConfirmed(isDepositsConfirmed() ? true : null)); // avoid checking pool if confirmed
|
||||||
|
if (filteredTxs.size() == 1) tx = filteredTxs.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// then check daemon
|
||||||
if (tx == null) tx = getXmrWalletService().getTxWithCache(txId);
|
if (tx == null) tx = getXmrWalletService().getTxWithCache(txId);
|
||||||
return tx;
|
return tx;
|
||||||
}
|
}
|
||||||
@ -1126,19 +1151,27 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutDown() {
|
public void onShutDownStarted() {
|
||||||
|
isShutDownStarted = true;
|
||||||
|
if (wallet != null) log.info("{} {} onShutDownStarted()", getClass().getSimpleName(), getId());
|
||||||
|
synchronized (this) {
|
||||||
|
synchronized (walletLock) {
|
||||||
|
stopPolling(); // allow locks to release before stopping
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutDown() {
|
||||||
|
if (wallet != null) log.info("{} {} onShutDown()", getClass().getSimpleName(), getId());
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
log.info("Shutting down {} {}", getClass().getSimpleName(), getId());
|
|
||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
isShutDown = true;
|
isShutDown = true;
|
||||||
|
synchronized (walletLock) {
|
||||||
if (wallet != null) closeWallet();
|
if (wallet != null) closeWallet();
|
||||||
|
}
|
||||||
if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe();
|
if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe();
|
||||||
if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe();
|
if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe();
|
||||||
if (idlePayoutSyncer != null) {
|
idlePayoutSyncer = null; // main wallet removes listener itself
|
||||||
xmrWalletService.removeWalletListener(idlePayoutSyncer);
|
|
||||||
idlePayoutSyncer = null;
|
|
||||||
}
|
|
||||||
log.info("Done shutting down {} {}", getClass().getSimpleName(), getId());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1280,6 +1313,18 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
errorMessageProperty.set(errorMessage);
|
errorMessageProperty.set(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void prependErrorMessage(String errorMessage) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(errorMessage);
|
||||||
|
if (this.errorMessage != null && !this.errorMessage.isEmpty()) {
|
||||||
|
sb.append("\n\n---- Previous Error -----\n\n");
|
||||||
|
sb.append(this.errorMessage);
|
||||||
|
}
|
||||||
|
String appendedErrorMessage = sb.toString();
|
||||||
|
this.errorMessage = appendedErrorMessage;
|
||||||
|
errorMessageProperty.set(appendedErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
public void setAssetTxProofResult(@Nullable AssetTxProofResult assetTxProofResult) {
|
public void setAssetTxProofResult(@Nullable AssetTxProofResult assetTxProofResult) {
|
||||||
this.assetTxProofResult = assetTxProofResult;
|
this.assetTxProofResult = assetTxProofResult;
|
||||||
assetTxProofResultUpdateProperty.set(assetTxProofResultUpdateProperty.get() + 1);
|
assetTxProofResultUpdateProperty.set(assetTxProofResultUpdateProperty.get() + 1);
|
||||||
@ -1434,6 +1479,8 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
final long tradeTime = getTakeOfferDate().getTime();
|
final long tradeTime = getTakeOfferDate().getTime();
|
||||||
MoneroDaemon daemonRpc = xmrWalletService.getDaemon();
|
MoneroDaemon daemonRpc = xmrWalletService.getDaemon();
|
||||||
if (daemonRpc == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because it has no connection to monerod");
|
if (daemonRpc == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because it has no connection to monerod");
|
||||||
|
if (getMakerDepositTx() == null || getTakerDepositTx() == null) throw new RuntimeException("Cannot set start time for trade " + getId() + " because its unlocked deposit tx is null. Is client connected to a daemon?");
|
||||||
|
|
||||||
long maxHeight = Math.max(getMakerDepositTx().getHeight(), getTakerDepositTx().getHeight());
|
long maxHeight = Math.max(getMakerDepositTx().getHeight(), getTakerDepositTx().getHeight());
|
||||||
long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp();
|
long blockTime = daemonRpc.getBlockByHeight(maxHeight).getTimestamp();
|
||||||
|
|
||||||
@ -1465,7 +1512,7 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDepositsPublished() {
|
public boolean isDepositsPublished() {
|
||||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal();
|
return getState().getPhase().ordinal() >= Phase.DEPOSITS_PUBLISHED.ordinal() && getMaker().getDepositTxHash() != null && getTaker().getDepositTxHash() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isFundsLockedIn() {
|
public boolean isFundsLockedIn() {
|
||||||
@ -1473,11 +1520,11 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDepositsConfirmed() {
|
public boolean isDepositsConfirmed() {
|
||||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_CONFIRMED.ordinal();
|
return isDepositsPublished() && getState().getPhase().ordinal() >= Phase.DEPOSITS_CONFIRMED.ordinal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isDepositsUnlocked() {
|
public boolean isDepositsUnlocked() {
|
||||||
return getState().getPhase().ordinal() >= Phase.DEPOSITS_UNLOCKED.ordinal();
|
return isDepositsPublished() && getState().getPhase().ordinal() >= Phase.DEPOSITS_UNLOCKED.ordinal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPaymentSent() {
|
public boolean isPaymentSent() {
|
||||||
@ -1616,7 +1663,7 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
*/
|
*/
|
||||||
public long getReprocessDelayInSeconds(int reprocessCount) {
|
public long getReprocessDelayInSeconds(int reprocessCount) {
|
||||||
int retryCycles = 3; // reprocess on next refresh periods for first few attempts (app might auto switch to a good connection)
|
int retryCycles = 3; // reprocess on next refresh periods for first few attempts (app might auto switch to a good connection)
|
||||||
if (reprocessCount < retryCycles) return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs() / 1000;
|
if (reprocessCount < retryCycles) return xmrWalletService.getConnectionsService().getRefreshPeriodMs() / 1000;
|
||||||
long delay = 60;
|
long delay = 60;
|
||||||
for (int i = retryCycles; i < reprocessCount; i++) delay *= 2;
|
for (int i = retryCycles; i < reprocessCount; i++) delay *= 2;
|
||||||
return Math.min(MAX_REPROCESS_DELAY_SECONDS, delay);
|
return Math.min(MAX_REPROCESS_DELAY_SECONDS, delay);
|
||||||
@ -1642,13 +1689,25 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
return tradeVolumeProperty;
|
return tradeVolumeProperty;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setDaemonConnection(MoneroRpcConnection connection) {
|
private void onConnectionChanged(MoneroRpcConnection connection) {
|
||||||
synchronized (walletLock) {
|
synchronized (walletLock) {
|
||||||
if (isShutDown) return;
|
|
||||||
MoneroWallet wallet = getWallet();
|
// check if ignored
|
||||||
if (wallet == null) return;
|
if (isShutDownStarted) return;
|
||||||
|
if (getWallet() == null) return;
|
||||||
|
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return;
|
||||||
|
|
||||||
|
// set daemon connection (must restart monero-wallet-rpc if proxy uri changed)
|
||||||
|
String oldProxyUri = wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri();
|
||||||
|
String newProxyUri = connection == null ? null : connection.getProxyUri();
|
||||||
log.info("Setting daemon connection for trade wallet {}: {}", getId() , connection == null ? null : connection.getUri());
|
log.info("Setting daemon connection for trade wallet {}: {}", getId() , connection == null ? null : connection.getUri());
|
||||||
|
if (wallet instanceof MoneroWalletRpc && !StringUtils.equals(oldProxyUri, newProxyUri)) {
|
||||||
|
log.info("Restarting monero-wallet-rpc for trade wallet to set proxy URI {}: {}", getId() , connection == null ? null : connection.getUri());
|
||||||
|
closeWallet();
|
||||||
|
wallet = getWallet();
|
||||||
|
} else {
|
||||||
wallet.setDaemonConnection(connection);
|
wallet.setDaemonConnection(connection);
|
||||||
|
}
|
||||||
updateWalletRefreshPeriod();
|
updateWalletRefreshPeriod();
|
||||||
|
|
||||||
// sync and reprocess messages on new thread
|
// sync and reprocess messages on new thread
|
||||||
@ -1665,14 +1724,14 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateSyncing() {
|
private void updateSyncing() {
|
||||||
if (isShutDown) return;
|
if (isShutDownStarted) return;
|
||||||
if (!isIdling()) {
|
if (!isIdling()) {
|
||||||
updateWalletRefreshPeriod();
|
updateWalletRefreshPeriod();
|
||||||
trySyncWallet();
|
trySyncWallet();
|
||||||
} else {
|
} else {
|
||||||
long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getWalletRefreshPeriod()); // random time to start syncing
|
long startSyncingInMs = ThreadLocalRandom.current().nextLong(0, getWalletRefreshPeriod()); // random time to start syncing
|
||||||
UserThread.runAfter(() -> {
|
UserThread.runAfter(() -> {
|
||||||
if (!isShutDown) {
|
if (!isShutDownStarted) {
|
||||||
updateWalletRefreshPeriod();
|
updateWalletRefreshPeriod();
|
||||||
trySyncWallet();
|
trySyncWallet();
|
||||||
}
|
}
|
||||||
@ -1680,13 +1739,13 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateWalletRefreshPeriod() {
|
public void updateWalletRefreshPeriod() {
|
||||||
setWalletRefreshPeriod(getWalletRefreshPeriod());
|
setWalletRefreshPeriod(getWalletRefreshPeriod());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setWalletRefreshPeriod(long walletRefreshPeriod) {
|
private void setWalletRefreshPeriod(long walletRefreshPeriod) {
|
||||||
synchronized (walletLock) {
|
synchronized (walletLock) {
|
||||||
if (this.isShutDown) return;
|
if (this.isShutDownStarted) return;
|
||||||
if (this.walletRefreshPeriod != null && this.walletRefreshPeriod == walletRefreshPeriod) return;
|
if (this.walletRefreshPeriod != null && this.walletRefreshPeriod == walletRefreshPeriod) return;
|
||||||
this.walletRefreshPeriod = walletRefreshPeriod;
|
this.walletRefreshPeriod = walletRefreshPeriod;
|
||||||
if (getWallet() != null) {
|
if (getWallet() != null) {
|
||||||
@ -1702,7 +1761,7 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
synchronized (walletLock) {
|
synchronized (walletLock) {
|
||||||
if (txPollLooper != null) return;
|
if (txPollLooper != null) return;
|
||||||
log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId());
|
log.info("Starting to poll wallet for {} {}", getClass().getSimpleName(), getId());
|
||||||
txPollLooper = new TaskLooper(() -> { pollWallet(); });
|
txPollLooper = new TaskLooper(() -> pollWallet());
|
||||||
txPollLooper.start(walletRefreshPeriod);
|
txPollLooper.start(walletRefreshPeriod);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1719,31 +1778,42 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
private void pollWallet() {
|
private void pollWallet() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
// skip if either deposit tx id is unknown
|
||||||
|
if (processModel.getMaker().getDepositTxHash() == null || processModel.getTaker().getDepositTxHash() == null) return;
|
||||||
|
|
||||||
// skip if payout unlocked
|
// skip if payout unlocked
|
||||||
if (isPayoutUnlocked()) return;
|
if (isPayoutUnlocked()) return;
|
||||||
|
|
||||||
// rescan spent if deposits unlocked
|
// rescan spent outputs to detect payout tx after deposits unlocked
|
||||||
if (isDepositsUnlocked()) getWallet().rescanSpent();
|
if (isDepositsUnlocked() && !isPayoutPublished()) getWallet().rescanSpent();
|
||||||
|
|
||||||
// get txs with outputs
|
// get txs from trade wallet
|
||||||
List<MoneroTxWallet> txs;
|
boolean payoutExpected = isPaymentReceived() || processModel.getPaymentReceivedMessage() != null || disputeState.ordinal() > DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal() || processModel.getDisputeClosedMessage() != null;
|
||||||
try {
|
boolean checkPool = !isDepositsConfirmed() || (!isPayoutConfirmed() && payoutExpected);
|
||||||
txs = getWallet().getTxs(new MoneroTxQuery()
|
MoneroTxQuery query = new MoneroTxQuery().setIncludeOutputs(true);
|
||||||
.setHashes(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()))
|
if (!checkPool) query.setInTxPool(false); // avoid pool check if possible
|
||||||
.setIncludeOutputs(true));
|
List<MoneroTxWallet> txs = wallet.getTxs(query);
|
||||||
} catch (Exception e) {
|
|
||||||
if (!isShutDown) log.info("Could not fetch deposit txs from wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage()); // expected at first
|
// warn on double spend // TODO: other handling?
|
||||||
return;
|
for (MoneroTxWallet tx : txs) {
|
||||||
|
if (Boolean.TRUE.equals(tx.isDoubleSpendSeen())) log.warn("Double spend seen for tx {} for {} {}", tx.getHash(), getClass().getSimpleName(), getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// check deposit txs
|
// check deposit txs
|
||||||
if (!isDepositsUnlocked()) {
|
if (!isDepositsUnlocked()) {
|
||||||
if (txs.size() == 2) {
|
|
||||||
|
|
||||||
// update trader state
|
// update trader txs
|
||||||
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
|
MoneroTxWallet makerDepositTx = null;
|
||||||
getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1));
|
MoneroTxWallet takerDepositTx = null;
|
||||||
getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0));
|
for (MoneroTxWallet tx : txs) {
|
||||||
|
if (tx.getHash().equals(processModel.getMaker().getDepositTxHash())) makerDepositTx = tx;
|
||||||
|
if (tx.getHash().equals(processModel.getTaker().getDepositTxHash())) takerDepositTx = tx;
|
||||||
|
}
|
||||||
|
getMaker().setDepositTx(makerDepositTx);
|
||||||
|
getTaker().setDepositTx(takerDepositTx);
|
||||||
|
|
||||||
|
// skip if deposit txs not seen
|
||||||
|
if (makerDepositTx == null || takerDepositTx == null) return;
|
||||||
|
|
||||||
// set security deposits
|
// set security deposits
|
||||||
if (getBuyer().getSecurityDeposit().longValueExact() == 0) {
|
if (getBuyer().getSecurityDeposit().longValueExact() == 0) {
|
||||||
@ -1753,19 +1823,16 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
getSeller().setSecurityDeposit(sellerSecurityDeposit);
|
getSeller().setSecurityDeposit(sellerSecurityDeposit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set deposits published state
|
// update state
|
||||||
setStateDepositsPublished();
|
setStateDepositsPublished();
|
||||||
|
if (makerDepositTx.isConfirmed() && takerDepositTx.isConfirmed()) setStateDepositsConfirmed();
|
||||||
// check if deposit txs confirmed
|
if (!makerDepositTx.isLocked() && !takerDepositTx.isLocked()) setStateDepositsUnlocked();
|
||||||
if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) setStateDepositsConfirmed();
|
|
||||||
if (!txs.get(0).isLocked() && !txs.get(1).isLocked()) setStateDepositsUnlocked();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check payout tx
|
// check payout tx
|
||||||
else {
|
if (isDepositsUnlocked()) {
|
||||||
|
|
||||||
// check if deposit txs spent (appears on payout published)
|
// check if any outputs spent (observed on payout published)
|
||||||
for (MoneroTxWallet tx : txs) {
|
for (MoneroTxWallet tx : txs) {
|
||||||
for (MoneroOutputWallet output : tx.getOutputsWallet()) {
|
for (MoneroOutputWallet output : tx.getOutputsWallet()) {
|
||||||
if (Boolean.TRUE.equals(output.isSpent())) {
|
if (Boolean.TRUE.equals(output.isSpent())) {
|
||||||
@ -1775,19 +1842,18 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// check for outgoing txs (appears after wallet submits payout tx or on payout confirmed)
|
// check for outgoing txs (appears after wallet submits payout tx or on payout confirmed)
|
||||||
List<MoneroTxWallet> outgoingTxs = getWallet().getTxs(new MoneroTxQuery().setIsOutgoing(true));
|
for (MoneroTxWallet tx : txs) {
|
||||||
if (!outgoingTxs.isEmpty()) {
|
if (tx.isOutgoing()) {
|
||||||
MoneroTxWallet payoutTx = outgoingTxs.get(0);
|
setPayoutTx(tx);
|
||||||
setPayoutTx(payoutTx);
|
|
||||||
setPayoutStatePublished();
|
setPayoutStatePublished();
|
||||||
if (payoutTx.isConfirmed()) setPayoutStateConfirmed();
|
if (tx.isConfirmed()) setPayoutStateConfirmed();
|
||||||
if (!payoutTx.isLocked()) setPayoutStateUnlocked();
|
if (!tx.isLocked()) setPayoutStateUnlocked();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (!isShutDown && getWallet() != null && isWalletConnected()) {
|
if (!isShutDown && getWallet() != null && isWalletConnected()) {
|
||||||
log.warn("Error polling trade wallet {}: {}", getId(), e.getMessage());
|
log.warn("Error polling trade wallet {}: {}", getId(), e.getMessage());
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1795,7 +1861,7 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
|
|
||||||
private long getWalletRefreshPeriod() {
|
private long getWalletRefreshPeriod() {
|
||||||
if (isIdling()) return IDLE_SYNC_PERIOD_MS;
|
if (isIdling()) return IDLE_SYNC_PERIOD_MS;
|
||||||
return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs();
|
return xmrWalletService.getConnectionsService().getRefreshPeriodMs();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setStateDepositsPublished() {
|
private void setStateDepositsPublished() {
|
||||||
@ -1867,7 +1933,7 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
processing = false;
|
processing = false;
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
if (isInitialized && !isShutDown && !isWalletConnected()) throw e;
|
if (isInitialized && !isShutDownStarted && !isWalletConnected()) throw e;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@
|
|||||||
package haveno.core.trade;
|
package haveno.core.trade;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
|
||||||
|
import common.utils.GenUtils;
|
||||||
import haveno.common.ClockWatcher;
|
import haveno.common.ClockWatcher;
|
||||||
import haveno.common.UserThread;
|
import haveno.common.UserThread;
|
||||||
import haveno.common.crypto.KeyRing;
|
import haveno.common.crypto.KeyRing;
|
||||||
@ -67,6 +69,7 @@ import haveno.core.user.User;
|
|||||||
import haveno.core.util.Validator;
|
import haveno.core.util.Validator;
|
||||||
import haveno.core.xmr.model.XmrAddressEntry;
|
import haveno.core.xmr.model.XmrAddressEntry;
|
||||||
import haveno.core.xmr.wallet.XmrWalletService;
|
import haveno.core.xmr.wallet.XmrWalletService;
|
||||||
|
import haveno.network.p2p.BootstrapListener;
|
||||||
import haveno.network.p2p.DecryptedDirectMessageListener;
|
import haveno.network.p2p.DecryptedDirectMessageListener;
|
||||||
import haveno.network.p2p.DecryptedMessageWithPubKey;
|
import haveno.network.p2p.DecryptedMessageWithPubKey;
|
||||||
import haveno.network.p2p.NodeAddress;
|
import haveno.network.p2p.NodeAddress;
|
||||||
@ -85,6 +88,7 @@ import monero.wallet.model.MoneroOutputQuery;
|
|||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
import org.bouncycastle.crypto.params.KeyParameter;
|
import org.bouncycastle.crypto.params.KeyParameter;
|
||||||
import org.fxmisc.easybind.EasyBind;
|
import org.fxmisc.easybind.EasyBind;
|
||||||
|
import org.fxmisc.easybind.monadic.MonadicBinding;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -198,6 +202,13 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
p2PService.addDecryptedDirectMessageListener(this);
|
p2PService.addDecryptedDirectMessageListener(this);
|
||||||
|
|
||||||
failedTradesManager.setUnFailTradeCallback(this::unFailTrade);
|
failedTradesManager.setUnFailTradeCallback(this::unFailTrade);
|
||||||
|
|
||||||
|
// initialize trades when connected to p2p network
|
||||||
|
p2PService.addP2PServiceListener(new BootstrapListener() {
|
||||||
|
public void onUpdatedDataReceived() {
|
||||||
|
initPersistedTrades();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -249,27 +260,24 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
|
|
||||||
public void onAllServicesInitialized() {
|
public void onAllServicesInitialized() {
|
||||||
|
|
||||||
// initialize
|
|
||||||
initialize();
|
|
||||||
|
|
||||||
// listen for account updates
|
// listen for account updates
|
||||||
accountService.addListener(new AccountServiceListener() {
|
accountService.addListener(new AccountServiceListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAccountCreated() {
|
public void onAccountCreated() {
|
||||||
log.info(getClass().getSimpleName() + ".accountService.onAccountCreated()");
|
log.info(TradeManager.class + ".accountService.onAccountCreated()");
|
||||||
initialize();
|
initPersistedTrades();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAccountOpened() {
|
public void onAccountOpened() {
|
||||||
log.info(getClass().getSimpleName() + ".accountService.onAccountOpened()");
|
log.info(TradeManager.class + ".accountService.onAccountOpened()");
|
||||||
initialize();
|
initPersistedTrades();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAccountClosed() {
|
public void onAccountClosed() {
|
||||||
log.info(getClass().getSimpleName() + ".accountService.onAccountClosed()");
|
log.info(TradeManager.class + ".accountService.onAccountClosed()");
|
||||||
closeAllTrades();
|
closeAllTrades();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,21 +288,36 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initialize() {
|
public void onShutDownStarted() {
|
||||||
|
log.info("{}.onShutDownStarted()", getClass().getSimpleName());
|
||||||
|
|
||||||
// initialize trades off main thread
|
// collect trades to prepare
|
||||||
new Thread(() -> initPersistedTrades()).start();
|
Set<Trade> trades = new HashSet<Trade>();
|
||||||
|
trades.addAll(tradableList.getList());
|
||||||
|
trades.addAll(closedTradableManager.getClosedTrades());
|
||||||
|
trades.addAll(failedTradesManager.getObservableList());
|
||||||
|
|
||||||
getObservableList().addListener((ListChangeListener<Trade>) change -> onTradesChanged());
|
// prepare to shut down trades in parallel
|
||||||
onTradesChanged();
|
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||||
|
for (Trade trade : trades) tasks.add(() -> {
|
||||||
xmrWalletService.setTradeManager(this);
|
try {
|
||||||
|
trade.onShutDownStarted();
|
||||||
// thaw unreserved outputs
|
} catch (Exception e) {
|
||||||
thawUnreservedOutputs();
|
if (e.getMessage() != null && e.getMessage().contains("Connection reset")) return; // expected if shut down with ctrl+c
|
||||||
|
log.warn("Error notifying {} {} that shut down started {}", getClass().getSimpleName(), trade.getId());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
HavenoUtils.executeTasks(tasks);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error notifying trades that shut down started: {}", e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutDown() {
|
public void shutDown() {
|
||||||
|
log.info("Shutting down {}", getClass().getSimpleName());
|
||||||
isShutDown = true;
|
isShutDown = true;
|
||||||
closeAllTrades();
|
closeAllTrades();
|
||||||
}
|
}
|
||||||
@ -313,7 +336,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
try {
|
try {
|
||||||
trade.shutDown();
|
trade.shutDown();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Error closing trade subprocess. Was Haveno stopped manually with ctrl+c?");
|
if (e.getMessage() != null && (e.getMessage().contains("Connection reset") || e.getMessage().contains("Connection refused"))) return; // expected if shut down with ctrl+c
|
||||||
|
log.warn("Error closing {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
@ -372,43 +397,31 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private void initPersistedTrades() {
|
private void initPersistedTrades() {
|
||||||
|
log.info("Initializing persisted trades");
|
||||||
|
|
||||||
|
// initialize off main thread
|
||||||
|
new Thread(() -> {
|
||||||
|
|
||||||
// get all trades
|
// get all trades
|
||||||
List<Trade> trades = getAllTrades();
|
List<Trade> trades = getAllTrades();
|
||||||
|
|
||||||
// open trades in parallel since each may open a multisig wallet
|
// initialize trades in parallel
|
||||||
log.info("Initializing trades");
|
|
||||||
int threadPoolSize = 10;
|
int threadPoolSize = 10;
|
||||||
Set<Runnable> tasks = new HashSet<Runnable>();
|
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||||
for (Trade trade : trades) {
|
for (Trade trade : trades) {
|
||||||
tasks.add(new Runnable() {
|
tasks.add(() -> {
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
initPersistedTrade(trade);
|
initPersistedTrade(trade);
|
||||||
|
|
||||||
|
// remove trade if protocol didn't initialize
|
||||||
|
if (getOpenTrade(trade.getId()).isPresent() && !trade.isDepositRequested()) {
|
||||||
|
log.warn("Removing persisted {} {} with uid={} because it did not finish initializing (state={})", trade.getClass().getSimpleName(), trade.getId(), trade.getUid(), trade.getState());
|
||||||
|
removeTradeOnError(trade);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
HavenoUtils.executeTasks(tasks, threadPoolSize);
|
HavenoUtils.executeTasks(tasks, threadPoolSize);
|
||||||
log.info("Done initializing trades");
|
log.info("Done initializing persisted trades");
|
||||||
|
|
||||||
// reset any available address entries
|
|
||||||
if (isShutDown) return;
|
if (isShutDown) return;
|
||||||
xmrWalletService.getAddressEntriesForAvailableBalanceStream()
|
|
||||||
.filter(addressEntry -> addressEntry.getOfferId() != null)
|
|
||||||
.forEach(addressEntry -> {
|
|
||||||
log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId());
|
|
||||||
xmrWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), addressEntry.getContext());
|
|
||||||
});
|
|
||||||
|
|
||||||
// notify that persisted trades initialized
|
|
||||||
persistedTradesInitialized.set(true);
|
|
||||||
|
|
||||||
// We do not include failed trades as they should not be counted anyway in the trade statistics
|
|
||||||
Set<Trade> nonFailedTrades = new HashSet<>(closedTradableManager.getClosedTrades());
|
|
||||||
nonFailedTrades.addAll(tradableList.getList());
|
|
||||||
String referralId = referralIdService.getOptionalReferralId().orElse(null);
|
|
||||||
boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode;
|
|
||||||
tradeStatisticsManager.maybeRepublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode);
|
|
||||||
|
|
||||||
// sync idle trades once in background after active trades
|
// sync idle trades once in background after active trades
|
||||||
for (Trade trade : trades) {
|
for (Trade trade : trades) {
|
||||||
@ -416,10 +429,48 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
HavenoUtils.submitTask(() -> trade.syncWallet());
|
HavenoUtils.submitTask(() -> trade.syncWallet());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getObservableList().addListener((ListChangeListener<Trade>) change -> onTradesChanged());
|
||||||
|
onTradesChanged();
|
||||||
|
|
||||||
|
xmrWalletService.setTradeManager(this);
|
||||||
|
|
||||||
|
// process after all wallets initialized
|
||||||
|
MonadicBinding<Boolean> walletsInitialized = EasyBind.combine(HavenoUtils.havenoSetup.getWalletInitialized(), persistedTradesInitialized, (a, b) -> a && b);
|
||||||
|
walletsInitialized.subscribe((observable, oldValue, newValue) -> {
|
||||||
|
if (!newValue) return;
|
||||||
|
|
||||||
|
// thaw unreserved outputs
|
||||||
|
thawUnreservedOutputs();
|
||||||
|
|
||||||
|
// reset any available funded address entries
|
||||||
|
xmrWalletService.getAddressEntriesForAvailableBalanceStream()
|
||||||
|
.filter(addressEntry -> addressEntry.getOfferId() != null)
|
||||||
|
.forEach(addressEntry -> {
|
||||||
|
log.warn("Swapping pending {} entries at startup. offerId={}", addressEntry.getContext(), addressEntry.getOfferId());
|
||||||
|
xmrWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), addressEntry.getContext());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// notify that persisted trades initialized
|
||||||
|
persistedTradesInitialized.set(true);
|
||||||
|
|
||||||
|
// We do not include failed trades as they should not be counted anyway in the trade statistics
|
||||||
|
// TODO: remove stats?
|
||||||
|
Set<Trade> nonFailedTrades = new HashSet<>(closedTradableManager.getClosedTrades());
|
||||||
|
nonFailedTrades.addAll(tradableList.getList());
|
||||||
|
String referralId = referralIdService.getOptionalReferralId().orElse(null);
|
||||||
|
boolean isTorNetworkNode = p2PService.getNetworkNode() instanceof TorNetworkNode;
|
||||||
|
tradeStatisticsManager.maybeRepublishTradeStatistics(nonFailedTrades, referralId, isTorNetworkNode);
|
||||||
|
}).start();
|
||||||
|
|
||||||
|
// allow execution to start
|
||||||
|
GenUtils.waitFor(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initPersistedTrade(Trade trade) {
|
private void initPersistedTrade(Trade trade) {
|
||||||
if (isShutDown) return;
|
if (isShutDown) return;
|
||||||
|
if (getTradeProtocol(trade) != null) return;
|
||||||
initTradeAndProtocol(trade, createTradeProtocol(trade));
|
initTradeAndProtocol(trade, createTradeProtocol(trade));
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
scheduleDeletionIfUnfunded(trade);
|
scheduleDeletionIfUnfunded(trade);
|
||||||
@ -1100,16 +1151,16 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void addTrade(Trade trade) {
|
private void addTrade(Trade trade) {
|
||||||
synchronized(tradableList) {
|
synchronized (tradableList) {
|
||||||
if (tradableList.add(trade)) {
|
if (tradableList.add(trade)) {
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void removeTrade(Trade trade) {
|
private void removeTrade(Trade trade) {
|
||||||
log.info("TradeManager.removeTrade() " + trade.getId());
|
log.info("TradeManager.removeTrade() " + trade.getId());
|
||||||
synchronized(tradableList) {
|
synchronized (tradableList) {
|
||||||
if (!tradableList.contains(trade)) return;
|
if (!tradableList.contains(trade)) return;
|
||||||
|
|
||||||
// remove trade
|
// remove trade
|
||||||
@ -1121,9 +1172,9 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void removeTradeOnError(Trade trade) {
|
private void removeTradeOnError(Trade trade) {
|
||||||
log.info("TradeManager.removeTradeOnError() " + trade.getId());
|
log.info("TradeManager.removeTradeOnError() " + trade.getId());
|
||||||
synchronized(tradableList) {
|
synchronized (tradableList) {
|
||||||
if (!tradableList.contains(trade)) return;
|
if (!tradableList.contains(trade)) return;
|
||||||
|
|
||||||
// unreserve taker key images
|
// unreserve taker key images
|
||||||
@ -1150,18 +1201,18 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void scheduleDeletionIfUnfunded(Trade trade) {
|
private void scheduleDeletionIfUnfunded(Trade trade) {
|
||||||
if (trade.isDepositRequested() && !trade.isDepositsPublished()) {
|
if (getOpenTrade(trade.getId()).isPresent() && trade.isDepositRequested() && !trade.isDepositsPublished()) {
|
||||||
log.warn("Scheduling to delete trade if unfunded for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
log.warn("Scheduling to delete open trade if unfunded for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
UserThread.runAfter(() -> {
|
UserThread.runAfter(() -> {
|
||||||
if (isShutDown) return;
|
if (isShutDown) return;
|
||||||
|
|
||||||
// get trade's deposit txs from daemon
|
// get trade's deposit txs from daemon
|
||||||
MoneroTx makerDepositTx = xmrWalletService.getDaemon().getTx(trade.getMaker().getDepositTxHash());
|
MoneroTx makerDepositTx = trade.getMaker().getDepositTxHash() == null ? null : xmrWalletService.getDaemon().getTx(trade.getMaker().getDepositTxHash());
|
||||||
MoneroTx takerDepositTx = xmrWalletService.getDaemon().getTx(trade.getTaker().getDepositTxHash());
|
MoneroTx takerDepositTx = trade.getTaker().getDepositTxHash() == null ? null : xmrWalletService.getDaemon().getTx(trade.getTaker().getDepositTxHash());
|
||||||
|
|
||||||
// delete multisig trade wallet if neither deposit tx published
|
// delete multisig trade wallet if neither deposit tx published
|
||||||
if (makerDepositTx == null && takerDepositTx == null) {
|
if (makerDepositTx == null && takerDepositTx == null) {
|
||||||
log.warn("Deleting {} {} after protocol timeout", trade.getClass().getSimpleName(), trade.getId());
|
log.warn("Deleting {} {} after protocol error", trade.getClass().getSimpleName(), trade.getId());
|
||||||
removeTrade(trade);
|
removeTrade(trade);
|
||||||
failedTradesManager.removeTrade(trade);
|
failedTradesManager.removeTrade(trade);
|
||||||
if (trade.walletExists()) trade.deleteWallet();
|
if (trade.walletExists()) trade.deleteWallet();
|
||||||
|
@ -182,7 +182,7 @@ public final class InitTradeRequest extends TradeMessage implements DirectMessag
|
|||||||
",\n reserveTxHex=" + reserveTxHex +
|
",\n reserveTxHex=" + reserveTxHex +
|
||||||
",\n reserveTxKey=" + reserveTxKey +
|
",\n reserveTxKey=" + reserveTxKey +
|
||||||
",\n payoutAddress=" + payoutAddress +
|
",\n payoutAddress=" + payoutAddress +
|
||||||
",\n makerSignature=" + Utilities.byteArrayToInteger(makerSignature) +
|
",\n makerSignature=" + (makerSignature == null ? null : Utilities.byteArrayToInteger(makerSignature)) +
|
||||||
"\n} " + super.toString();
|
"\n} " + super.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,10 +141,11 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
|
|||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "PaymentSentMessage{" +
|
return "PaymentSentMessage{" +
|
||||||
|
",\n tradeId=" + tradeId +
|
||||||
|
",\n uid='" + uid + '\'' +
|
||||||
",\n senderNodeAddress=" + senderNodeAddress +
|
",\n senderNodeAddress=" + senderNodeAddress +
|
||||||
",\n counterCurrencyTxId=" + counterCurrencyTxId +
|
",\n counterCurrencyTxId=" + counterCurrencyTxId +
|
||||||
",\n counterCurrencyExtraData=" + counterCurrencyExtraData +
|
",\n counterCurrencyExtraData=" + counterCurrencyExtraData +
|
||||||
",\n uid='" + uid + '\'' +
|
|
||||||
",\n payoutTxHex=" + payoutTxHex +
|
",\n payoutTxHex=" + payoutTxHex +
|
||||||
",\n updatedMultisigHex=" + updatedMultisigHex +
|
",\n updatedMultisigHex=" + updatedMultisigHex +
|
||||||
",\n paymentAccountKey=" + paymentAccountKey +
|
",\n paymentAccountKey=" + paymentAccountKey +
|
||||||
|
@ -324,7 +324,7 @@ public class FluentProtocol {
|
|||||||
log.info(info);
|
log.info(info);
|
||||||
return Result.VALID.info(info);
|
return Result.VALID.info(info);
|
||||||
} else {
|
} else {
|
||||||
String info = MessageFormat.format("We received a {0} but we are are not in the expected state. " +
|
String info = MessageFormat.format("We received a {0} but we are not in the expected state. " +
|
||||||
"Expected states={1}, Trade state= {2}, tradeId={3}",
|
"Expected states={1}, Trade state= {2}, tradeId={3}",
|
||||||
trigger,
|
trigger,
|
||||||
expectedStates,
|
expectedStates,
|
||||||
|
@ -52,7 +52,7 @@ import haveno.core.trade.protocol.tasks.ProcessPaymentSentMessage;
|
|||||||
import haveno.core.trade.protocol.tasks.ProcessSignContractRequest;
|
import haveno.core.trade.protocol.tasks.ProcessSignContractRequest;
|
||||||
import haveno.core.trade.protocol.tasks.ProcessSignContractResponse;
|
import haveno.core.trade.protocol.tasks.ProcessSignContractResponse;
|
||||||
import haveno.core.trade.protocol.tasks.RemoveOffer;
|
import haveno.core.trade.protocol.tasks.RemoveOffer;
|
||||||
import haveno.core.trade.protocol.tasks.ResendDisputeClosedMessageWithPayout;
|
import haveno.core.trade.protocol.tasks.MaybeResendDisputeClosedMessageWithPayout;
|
||||||
import haveno.core.trade.protocol.tasks.TradeTask;
|
import haveno.core.trade.protocol.tasks.TradeTask;
|
||||||
import haveno.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
|
import haveno.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
|
||||||
import haveno.core.util.Validator;
|
import haveno.core.util.Validator;
|
||||||
@ -242,32 +242,19 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void onInitialized() {
|
protected void onInitialized() {
|
||||||
if (!trade.isCompleted()) {
|
|
||||||
processModel.getP2PService().addDecryptedDirectMessageListener(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialize trade
|
// listen for direct messages unless completed
|
||||||
|
if (!trade.isCompleted()) processModel.getP2PService().addDecryptedDirectMessageListener(this);
|
||||||
|
|
||||||
|
// initialize trade with lock
|
||||||
|
synchronized (trade) {
|
||||||
trade.initialize(processModel.getProvider());
|
trade.initialize(processModel.getProvider());
|
||||||
|
}
|
||||||
|
|
||||||
// process mailbox messages
|
// process mailbox messages
|
||||||
MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService();
|
MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService();
|
||||||
mailboxMessageService.addDecryptedMailboxListener(this);
|
if (!trade.isCompleted()) mailboxMessageService.addDecryptedMailboxListener(this);
|
||||||
handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages());
|
handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages());
|
||||||
|
|
||||||
// send deposit confirmed message on startup or event
|
|
||||||
if (trade.getState().ordinal() >= Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN.ordinal()) {
|
|
||||||
new Thread(() -> sendDepositsConfirmedMessages()).start();
|
|
||||||
} else {
|
|
||||||
EasyBind.subscribe(trade.stateProperty(), state -> {
|
|
||||||
if (state == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN) {
|
|
||||||
new Thread(() -> sendDepositsConfirmedMessages()).start();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// reprocess payout messages if pending
|
|
||||||
maybeReprocessPaymentReceivedMessage(true);
|
|
||||||
HavenoUtils.arbitrationManager.maybeReprocessDisputeClosedMessage(trade, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) {
|
public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) {
|
||||||
@ -448,7 +435,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
.setup(tasks(
|
.setup(tasks(
|
||||||
ProcessDepositsConfirmedMessage.class,
|
ProcessDepositsConfirmedMessage.class,
|
||||||
VerifyPeersAccountAgeWitness.class,
|
VerifyPeersAccountAgeWitness.class,
|
||||||
ResendDisputeClosedMessageWithPayout.class)
|
MaybeResendDisputeClosedMessageWithPayout.class)
|
||||||
.using(new TradeTaskRunner(trade,
|
.using(new TradeTaskRunner(trade,
|
||||||
() -> {
|
() -> {
|
||||||
handleTaskRunnerSuccess(sender, response);
|
handleTaskRunnerSuccess(sender, response);
|
||||||
@ -475,7 +462,8 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
// the mailbox msg once wallet is ready and trade state set.
|
// the mailbox msg once wallet is ready and trade state set.
|
||||||
synchronized (trade) {
|
synchronized (trade) {
|
||||||
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) {
|
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) {
|
||||||
log.warn("Ignoring PaymentSentMessage which was already processed");
|
log.warn("Received another PaymentSentMessage which was already processed, ACKing");
|
||||||
|
handleTaskRunnerSuccess(peer, message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
latchTrade();
|
latchTrade();
|
||||||
@ -518,6 +506,11 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
synchronized (trade) {
|
synchronized (trade) {
|
||||||
|
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_RECEIVED.ordinal()) {
|
||||||
|
log.warn("Received another PaymentReceivedMessage which was already processed, ACKing");
|
||||||
|
handleTaskRunnerSuccess(peer, message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
latchTrade();
|
latchTrade();
|
||||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||||
processModel.setTradeMessage(message);
|
processModel.setTradeMessage(message);
|
||||||
@ -844,7 +837,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendDepositsConfirmedMessages() {
|
public void maybeSendDepositsConfirmedMessages() {
|
||||||
synchronized (trade) {
|
synchronized (trade) {
|
||||||
if (!trade.isInitialized()) return; // skip if shutting down
|
if (!trade.isInitialized()) return; // skip if shutting down
|
||||||
if (trade.getProcessModel().isDepositsConfirmedMessagesDelivered()) return; // skip if already delivered
|
if (trade.getProcessModel().isDepositsConfirmedMessagesDelivered()) return; // skip if already delivered
|
||||||
@ -860,7 +853,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
|
|
||||||
// retry in 15 minutes
|
// retry in 15 minutes
|
||||||
UserThread.runAfter(() -> {
|
UserThread.runAfter(() -> {
|
||||||
sendDepositsConfirmedMessages();
|
maybeSendDepositsConfirmedMessages();
|
||||||
}, 15, TimeUnit.MINUTES);
|
}, 15, TimeUnit.MINUTES);
|
||||||
handleTaskRunnerFault(null, null, "SendDepositsConfirmedMessages", errorMessage);
|
handleTaskRunnerFault(null, null, "SendDepositsConfirmedMessages", errorMessage);
|
||||||
})))
|
})))
|
||||||
|
@ -70,7 +70,7 @@ public class MakerSendInitTradeRequest extends TradeTask {
|
|||||||
trade.getSelf().getReserveTxHash(),
|
trade.getSelf().getReserveTxHash(),
|
||||||
trade.getSelf().getReserveTxHex(),
|
trade.getSelf().getReserveTxHex(),
|
||||||
trade.getSelf().getReserveTxKey(),
|
trade.getSelf().getReserveTxKey(),
|
||||||
model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(),
|
model.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.RESERVED_FOR_TRADE).get().getAddressString(),
|
||||||
null);
|
null);
|
||||||
|
|
||||||
// send request to arbitrator
|
// send request to arbitrator
|
||||||
|
@ -32,10 +32,10 @@ import java.util.List;
|
|||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class ResendDisputeClosedMessageWithPayout extends TradeTask {
|
public class MaybeResendDisputeClosedMessageWithPayout extends TradeTask {
|
||||||
|
|
||||||
@SuppressWarnings({"unused"})
|
@SuppressWarnings({"unused"})
|
||||||
public ResendDisputeClosedMessageWithPayout(TaskRunner taskHandler, Trade trade) {
|
public MaybeResendDisputeClosedMessageWithPayout(TaskRunner taskHandler, Trade trade) {
|
||||||
super(taskHandler, trade);
|
super(taskHandler, trade);
|
||||||
}
|
}
|
||||||
|
|
@ -83,7 +83,7 @@ public class MaybeSendSignContractRequest extends TradeTask {
|
|||||||
trade.getSelf().setDepositTx(depositTx);
|
trade.getSelf().setDepositTx(depositTx);
|
||||||
trade.getSelf().setDepositTxHash(depositTx.getHash());
|
trade.getSelf().setDepositTxHash(depositTx.getHash());
|
||||||
trade.getSelf().setReserveTxKeyImages(reservedKeyImages);
|
trade.getSelf().setReserveTxKeyImages(reservedKeyImages);
|
||||||
trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getAddressEntry(processModel.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString()); // TODO (woodser): allow custom payout address?
|
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()));
|
trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade.getSelf().getPaymentAccountId()));
|
||||||
|
|
||||||
// maker signs deposit hash nonce to avoid challenge protocol
|
// maker signs deposit hash nonce to avoid challenge protocol
|
||||||
|
@ -53,14 +53,21 @@ public class ProcessDepositsConfirmedMessage extends TradeTask {
|
|||||||
if (sender.getNodeAddress().equals(trade.getSeller().getNodeAddress()) && sender != trade.getSeller()) trade.getSeller().setNodeAddress(null);
|
if (sender.getNodeAddress().equals(trade.getSeller().getNodeAddress()) && sender != trade.getSeller()) trade.getSeller().setNodeAddress(null);
|
||||||
if (sender.getNodeAddress().equals(trade.getArbitrator().getNodeAddress()) && sender != trade.getArbitrator()) trade.getArbitrator().setNodeAddress(null);
|
if (sender.getNodeAddress().equals(trade.getArbitrator().getNodeAddress()) && sender != trade.getArbitrator()) trade.getArbitrator().setNodeAddress(null);
|
||||||
|
|
||||||
// update multisig hex
|
|
||||||
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
|
|
||||||
|
|
||||||
// decrypt seller payment account payload if key given
|
// decrypt seller payment account payload if key given
|
||||||
if (request.getSellerPaymentAccountKey() != null && trade.getTradePeer().getPaymentAccountPayload() == null) {
|
if (request.getSellerPaymentAccountKey() != null && trade.getTradePeer().getPaymentAccountPayload() == null) {
|
||||||
log.info(trade.getClass().getSimpleName() + " decrypting using seller payment account key");
|
log.info(trade.getClass().getSimpleName() + " decrypting using seller payment account key");
|
||||||
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
|
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
|
||||||
}
|
}
|
||||||
|
processModel.getTradeManager().requestPersistence(); // in case importing multisig hex fails
|
||||||
|
|
||||||
|
// update multisig hex
|
||||||
|
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
|
||||||
|
try {
|
||||||
|
trade.importMultisigHex();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error importing multisig hex for {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
// persist and complete
|
// persist and complete
|
||||||
processModel.getTradeManager().requestPersistence();
|
processModel.getTradeManager().requestPersistence();
|
||||||
|
@ -111,6 +111,7 @@ public class ProcessInitMultisigRequest extends TradeTask {
|
|||||||
processModel.setMultisigAddress(result.getAddress());
|
processModel.setMultisigAddress(result.getAddress());
|
||||||
trade.saveWallet(); // save multisig wallet on completion
|
trade.saveWallet(); // save multisig wallet on completion
|
||||||
trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_COMPLETED);
|
trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_COMPLETED);
|
||||||
|
trade.updateWalletRefreshPeriod(); // starts syncing
|
||||||
}
|
}
|
||||||
|
|
||||||
// update multisig participants if new state to communicate
|
// update multisig participants if new state to communicate
|
||||||
|
@ -113,7 +113,8 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
|||||||
|
|
||||||
// wait to sign and publish payout tx if defer flag set (seller recently saw payout tx arrive at buyer)
|
// wait to sign and publish payout tx if defer flag set (seller recently saw payout tx arrive at buyer)
|
||||||
boolean isSigned = message.getSignedPayoutTxHex() != null;
|
boolean isSigned = message.getSignedPayoutTxHex() != null;
|
||||||
if (trade instanceof ArbitratorTrade && !isSigned && message.isDeferPublishPayout()) {
|
boolean deferSignAndPublish = trade instanceof ArbitratorTrade && !isSigned && message.isDeferPublishPayout();
|
||||||
|
if (deferSignAndPublish) {
|
||||||
log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
log.info("Deferring signing and publishing payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
|
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
|
||||||
if (!trade.isPayoutUnlocked()) trade.syncWallet();
|
if (!trade.isPayoutUnlocked()) trade.syncWallet();
|
||||||
@ -135,6 +136,7 @@ public class ProcessPaymentReceivedMessage extends TradeTask {
|
|||||||
trade.verifyPayoutTx(trade.getPayoutTxHex(), false, true);
|
trade.verifyPayoutTx(trade.getPayoutTxHex(), false, true);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
trade.syncWallet();
|
||||||
if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId());
|
if (trade.isPayoutPublished()) log.info("Payout tx already published for {} {}", trade.getClass().getName(), trade.getId());
|
||||||
else throw e;
|
else throw e;
|
||||||
}
|
}
|
||||||
|
@ -47,10 +47,7 @@ public class ProcessPaymentSentMessage extends TradeTask {
|
|||||||
// update latest peer address
|
// update latest peer address
|
||||||
trade.getBuyer().setNodeAddress(processModel.getTempTradePeerNodeAddress());
|
trade.getBuyer().setNodeAddress(processModel.getTempTradePeerNodeAddress());
|
||||||
|
|
||||||
// if seller, decrypt buyer's payment account payload
|
// update state from message
|
||||||
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
|
|
||||||
|
|
||||||
// update state
|
|
||||||
processModel.setPaymentSentMessage(message);
|
processModel.setPaymentSentMessage(message);
|
||||||
trade.setPayoutTxHex(message.getPayoutTxHex());
|
trade.setPayoutTxHex(message.getPayoutTxHex());
|
||||||
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
||||||
@ -59,6 +56,20 @@ public class ProcessPaymentSentMessage extends TradeTask {
|
|||||||
if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) trade.setCounterCurrencyTxId(counterCurrencyTxId);
|
if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) trade.setCounterCurrencyTxId(counterCurrencyTxId);
|
||||||
String counterCurrencyExtraData = message.getCounterCurrencyExtraData();
|
String counterCurrencyExtraData = message.getCounterCurrencyExtraData();
|
||||||
if (counterCurrencyExtraData != null && counterCurrencyExtraData.length() < 100) trade.setCounterCurrencyExtraData(counterCurrencyExtraData);
|
if (counterCurrencyExtraData != null && counterCurrencyExtraData.length() < 100) trade.setCounterCurrencyExtraData(counterCurrencyExtraData);
|
||||||
|
|
||||||
|
// if seller, decrypt buyer's payment account payload
|
||||||
|
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
|
||||||
|
trade.requestPersistence();
|
||||||
|
|
||||||
|
// import multisig hex
|
||||||
|
try {
|
||||||
|
trade.importMultisigHex();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error importing multisig hex for {} {}: {}", trade.getClass().getSimpleName(), trade.getId(), e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
// update state
|
||||||
trade.advanceState(trade.isSeller() ? Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG : Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
|
trade.advanceState(trade.isSeller() ? Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG : Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
|
||||||
trade.requestPersistence();
|
trade.requestPersistence();
|
||||||
complete();
|
complete();
|
||||||
|
@ -58,8 +58,8 @@ public abstract class SendMailboxMessageTask extends TradeTask {
|
|||||||
TradeMailboxMessage message = getTradeMailboxMessage(id);
|
TradeMailboxMessage message = getTradeMailboxMessage(id);
|
||||||
setStateSent();
|
setStateSent();
|
||||||
NodeAddress peersNodeAddress = getReceiverNodeAddress();
|
NodeAddress peersNodeAddress = getReceiverNodeAddress();
|
||||||
log.info("Send {} to peer {} for {} {}", trade.getClass().getSimpleName(), trade.getId(),
|
log.info("Send {} to peer {} for {} {}, uid={}",
|
||||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
message.getClass().getSimpleName(), peersNodeAddress, trade.getClass().getSimpleName(), trade.getId(), message.getUid());
|
||||||
|
|
||||||
TradeTask task = this;
|
TradeTask task = this;
|
||||||
processModel.getP2PService().getMailboxMessageService().sendEncryptedMailboxMessage(
|
processModel.getP2PService().getMailboxMessageService().sendEncryptedMailboxMessage(
|
||||||
|
@ -43,7 +43,7 @@ public class TakerReserveTradeFunds extends TradeTask {
|
|||||||
BigInteger takerFee = trade.getTakerFee();
|
BigInteger takerFee = trade.getTakerFee();
|
||||||
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getAmount() : BigInteger.valueOf(0);
|
BigInteger sendAmount = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getAmount() : BigInteger.valueOf(0);
|
||||||
BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getSellerSecurityDeposit() : trade.getOffer().getBuyerSecurityDeposit();
|
BigInteger securityDeposit = trade.getOffer().getDirection() == OfferDirection.BUY ? trade.getOffer().getSellerSecurityDeposit() : trade.getOffer().getBuyerSecurityDeposit();
|
||||||
String returnAddress = model.getXmrWalletService().getAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString();
|
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||||
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(takerFee, sendAmount, securityDeposit, returnAddress);
|
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(takerFee, sendAmount, securityDeposit, returnAddress);
|
||||||
|
|
||||||
// collect reserved key images
|
// collect reserved key images
|
||||||
|
@ -78,7 +78,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
|
|||||||
extraDataMap.put(OfferPayload.REFERRAL_ID, referralId);
|
extraDataMap.put(OfferPayload.REFERRAL_ID, referralId);
|
||||||
}
|
}
|
||||||
|
|
||||||
NodeAddress arbitratorNodeAddress = checkNotNull(trade.getArbitrator().getNodeAddress());
|
NodeAddress arbitratorNodeAddress = checkNotNull(trade.getArbitrator().getNodeAddress(), "Arbitrator address is null", trade.getClass().getSimpleName(), trade.getId());
|
||||||
|
|
||||||
// The first 4 chars are sufficient to identify an arbitrator.
|
// The first 4 chars are sufficient to identify an arbitrator.
|
||||||
// For testing with regtest/localhost we use the full address as its localhost and would result in
|
// For testing with regtest/localhost we use the full address as its localhost and would result in
|
||||||
|
@ -170,7 +170,13 @@ public class TradeStatisticsManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
TradeStatistics3 tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode);
|
TradeStatistics3 tradeStatistics3 = null;
|
||||||
|
try {
|
||||||
|
tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
boolean hasTradeStatistics3 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3.getHash()));
|
boolean hasTradeStatistics3 = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3.getHash()));
|
||||||
if (hasTradeStatistics3) {
|
if (hasTradeStatistics3) {
|
||||||
log.debug("Trade: {}. We have already a tradeStatistics matching the hash of tradeStatistics3.",
|
log.debug("Trade: {}. We have already a tradeStatistics matching the hash of tradeStatistics3.",
|
||||||
|
@ -37,6 +37,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import monero.common.MoneroError;
|
import monero.common.MoneroError;
|
||||||
import monero.wallet.model.MoneroOutputQuery;
|
import monero.wallet.model.MoneroOutputQuery;
|
||||||
import monero.wallet.model.MoneroOutputWallet;
|
import monero.wallet.model.MoneroOutputWallet;
|
||||||
|
import monero.wallet.model.MoneroTxQuery;
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
@ -127,8 +128,10 @@ public class Balances {
|
|||||||
List<Trade> openTrades = tradeManager.getTradesStreamWithFundsLockedIn().collect(Collectors.toList());
|
List<Trade> openTrades = tradeManager.getTradesStreamWithFundsLockedIn().collect(Collectors.toList());
|
||||||
for (Trade trade : openTrades) {
|
for (Trade trade : openTrades) {
|
||||||
try {
|
try {
|
||||||
MoneroTxWallet depositTx = xmrWalletService.getWallet().getTx(trade.getSelf().getDepositTxHash());
|
List<MoneroTxWallet> depositTxs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery()
|
||||||
if (!depositTx.isConfirmed()) continue; // outputs are frozen until confirmed by arbitrator's broadcast
|
.setHash(trade.getSelf().getDepositTxHash())
|
||||||
|
.setInTxPool(false)); // don't check pool
|
||||||
|
if (depositTxs.size() != 1 || !depositTxs.get(0).isConfirmed()) continue; // outputs are frozen until confirmed by arbitrator's broadcast
|
||||||
} catch (MoneroError e) {
|
} catch (MoneroError e) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -25,10 +25,12 @@ public class MoneroWalletRpcManager {
|
|||||||
/**
|
/**
|
||||||
* Manage monero-wallet-rpc instances by auto-assigning ports.
|
* Manage monero-wallet-rpc instances by auto-assigning ports.
|
||||||
*/
|
*/
|
||||||
public MoneroWalletRpcManager() { }
|
public MoneroWalletRpcManager() {
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manage monero-wallet-rpc instances by assigning consecutive ports from a starting port.
|
* Manage monero-wallet-rpc instances by assigning consecutive ports from a
|
||||||
|
* starting port.
|
||||||
*
|
*
|
||||||
* @param startPort is the starting port to bind to
|
* @param startPort is the starting port to bind to
|
||||||
*/
|
*/
|
||||||
@ -106,32 +108,18 @@ public class MoneroWalletRpcManager {
|
|||||||
* Stop an instance of monero-wallet-rpc.
|
* Stop an instance of monero-wallet-rpc.
|
||||||
*
|
*
|
||||||
* @param walletRpc the client connected to the monero-wallet-rpc instance to stop
|
* @param walletRpc the client connected to the monero-wallet-rpc instance to stop
|
||||||
|
* @param path the wallet path, since the wallet might be closed
|
||||||
|
* @param force specifies if the process should be forcibly destroyed
|
||||||
*/
|
*/
|
||||||
public void stopInstance(MoneroWalletRpc walletRpc) {
|
public void stopInstance(MoneroWalletRpc walletRpc, String path, boolean force) {
|
||||||
|
|
||||||
// unregister port
|
// unregister port
|
||||||
int port = -1;
|
int port = unregisterPort(walletRpc);
|
||||||
synchronized (registeredPorts) {
|
|
||||||
boolean found = false;
|
|
||||||
for (Map.Entry<Integer, MoneroWalletRpc> entry : registeredPorts.entrySet()) {
|
|
||||||
if (walletRpc == entry.getValue()) {
|
|
||||||
found = true;
|
|
||||||
try {
|
|
||||||
port = entry.getKey();
|
|
||||||
unregisterPort(port);
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new MoneroError(e);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) throw new RuntimeException("MoneroWalletRpc instance not registered with a port");
|
|
||||||
}
|
|
||||||
|
|
||||||
// stop process
|
// stop process
|
||||||
String pid = walletRpc.getProcess() == null ? null : String.valueOf(walletRpc.getProcess().pid());
|
String pid = walletRpc.getProcess() == null ? null : String.valueOf(walletRpc.getProcess().pid());
|
||||||
log.info("Stopping MoneroWalletRpc path={}, port={}, pid={}", walletRpc.getPath(), port, pid);
|
log.info("Stopping MoneroWalletRpc path={}, port={}, pid={}", path, port, pid);
|
||||||
walletRpc.stopProcess();
|
walletRpc.stopProcess(force);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int registerNextPort() throws IOException {
|
private int registerNextPort() throws IOException {
|
||||||
@ -143,7 +131,28 @@ public class MoneroWalletRpcManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void unregisterPort(int port) {
|
private int unregisterPort(MoneroWalletRpc walletRpc) {
|
||||||
|
synchronized (registeredPorts) {
|
||||||
|
int port = -1;
|
||||||
|
boolean found = false;
|
||||||
|
for (Map.Entry<Integer, MoneroWalletRpc> entry : registeredPorts.entrySet()) {
|
||||||
|
if (walletRpc == entry.getValue()) {
|
||||||
|
found = true;
|
||||||
|
try {
|
||||||
|
port = entry.getKey();
|
||||||
|
removePort(port);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new MoneroError(e);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) throw new RuntimeException("MoneroWalletRpc instance not registered with a port");
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removePort(int port) {
|
||||||
synchronized (registeredPorts) {
|
synchronized (registeredPorts) {
|
||||||
registeredPorts.remove(port);
|
registeredPorts.remove(port);
|
||||||
}
|
}
|
||||||
|
@ -117,8 +117,10 @@ public class MoneroKeyImagePoller {
|
|||||||
* @return the key images to listen to
|
* @return the key images to listen to
|
||||||
*/
|
*/
|
||||||
public Collection<String> getKeyImages() {
|
public Collection<String> getKeyImages() {
|
||||||
|
synchronized (keyImages) {
|
||||||
return new ArrayList<String>(keyImages);
|
return new ArrayList<String>(keyImages);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the key images to listen to.
|
* Set the key images to listen to.
|
||||||
@ -197,6 +199,13 @@ public class MoneroKeyImagePoller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the key images which stops polling.
|
||||||
|
*/
|
||||||
|
public void clearKeyImages() {
|
||||||
|
setKeyImages();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates if the given key image is spent.
|
* Indicates if the given key image is spent.
|
||||||
*
|
*
|
||||||
@ -215,23 +224,32 @@ public class MoneroKeyImagePoller {
|
|||||||
log.warn("Cannot poll key images because daemon is null");
|
log.warn("Cannot poll key images because daemon is null");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
|
// get copy of key images to fetch
|
||||||
|
List<String> keyImages = new ArrayList<String>(getKeyImages());
|
||||||
|
|
||||||
// fetch spent statuses
|
// fetch spent statuses
|
||||||
List<MoneroKeyImageSpentStatus> spentStatuses = keyImages.isEmpty() ? new ArrayList<MoneroKeyImageSpentStatus>() : daemon.getKeyImageSpentStatuses(keyImages);
|
List<MoneroKeyImageSpentStatus> spentStatuses = null;
|
||||||
|
try {
|
||||||
|
if (keyImages.isEmpty()) spentStatuses = new ArrayList<MoneroKeyImageSpentStatus>();
|
||||||
|
else {
|
||||||
|
spentStatuses = daemon.getKeyImageSpentStatuses(keyImages); // TODO monero-java: if order of getKeyImageSpentStatuses is guaranteed, then it should take list parameter
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error polling spent status of key images: " + e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// collect changed statuses
|
// collect changed statuses
|
||||||
Map<String, MoneroKeyImageSpentStatus> changedStatuses = new HashMap<String, MoneroKeyImageSpentStatus>();
|
Map<String, MoneroKeyImageSpentStatus> changedStatuses = new HashMap<String, MoneroKeyImageSpentStatus>();
|
||||||
synchronized (lastStatuses) {
|
synchronized (lastStatuses) {
|
||||||
synchronized (keyImages) {
|
for (int i = 0; i < spentStatuses.size(); i++) {
|
||||||
for (int i = 0; i < keyImages.size(); i++) {
|
if (spentStatuses.get(i) != lastStatuses.get(keyImages.get(i))) {
|
||||||
if (lastStatuses.get(keyImages.get(i)) != spentStatuses.get(i)) {
|
|
||||||
lastStatuses.put(keyImages.get(i), spentStatuses.get(i));
|
lastStatuses.put(keyImages.get(i), spentStatuses.get(i));
|
||||||
changedStatuses.put(keyImages.get(i), spentStatuses.get(i));
|
changedStatuses.put(keyImages.get(i), spentStatuses.get(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// announce changes
|
// announce changes
|
||||||
if (!changedStatuses.isEmpty()) {
|
if (!changedStatuses.isEmpty()) {
|
||||||
@ -239,14 +257,13 @@ public class MoneroKeyImagePoller {
|
|||||||
listener.onSpentStatusChanged(changedStatuses);
|
listener.onSpentStatusChanged(changedStatuses);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("Error polling key images: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshPolling() {
|
private void refreshPolling() {
|
||||||
|
synchronized (keyImages) {
|
||||||
setIsPolling(keyImages.size() > 0 && listeners.size() > 0);
|
setIsPolling(keyImages.size() > 0 && listeners.size() > 0);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private synchronized void setIsPolling(boolean enabled) {
|
private synchronized void setIsPolling(boolean enabled) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
@ -2,6 +2,8 @@ package haveno.core.xmr.wallet;
|
|||||||
|
|
||||||
import com.google.common.util.concurrent.Service.State;
|
import com.google.common.util.concurrent.Service.State;
|
||||||
import com.google.inject.name.Named;
|
import com.google.inject.name.Named;
|
||||||
|
|
||||||
|
import common.utils.GenUtils;
|
||||||
import common.utils.JsonUtils;
|
import common.utils.JsonUtils;
|
||||||
import haveno.common.UserThread;
|
import haveno.common.UserThread;
|
||||||
import haveno.common.config.BaseCurrencyNetwork;
|
import haveno.common.config.BaseCurrencyNetwork;
|
||||||
@ -12,7 +14,6 @@ import haveno.common.util.Utilities;
|
|||||||
import haveno.core.api.AccountServiceListener;
|
import haveno.core.api.AccountServiceListener;
|
||||||
import haveno.core.api.CoreAccountService;
|
import haveno.core.api.CoreAccountService;
|
||||||
import haveno.core.api.CoreMoneroConnectionsService;
|
import haveno.core.api.CoreMoneroConnectionsService;
|
||||||
import haveno.core.app.HavenoSetup;
|
|
||||||
import haveno.core.offer.Offer;
|
import haveno.core.offer.Offer;
|
||||||
import haveno.core.trade.BuyerTrade;
|
import haveno.core.trade.BuyerTrade;
|
||||||
import haveno.core.trade.HavenoUtils;
|
import haveno.core.trade.HavenoUtils;
|
||||||
@ -41,6 +42,7 @@ import monero.wallet.model.MoneroDestination;
|
|||||||
import monero.wallet.model.MoneroOutputQuery;
|
import monero.wallet.model.MoneroOutputQuery;
|
||||||
import monero.wallet.model.MoneroOutputWallet;
|
import monero.wallet.model.MoneroOutputWallet;
|
||||||
import monero.wallet.model.MoneroSubaddress;
|
import monero.wallet.model.MoneroSubaddress;
|
||||||
|
import monero.wallet.model.MoneroSyncResult;
|
||||||
import monero.wallet.model.MoneroTransferQuery;
|
import monero.wallet.model.MoneroTransferQuery;
|
||||||
import monero.wallet.model.MoneroTxConfig;
|
import monero.wallet.model.MoneroTxConfig;
|
||||||
import monero.wallet.model.MoneroTxQuery;
|
import monero.wallet.model.MoneroTxQuery;
|
||||||
@ -48,6 +50,8 @@ import monero.wallet.model.MoneroTxWallet;
|
|||||||
import monero.wallet.model.MoneroWalletConfig;
|
import monero.wallet.model.MoneroWalletConfig;
|
||||||
import monero.wallet.model.MoneroWalletListener;
|
import monero.wallet.model.MoneroWalletListener;
|
||||||
import monero.wallet.model.MoneroWalletListenerI;
|
import monero.wallet.model.MoneroWalletListenerI;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@ -64,7 +68,11 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@ -88,6 +96,8 @@ public class XmrWalletService {
|
|||||||
private static final double SECURITY_DEPOSIT_TOLERANCE = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? 0.25 : 0.05; // security deposit can absorb miner fee up to percent
|
private static final double SECURITY_DEPOSIT_TOLERANCE = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? 0.25 : 0.05; // security deposit can absorb miner fee up to percent
|
||||||
private static final double DUST_TOLERANCE = 0.01; // max dust as percent of mining fee
|
private static final double DUST_TOLERANCE = 0.01; // max dust as percent of mining fee
|
||||||
private static final int NUM_MAX_BACKUP_WALLETS = 10;
|
private static final int NUM_MAX_BACKUP_WALLETS = 10;
|
||||||
|
private static final int MONERO_LOG_LEVEL = 0;
|
||||||
|
private static final boolean PRINT_STACK_TRACE = false;
|
||||||
|
|
||||||
private final CoreAccountService accountService;
|
private final CoreAccountService accountService;
|
||||||
private final CoreMoneroConnectionsService connectionsService;
|
private final CoreMoneroConnectionsService connectionsService;
|
||||||
@ -103,9 +113,8 @@ public class XmrWalletService {
|
|||||||
private TradeManager tradeManager;
|
private TradeManager tradeManager;
|
||||||
private MoneroWalletRpc wallet;
|
private MoneroWalletRpc wallet;
|
||||||
private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
|
private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
|
||||||
private boolean isShutDown = false;
|
private boolean isShutDownStarted = false;
|
||||||
|
private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type
|
||||||
private HavenoSetup havenoSetup;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
XmrWalletService(CoreAccountService accountService,
|
XmrWalletService(CoreAccountService accountService,
|
||||||
@ -122,6 +131,9 @@ public class XmrWalletService {
|
|||||||
this.rpcBindPort = rpcBindPort;
|
this.rpcBindPort = rpcBindPort;
|
||||||
this.xmrWalletFile = new File(walletDir, MONERO_WALLET_NAME);
|
this.xmrWalletFile = new File(walletDir, MONERO_WALLET_NAME);
|
||||||
|
|
||||||
|
// set monero logging
|
||||||
|
MoneroUtils.setLogLevel(MONERO_LOG_LEVEL);
|
||||||
|
|
||||||
// initialize after account open and basic setup
|
// initialize after account open and basic setup
|
||||||
walletsSetup.addSetupTaskHandler(() -> { // TODO: use something better than legacy WalletSetup for notification to initialize
|
walletsSetup.addSetupTaskHandler(() -> { // TODO: use something better than legacy WalletSetup for notification to initialize
|
||||||
|
|
||||||
@ -210,7 +222,7 @@ public class XmrWalletService {
|
|||||||
|
|
||||||
public MoneroWalletRpc createWallet(String walletName) {
|
public MoneroWalletRpc createWallet(String walletName) {
|
||||||
log.info("{}.createWallet({})", getClass().getSimpleName(), walletName);
|
log.info("{}.createWallet({})", getClass().getSimpleName(), walletName);
|
||||||
if (isShutDown) throw new IllegalStateException("Cannot create wallet because shutting down");
|
if (isShutDownStarted) throw new IllegalStateException("Cannot create wallet because shutting down");
|
||||||
return createWalletRpc(new MoneroWalletConfig()
|
return createWalletRpc(new MoneroWalletConfig()
|
||||||
.setPath(walletName)
|
.setPath(walletName)
|
||||||
.setPassword(getWalletPassword()),
|
.setPassword(getWalletPassword()),
|
||||||
@ -219,13 +231,26 @@ public class XmrWalletService {
|
|||||||
|
|
||||||
public MoneroWalletRpc openWallet(String walletName) {
|
public MoneroWalletRpc openWallet(String walletName) {
|
||||||
log.info("{}.openWallet({})", getClass().getSimpleName(), walletName);
|
log.info("{}.openWallet({})", getClass().getSimpleName(), walletName);
|
||||||
if (isShutDown) throw new IllegalStateException("Cannot open wallet because shutting down");
|
if (isShutDownStarted) throw new IllegalStateException("Cannot open wallet because shutting down");
|
||||||
return openWalletRpc(new MoneroWalletConfig()
|
return openWalletRpc(new MoneroWalletConfig()
|
||||||
.setPath(walletName)
|
.setPath(walletName)
|
||||||
.setPassword(getWalletPassword()),
|
.setPassword(getWalletPassword()),
|
||||||
null);
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync the given wallet in a thread pool with other wallets.
|
||||||
|
*/
|
||||||
|
public MoneroSyncResult syncWallet(MoneroWallet wallet) {
|
||||||
|
Callable<MoneroSyncResult> task = () -> wallet.sync();
|
||||||
|
Future<MoneroSyncResult> future = syncWalletThreadPool.submit(task);
|
||||||
|
try {
|
||||||
|
return future.get();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new MoneroError(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void saveWallet(MoneroWallet wallet, boolean backup) {
|
public void saveWallet(MoneroWallet wallet, boolean backup) {
|
||||||
wallet.save();
|
wallet.save();
|
||||||
if (backup) backupWallet(wallet.getPath());
|
if (backup) backupWallet(wallet.getPath());
|
||||||
@ -234,17 +259,25 @@ public class XmrWalletService {
|
|||||||
public void closeWallet(MoneroWallet wallet, boolean save) {
|
public void closeWallet(MoneroWallet wallet, boolean save) {
|
||||||
log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), wallet.getPath(), save);
|
log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), wallet.getPath(), save);
|
||||||
MoneroError err = null;
|
MoneroError err = null;
|
||||||
try {
|
|
||||||
String path = wallet.getPath();
|
String path = wallet.getPath();
|
||||||
|
try {
|
||||||
wallet.close(save);
|
wallet.close(save);
|
||||||
if (save) backupWallet(path);
|
if (save) backupWallet(path);
|
||||||
} catch (MoneroError e) {
|
} catch (MoneroError e) {
|
||||||
err = e;
|
err = e;
|
||||||
}
|
}
|
||||||
MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) wallet);
|
stopWallet(wallet, path);
|
||||||
if (err != null) throw err;
|
if (err != null) throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void stopWallet(MoneroWallet wallet, String path) {
|
||||||
|
stopWallet(wallet, path, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopWallet(MoneroWallet wallet, String path, boolean force) {
|
||||||
|
MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) wallet, path, force);
|
||||||
|
}
|
||||||
|
|
||||||
public void deleteWallet(String walletName) {
|
public void deleteWallet(String walletName) {
|
||||||
log.info("{}.deleteWallet({})", getClass().getSimpleName(), walletName);
|
log.info("{}.deleteWallet({})", getClass().getSimpleName(), walletName);
|
||||||
if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName);
|
if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName);
|
||||||
@ -506,7 +539,7 @@ public class XmrWalletService {
|
|||||||
synchronized (txCache) {
|
synchronized (txCache) {
|
||||||
for (MoneroTx tx : txs) txCache.remove(tx.getHash());
|
for (MoneroTx tx : txs) txCache.remove(tx.getHash());
|
||||||
}
|
}
|
||||||
}, connectionsService.getDefaultRefreshPeriodMs() / 1000);
|
}, connectionsService.getRefreshPeriodMs() / 1000);
|
||||||
return txs;
|
return txs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -518,7 +551,7 @@ public class XmrWalletService {
|
|||||||
|
|
||||||
public List<MoneroTx> getTxsWithCache(List<String> txHashes) {
|
public List<MoneroTx> getTxsWithCache(List<String> txHashes) {
|
||||||
synchronized (txCache) {
|
synchronized (txCache) {
|
||||||
|
try {
|
||||||
// get cached txs
|
// get cached txs
|
||||||
List<MoneroTx> cachedTxs = new ArrayList<MoneroTx>();
|
List<MoneroTx> cachedTxs = new ArrayList<MoneroTx>();
|
||||||
List<String> uncachedTxHashes = new ArrayList<String>();
|
List<String> uncachedTxHashes = new ArrayList<String>();
|
||||||
@ -529,22 +562,39 @@ public class XmrWalletService {
|
|||||||
|
|
||||||
// return txs from cache if available, otherwise fetch
|
// return txs from cache if available, otherwise fetch
|
||||||
return uncachedTxHashes.isEmpty() ? cachedTxs : getTxs(txHashes);
|
return uncachedTxHashes.isEmpty() ? cachedTxs : getTxs(txHashes);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void closeMainWallet(boolean save) {
|
|
||||||
try {
|
|
||||||
closeWallet(wallet, true);
|
|
||||||
wallet = null;
|
|
||||||
walletListeners.clear();
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Error closing main monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?");
|
if (!isShutDownStarted) throw e;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutDown(boolean save) {
|
public void onShutDownStarted() {
|
||||||
this.isShutDown = true;
|
log.info("XmrWalletService.onShutDownStarted()");
|
||||||
closeMainWallet(save);
|
this.isShutDownStarted = true;
|
||||||
|
|
||||||
|
// remove listeners which stops polling wallet
|
||||||
|
// TODO monero-java: wallet.stopPolling()?
|
||||||
|
if (wallet != null) {
|
||||||
|
for (MoneroWalletListenerI listener : new HashSet<>(wallet.getListeners())) {
|
||||||
|
wallet.removeListener(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepare trades for shut down
|
||||||
|
if (tradeManager != null) tradeManager.onShutDownStarted();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutDown() {
|
||||||
|
log.info("Shutting down {}", getClass().getSimpleName());
|
||||||
|
|
||||||
|
// shut down trade and main wallets at same time
|
||||||
|
walletListeners.clear();
|
||||||
|
List<Runnable> tasks = new ArrayList<Runnable>();
|
||||||
|
if (tradeManager != null) tasks.add(() -> tradeManager.shutDown());
|
||||||
|
tasks.add(() -> closeMainWallet(true));
|
||||||
|
HavenoUtils.executeTasks(tasks);
|
||||||
|
log.info("Done shutting down all wallets");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------ PRIVATE HELPERS -------------------------
|
// ------------------------------ PRIVATE HELPERS -------------------------
|
||||||
@ -555,25 +605,27 @@ public class XmrWalletService {
|
|||||||
maybeInitMainWallet();
|
maybeInitMainWallet();
|
||||||
|
|
||||||
// set and listen to daemon connection
|
// set and listen to daemon connection
|
||||||
connectionsService.addListener(newConnection -> setDaemonConnection(newConnection));
|
connectionsService.addListener(newConnection -> onConnectionChanged(newConnection));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeInitMainWallet() {
|
private synchronized void maybeInitMainWallet() {
|
||||||
if (wallet != null) throw new RuntimeException("Main wallet is already initialized");
|
|
||||||
|
|
||||||
// open or create wallet
|
// open or create wallet main wallet
|
||||||
|
if (wallet == null) {
|
||||||
MoneroDaemonRpc daemon = connectionsService.getDaemon();
|
MoneroDaemonRpc daemon = connectionsService.getDaemon();
|
||||||
log.info("Initializing main wallet with " + (daemon == null ? "daemon: null" : "monerod uri=" + daemon.getRpcConnection().getUri()));
|
log.info("Initializing main wallet with monerod=" + (daemon == null ? "null" : daemon.getRpcConnection().getUri()));
|
||||||
MoneroWalletConfig walletConfig = new MoneroWalletConfig().setPath(MONERO_WALLET_NAME).setPassword(getWalletPassword());
|
MoneroWalletConfig walletConfig = new MoneroWalletConfig().setPath(MONERO_WALLET_NAME).setPassword(getWalletPassword());
|
||||||
if (MoneroUtils.walletExists(xmrWalletFile.getPath())) {
|
if (MoneroUtils.walletExists(xmrWalletFile.getPath())) {
|
||||||
wallet = openWalletRpc(walletConfig, rpcBindPort);
|
wallet = openWalletRpc(walletConfig, rpcBindPort);
|
||||||
} else if (connectionsService.getConnection() != null && Boolean.TRUE.equals(connectionsService.getConnection().isConnected())) {
|
} else if (connectionsService.getConnection() != null && Boolean.TRUE.equals(connectionsService.getConnection().isConnected())) {
|
||||||
wallet = createWalletRpc(walletConfig, rpcBindPort);
|
wallet = createWalletRpc(walletConfig, rpcBindPort);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// handle when wallet initialized and synced
|
// sync wallet if open
|
||||||
if (wallet != null) {
|
if (wallet != null) {
|
||||||
log.info("Monero wallet uri={}, path={}", wallet.getRpcConnection().getUri(), wallet.getPath());
|
log.info("Monero wallet uri={}, path={}", wallet.getRpcConnection().getUri(), wallet.getPath());
|
||||||
|
while (!HavenoUtils.havenoSetup.getWalletInitialized().get()) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// sync main wallet
|
// sync main wallet
|
||||||
@ -581,21 +633,24 @@ public class XmrWalletService {
|
|||||||
long time = System.currentTimeMillis();
|
long time = System.currentTimeMillis();
|
||||||
wallet.sync(); // blocking
|
wallet.sync(); // blocking
|
||||||
log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms");
|
log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms");
|
||||||
wallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs());
|
wallet.startSyncing(connectionsService.getRefreshPeriodMs());
|
||||||
if (getMoneroNetworkType() != MoneroNetworkType.MAINNET) log.info("Monero wallet balance={}, unlocked balance={}", wallet.getBalance(0), wallet.getUnlockedBalance(0));
|
if (getMoneroNetworkType() != MoneroNetworkType.MAINNET) log.info("Monero wallet balance={}, unlocked balance={}", wallet.getBalance(0), wallet.getUnlockedBalance(0));
|
||||||
|
|
||||||
// TODO: using this to signify both daemon and wallet synced, refactor sync handling of both
|
// TODO: using this to signify both daemon and wallet synced, use separate sync handlers
|
||||||
connectionsService.doneDownload();
|
connectionsService.doneDownload();
|
||||||
|
|
||||||
|
// notify setup that main wallet is initialized
|
||||||
|
// TODO: app fully initializes after this is set to true, even though wallet might not be initialized if unconnected. wallet will be created when connection detected
|
||||||
|
// refactor startup to call this and sync off main thread? but the calls to e.g. getBalance() fail with 'wallet and network is not yet initialized'
|
||||||
|
HavenoUtils.havenoSetup.getWalletInitialized().set(true);
|
||||||
|
|
||||||
// save but skip backup on initialization
|
// save but skip backup on initialization
|
||||||
saveMainWallet(false);
|
saveMainWallet(false);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Error syncing main wallet: {}", e.getMessage());
|
log.warn("Error syncing main wallet: {}. Trying again in {} seconds", e.getMessage(), connectionsService.getRefreshPeriodMs() / 1000);
|
||||||
|
GenUtils.waitFor(connectionsService.getRefreshPeriodMs());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// notify setup that main wallet is initialized
|
|
||||||
// TODO: move to try..catch? refactor startup to call this and sync off main thread?
|
|
||||||
havenoSetup.getWalletInitialized().set(true); // TODO: change to listener pattern
|
|
||||||
|
|
||||||
// register internal listener to notify external listeners
|
// register internal listener to notify external listeners
|
||||||
wallet.addListener(new XmrWalletListener());
|
wallet.addListener(new XmrWalletListener());
|
||||||
@ -610,6 +665,7 @@ public class XmrWalletService {
|
|||||||
|
|
||||||
// start monero-wallet-rpc instance
|
// start monero-wallet-rpc instance
|
||||||
MoneroWalletRpc walletRpc = startWalletRpcInstance(port);
|
MoneroWalletRpc walletRpc = startWalletRpcInstance(port);
|
||||||
|
walletRpc.getRpcConnection().setPrintStackTrace(PRINT_STACK_TRACE);
|
||||||
|
|
||||||
// create wallet
|
// create wallet
|
||||||
try {
|
try {
|
||||||
@ -621,11 +677,12 @@ public class XmrWalletService {
|
|||||||
log.info("Creating wallet " + config.getPath() + " connected to daemon " + connection.getUri());
|
log.info("Creating wallet " + config.getPath() + " connected to daemon " + connection.getUri());
|
||||||
long time = System.currentTimeMillis();
|
long time = System.currentTimeMillis();
|
||||||
walletRpc.createWallet(config.setServer(connection));
|
walletRpc.createWallet(config.setServer(connection));
|
||||||
|
walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE);
|
||||||
log.info("Done creating wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms");
|
log.info("Done creating wallet " + config.getPath() + " in " + (System.currentTimeMillis() - time) + " ms");
|
||||||
return walletRpc;
|
return walletRpc;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
MONERO_WALLET_RPC_MANAGER.stopInstance(walletRpc);
|
stopWallet(walletRpc, config.getPath());
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -634,6 +691,7 @@ public class XmrWalletService {
|
|||||||
|
|
||||||
// start monero-wallet-rpc instance
|
// start monero-wallet-rpc instance
|
||||||
MoneroWalletRpc walletRpc = startWalletRpcInstance(port);
|
MoneroWalletRpc walletRpc = startWalletRpcInstance(port);
|
||||||
|
walletRpc.getRpcConnection().setPrintStackTrace(PRINT_STACK_TRACE);
|
||||||
|
|
||||||
// open wallet
|
// open wallet
|
||||||
try {
|
try {
|
||||||
@ -644,11 +702,12 @@ public class XmrWalletService {
|
|||||||
// open wallet
|
// open wallet
|
||||||
log.info("Opening wallet " + config.getPath());
|
log.info("Opening wallet " + config.getPath());
|
||||||
walletRpc.openWallet(config.setServer(connectionsService.getConnection()));
|
walletRpc.openWallet(config.setServer(connectionsService.getConnection()));
|
||||||
|
if (walletRpc.getDaemonConnection() != null) walletRpc.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE);
|
||||||
log.info("Done opening wallet " + config.getPath());
|
log.info("Done opening wallet " + config.getPath());
|
||||||
return walletRpc;
|
return walletRpc;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
MONERO_WALLET_RPC_MANAGER.stopInstance(walletRpc);
|
stopWallet(walletRpc, config.getPath());
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -693,15 +752,23 @@ public class XmrWalletService {
|
|||||||
return MONERO_WALLET_RPC_MANAGER.startInstance(cmd);
|
return MONERO_WALLET_RPC_MANAGER.startInstance(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: monero-wallet-rpc needs restarted if applying tor proxy
|
private void onConnectionChanged(MoneroRpcConnection connection) {
|
||||||
private void setDaemonConnection(MoneroRpcConnection connection) {
|
if (isShutDownStarted) return;
|
||||||
if (isShutDown) return;
|
if (wallet != null && HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return;
|
||||||
log.info("Setting wallet daemon connection: " + (connection == null ? null : connection.getUri()));
|
|
||||||
|
log.info("Setting main wallet daemon connection: " + (connection == null ? null : connection.getUri()));
|
||||||
|
String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri();
|
||||||
|
String newProxyUri = connection == null ? null : connection.getProxyUri();
|
||||||
if (wallet == null) maybeInitMainWallet();
|
if (wallet == null) maybeInitMainWallet();
|
||||||
else {
|
else if (wallet instanceof MoneroWalletRpc && !StringUtils.equals(oldProxyUri, newProxyUri)) {
|
||||||
|
log.info("Restarting main wallet since proxy URI has changed");
|
||||||
|
closeMainWallet(true);
|
||||||
|
maybeInitMainWallet();
|
||||||
|
} else {
|
||||||
wallet.setDaemonConnection(connection);
|
wallet.setDaemonConnection(connection);
|
||||||
|
if (connection != null) wallet.getDaemonConnection().setPrintStackTrace(PRINT_STACK_TRACE);
|
||||||
if (connection != null && !Boolean.FALSE.equals(connection.isConnected())) {
|
if (connection != null && !Boolean.FALSE.equals(connection.isConnected())) {
|
||||||
wallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs());
|
wallet.startSyncing(connectionsService.getRefreshPeriodMs());
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
wallet.sync();
|
wallet.sync();
|
||||||
@ -711,6 +778,8 @@ public class XmrWalletService {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info("Done setting main wallet daemon connection: " + (connection == null ? null : connection.getUri()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void notifyBalanceListeners() {
|
private void notifyBalanceListeners() {
|
||||||
@ -755,6 +824,17 @@ public class XmrWalletService {
|
|||||||
HavenoUtils.executeTasks(tasks, Math.min(10, 1 + trades.size()));
|
HavenoUtils.executeTasks(tasks, Math.min(10, 1 + trades.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void closeMainWallet(boolean save) {
|
||||||
|
try {
|
||||||
|
if (wallet != null) {
|
||||||
|
closeWallet(wallet, true);
|
||||||
|
wallet = null;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error closing main monero-wallet-rpc subprocess: " + e.getMessage() + ". Was Haveno stopped manually with ctrl+c?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------- LEGACY APP -------------------------------
|
// ----------------------------- LEGACY APP -------------------------------
|
||||||
|
|
||||||
public synchronized XmrAddressEntry getNewAddressEntry() {
|
public synchronized XmrAddressEntry getNewAddressEntry() {
|
||||||
@ -764,7 +844,7 @@ public class XmrWalletService {
|
|||||||
public synchronized XmrAddressEntry getNewAddressEntry(String offerId, XmrAddressEntry.Context context) {
|
public synchronized XmrAddressEntry getNewAddressEntry(String offerId, XmrAddressEntry.Context context) {
|
||||||
|
|
||||||
// try to use available and not yet used entries
|
// try to use available and not yet used entries
|
||||||
List<MoneroTxWallet> incomingTxs = getIncomingTxs(null); // pre-fetch all incoming txs to avoid query per subaddress
|
List<MoneroTxWallet> incomingTxs = getIncomingTxs(); // prefetch all incoming txs to avoid query per subaddress
|
||||||
Optional<XmrAddressEntry> emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> XmrAddressEntry.Context.AVAILABLE == e.getContext()).filter(e -> isSubaddressUnused(e.getSubaddressIndex(), incomingTxs)).findAny();
|
Optional<XmrAddressEntry> emptyAvailableAddressEntry = getAddressEntryListAsImmutableList().stream().filter(e -> XmrAddressEntry.Context.AVAILABLE == e.getContext()).filter(e -> isSubaddressUnused(e.getSubaddressIndex(), incomingTxs)).findAny();
|
||||||
if (emptyAvailableAddressEntry.isPresent()) return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId);
|
if (emptyAvailableAddressEntry.isPresent()) return xmrAddressEntryList.swapAvailableToAddressEntryWithOfferId(emptyAvailableAddressEntry.get(), context, offerId);
|
||||||
|
|
||||||
@ -820,7 +900,6 @@ public class XmrWalletService {
|
|||||||
log.info("resetAddressEntriesForOpenOffer offerId={}", offerId);
|
log.info("resetAddressEntriesForOpenOffer offerId={}", offerId);
|
||||||
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING);
|
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING);
|
||||||
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.RESERVED_FOR_TRADE);
|
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.RESERVED_FOR_TRADE);
|
||||||
swapTradeEntryToAvailableEntry(offerId, XmrAddressEntry.Context.TRADE_PAYOUT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void resetAddressEntriesForPendingTrade(String offerId) {
|
public synchronized void resetAddressEntriesForPendingTrade(String offerId) {
|
||||||
@ -881,8 +960,9 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<XmrAddressEntry> getUnusedAddressEntries() {
|
public List<XmrAddressEntry> getUnusedAddressEntries() {
|
||||||
|
List<MoneroTxWallet> incomingTxs = getIncomingTxs(); // prefetch all incoming txs to avoid query per subaddress
|
||||||
return getAvailableAddressEntries().stream()
|
return getAvailableAddressEntries().stream()
|
||||||
.filter(e -> isSubaddressUnused(e.getSubaddressIndex()))
|
.filter(e -> isSubaddressUnused(e.getSubaddressIndex(), incomingTxs))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -997,10 +1077,6 @@ public class XmrWalletService {
|
|||||||
log.info("\n" + tracePrefix + ":" + sb.toString());
|
log.info("\n" + tracePrefix + ":" + sb.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHavenoSetup(HavenoSetup havenoSetup) {
|
|
||||||
this.havenoSetup = havenoSetup;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------- HELPERS -------------------------------
|
// -------------------------------- HELPERS -------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,11 +54,11 @@ public class ArbitratorManagerTest {
|
|||||||
add("es");
|
add("es");
|
||||||
}};
|
}};
|
||||||
|
|
||||||
Arbitrator one = new Arbitrator(new NodeAddress("arbitrator:1"), null, null,
|
Arbitrator one = new Arbitrator(new NodeAddress("arbitrator:1"), null,
|
||||||
languagesOne, 0L, null, "", null,
|
languagesOne, 0L, null, "", null,
|
||||||
null, null);
|
null, null);
|
||||||
|
|
||||||
Arbitrator two = new Arbitrator(new NodeAddress("arbitrator:2"), null, null,
|
Arbitrator two = new Arbitrator(new NodeAddress("arbitrator:2"), null,
|
||||||
languagesTwo, 0L, null, "", null,
|
languagesTwo, 0L, null, "", null,
|
||||||
null, null);
|
null, null);
|
||||||
|
|
||||||
@ -90,11 +90,11 @@ public class ArbitratorManagerTest {
|
|||||||
add("es");
|
add("es");
|
||||||
}};
|
}};
|
||||||
|
|
||||||
Arbitrator one = new Arbitrator(new NodeAddress("arbitrator:1"), null, null,
|
Arbitrator one = new Arbitrator(new NodeAddress("arbitrator:1"), null,
|
||||||
languagesOne, 0L, null, "", null,
|
languagesOne, 0L, null, "", null,
|
||||||
null, null);
|
null, null);
|
||||||
|
|
||||||
Arbitrator two = new Arbitrator(new NodeAddress("arbitrator:2"), null, null,
|
Arbitrator two = new Arbitrator(new NodeAddress("arbitrator:2"), null,
|
||||||
languagesTwo, 0L, null, "", null,
|
languagesTwo, 0L, null, "", null,
|
||||||
null, null);
|
null, null);
|
||||||
|
|
||||||
|
@ -39,7 +39,6 @@ public class ArbitratorTest {
|
|||||||
|
|
||||||
public static Arbitrator getArbitratorMock() {
|
public static Arbitrator getArbitratorMock() {
|
||||||
return new Arbitrator(new NodeAddress("host", 1000),
|
return new Arbitrator(new NodeAddress("host", 1000),
|
||||||
"xmraddress",
|
|
||||||
new PubKeyRing(getBytes(100), getBytes(100)),
|
new PubKeyRing(getBytes(100), getBytes(100)),
|
||||||
Lists.newArrayList(),
|
Lists.newArrayList(),
|
||||||
new Date().getTime(),
|
new Date().getTime(),
|
||||||
|
@ -45,7 +45,6 @@ public class ArbitratorRegistrationViewModel extends AgentRegistrationViewModel<
|
|||||||
String emailAddress) {
|
String emailAddress) {
|
||||||
return new Arbitrator(
|
return new Arbitrator(
|
||||||
p2PService.getAddress(),
|
p2PService.getAddress(),
|
||||||
xmrWalletService.getWallet().getPrimaryAddress(), // TODO: how is arbitrator address used?
|
|
||||||
keyRing.getPubKeyRing(),
|
keyRing.getPubKeyRing(),
|
||||||
new ArrayList<>(languageCodes),
|
new ArrayList<>(languageCodes),
|
||||||
new Date().getTime(),
|
new Date().getTime(),
|
||||||
|
@ -19,6 +19,8 @@ package haveno.desktop.main.funds.deposit;
|
|||||||
|
|
||||||
import com.google.common.base.Supplier;
|
import com.google.common.base.Supplier;
|
||||||
import com.google.common.base.Suppliers;
|
import com.google.common.base.Suppliers;
|
||||||
|
|
||||||
|
import common.types.Filter;
|
||||||
import haveno.core.locale.Res;
|
import haveno.core.locale.Res;
|
||||||
import haveno.core.trade.HavenoUtils;
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.util.coin.CoinFormatter;
|
import haveno.core.util.coin.CoinFormatter;
|
||||||
@ -85,7 +87,7 @@ class DepositListItem {
|
|||||||
tooltip = new Tooltip(Res.get("shared.notUsedYet"));
|
tooltip = new Tooltip(Res.get("shared.notUsedYet"));
|
||||||
txConfidenceIndicator.setProgress(0);
|
txConfidenceIndicator.setProgress(0);
|
||||||
txConfidenceIndicator.setTooltip(tooltip);
|
txConfidenceIndicator.setTooltip(tooltip);
|
||||||
MoneroTx tx = getTxWithFewestConfirmations();
|
MoneroTx tx = getTxWithFewestConfirmations(cachedTxs);
|
||||||
if (tx == null) {
|
if (tx == null) {
|
||||||
txConfidenceIndicator.setVisible(false);
|
txConfidenceIndicator.setVisible(false);
|
||||||
} else {
|
} else {
|
||||||
@ -132,19 +134,19 @@ class DepositListItem {
|
|||||||
return numTxOutputs;
|
return numTxOutputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getNumConfirmationsSinceFirstUsed() {
|
public long getNumConfirmationsSinceFirstUsed(List<MoneroTxWallet> incomingTxs) {
|
||||||
MoneroTx tx = getTxWithFewestConfirmations();
|
MoneroTx tx = getTxWithFewestConfirmations(incomingTxs);
|
||||||
return tx == null ? 0 : tx.getNumConfirmations();
|
return tx == null ? 0 : tx.getNumConfirmations();
|
||||||
}
|
}
|
||||||
|
|
||||||
private MoneroTxWallet getTxWithFewestConfirmations() {
|
private MoneroTxWallet getTxWithFewestConfirmations(List<MoneroTxWallet> incomingTxs) {
|
||||||
|
|
||||||
// get txs with incoming transfers to subaddress
|
// get txs with incoming transfers to subaddress
|
||||||
List<MoneroTxWallet> txs = xmrWalletService.getWallet()
|
MoneroTxQuery query = new MoneroTxQuery()
|
||||||
.getTxs(new MoneroTxQuery()
|
|
||||||
.setTransferQuery(new MoneroTransferQuery()
|
.setTransferQuery(new MoneroTransferQuery()
|
||||||
.setIsIncoming(true)
|
.setIsIncoming(true)
|
||||||
.setSubaddressIndex(addressEntry.getSubaddressIndex())));
|
.setSubaddressIndex(addressEntry.getSubaddressIndex()));
|
||||||
|
List<MoneroTxWallet> txs = incomingTxs == null ? xmrWalletService.getWallet().getTxs(query) : Filter.apply(query, incomingTxs);
|
||||||
|
|
||||||
// get tx with fewest confirmations
|
// get tx with fewest confirmations
|
||||||
MoneroTxWallet highestTx = null;
|
MoneroTxWallet highestTx = null;
|
||||||
|
@ -147,9 +147,12 @@ public class DepositView extends ActivatableView<VBox, Void> {
|
|||||||
setUsageColumnCellFactory();
|
setUsageColumnCellFactory();
|
||||||
setConfidenceColumnCellFactory();
|
setConfidenceColumnCellFactory();
|
||||||
|
|
||||||
|
// prefetch all incoming txs to avoid query per subaddress
|
||||||
|
List<MoneroTxWallet> incomingTxs = xmrWalletService.getIncomingTxs();
|
||||||
|
|
||||||
addressColumn.setComparator(Comparator.comparing(DepositListItem::getAddressString));
|
addressColumn.setComparator(Comparator.comparing(DepositListItem::getAddressString));
|
||||||
balanceColumn.setComparator(Comparator.comparing(DepositListItem::getBalanceAsBI));
|
balanceColumn.setComparator(Comparator.comparing(DepositListItem::getBalanceAsBI));
|
||||||
confirmationsColumn.setComparator(Comparator.comparingLong(o -> o.getNumConfirmationsSinceFirstUsed()));
|
confirmationsColumn.setComparator(Comparator.comparingLong(o -> o.getNumConfirmationsSinceFirstUsed(incomingTxs)));
|
||||||
usageColumn.setComparator(Comparator.comparingInt(DepositListItem::getNumTxOutputs));
|
usageColumn.setComparator(Comparator.comparingInt(DepositListItem::getNumTxOutputs));
|
||||||
tableView.getSortOrder().add(usageColumn);
|
tableView.getSortOrder().add(usageColumn);
|
||||||
tableView.setItems(sortedList);
|
tableView.setItems(sortedList);
|
||||||
|
@ -35,7 +35,7 @@ class TransactionAwareOpenOffer implements TransactionAwareTradable {
|
|||||||
|
|
||||||
String txId = transaction.getHash();
|
String txId = transaction.getHash();
|
||||||
|
|
||||||
return paymentTxId.equals(txId);
|
return txId.equals(paymentTxId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Tradable asTradable() {
|
public Tradable asTradable() {
|
||||||
|
@ -34,6 +34,7 @@ import lombok.Getter;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import monero.wallet.model.MoneroIncomingTransfer;
|
import monero.wallet.model.MoneroIncomingTransfer;
|
||||||
import monero.wallet.model.MoneroOutgoingTransfer;
|
import monero.wallet.model.MoneroOutgoingTransfer;
|
||||||
|
import monero.wallet.model.MoneroTxQuery;
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
import monero.wallet.model.MoneroWalletListener;
|
import monero.wallet.model.MoneroWalletListener;
|
||||||
|
|
||||||
@ -60,6 +61,8 @@ class TransactionsListItem {
|
|||||||
@Getter
|
@Getter
|
||||||
private boolean initialTxConfidenceVisibility = true;
|
private boolean initialTxConfidenceVisibility = true;
|
||||||
private final Supplier<LazyFields> lazyFieldsSupplier;
|
private final Supplier<LazyFields> lazyFieldsSupplier;
|
||||||
|
private XmrWalletService xmrWalletService;
|
||||||
|
MoneroWalletListener walletListener;
|
||||||
|
|
||||||
private static class LazyFields {
|
private static class LazyFields {
|
||||||
TxConfidenceIndicator txConfidenceIndicator;
|
TxConfidenceIndicator txConfidenceIndicator;
|
||||||
@ -82,6 +85,8 @@ class TransactionsListItem {
|
|||||||
TransactionAwareTradable transactionAwareTradable) {
|
TransactionAwareTradable transactionAwareTradable) {
|
||||||
this.memo = tx.getNote();
|
this.memo = tx.getNote();
|
||||||
this.txId = tx.getHash();
|
this.txId = tx.getHash();
|
||||||
|
this.xmrWalletService = xmrWalletService;
|
||||||
|
this.confirmations = tx.getNumConfirmations() == null ? 0 : tx.getNumConfirmations();
|
||||||
|
|
||||||
Optional<Tradable> optionalTradable = Optional.ofNullable(transactionAwareTradable)
|
Optional<Tradable> optionalTradable = Optional.ofNullable(transactionAwareTradable)
|
||||||
.map(TransactionAwareTradable::asTradable);
|
.map(TransactionAwareTradable::asTradable);
|
||||||
@ -182,18 +187,24 @@ class TransactionsListItem {
|
|||||||
}});
|
}});
|
||||||
|
|
||||||
// listen for tx updates
|
// listen for tx updates
|
||||||
// TODO: this only listens for new blocks, listen for double spend
|
walletListener = new MoneroWalletListener() {
|
||||||
xmrWalletService.addWalletListener(new MoneroWalletListener() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onNewBlock(long height) {
|
public void onNewBlock(long height) {
|
||||||
MoneroTxWallet tx = xmrWalletService.getWallet().getTx(txId);
|
MoneroTxWallet tx = xmrWalletService.getWallet().getTxs(new MoneroTxQuery()
|
||||||
|
.setHash(txId)
|
||||||
|
.setInTxPool(confirmations > 0 ? false : null)).get(0);
|
||||||
GUIUtil.updateConfidence(tx, lazy().tooltip, lazy().txConfidenceIndicator);
|
GUIUtil.updateConfidence(tx, lazy().tooltip, lazy().txConfidenceIndicator);
|
||||||
confirmations = tx.getNumConfirmations();
|
confirmations = tx.getNumConfirmations();
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
xmrWalletService.addWalletListener(walletListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
|
if (walletListener != null) {
|
||||||
|
xmrWalletService.removeWalletListener(walletListener);
|
||||||
|
walletListener = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public TxConfidenceIndicator getTxConfidenceIndicator() {
|
public TxConfidenceIndicator getTxConfidenceIndicator() {
|
||||||
|
@ -67,7 +67,6 @@ import javafx.scene.layout.HBox;
|
|||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
import org.bitcoinj.core.Coin;
|
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -727,13 +726,13 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||||||
sellerPayoutAmount.equals(sellerSecurityDeposit)) {
|
sellerPayoutAmount.equals(sellerSecurityDeposit)) {
|
||||||
buyerGetsTradeAmountRadioButton.setSelected(true);
|
buyerGetsTradeAmountRadioButton.setSelected(true);
|
||||||
} else if (buyerPayoutAmount.equals(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)) &&
|
} else if (buyerPayoutAmount.equals(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)) &&
|
||||||
sellerPayoutAmount.equals(Coin.ZERO)) {
|
sellerPayoutAmount.equals(BigInteger.valueOf(0))) {
|
||||||
buyerGetsAllRadioButton.setSelected(true);
|
buyerGetsAllRadioButton.setSelected(true);
|
||||||
} else if (sellerPayoutAmount.equals(tradeAmount.add(sellerSecurityDeposit))
|
} else if (sellerPayoutAmount.equals(tradeAmount.add(sellerSecurityDeposit))
|
||||||
&& buyerPayoutAmount.equals(buyerSecurityDeposit)) {
|
&& buyerPayoutAmount.equals(buyerSecurityDeposit)) {
|
||||||
sellerGetsTradeAmountRadioButton.setSelected(true);
|
sellerGetsTradeAmountRadioButton.setSelected(true);
|
||||||
} else if (sellerPayoutAmount.equals(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit))
|
} else if (sellerPayoutAmount.equals(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit))
|
||||||
&& buyerPayoutAmount.equals(Coin.ZERO)) {
|
&& buyerPayoutAmount.equals(BigInteger.valueOf(0))) {
|
||||||
sellerGetsAllRadioButton.setSelected(true);
|
sellerGetsAllRadioButton.setSelected(true);
|
||||||
} else {
|
} else {
|
||||||
customRadioButton.setSelected(true);
|
customRadioButton.setSelected(true);
|
||||||
|
@ -448,6 +448,8 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
|||||||
|
|
||||||
switch (payoutState) {
|
switch (payoutState) {
|
||||||
case PAYOUT_PUBLISHED:
|
case PAYOUT_PUBLISHED:
|
||||||
|
case PAYOUT_CONFIRMED:
|
||||||
|
case PAYOUT_UNLOCKED:
|
||||||
sellerState.set(SellerState.STEP4);
|
sellerState.set(SellerState.STEP4);
|
||||||
buyerState.set(BuyerState.STEP4);
|
buyerState.set(BuyerState.STEP4);
|
||||||
break;
|
break;
|
||||||
|
@ -788,6 +788,14 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
|
|||||||
.show();
|
.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// We use opposite property (useStandbyMode) in preferences to have the default value (false) set as we want it,
|
||||||
|
// so users who update gets set avoidStandbyMode=true (useStandbyMode=false)
|
||||||
|
if (displayStandbyModeFeature) {
|
||||||
|
avoidStandbyMode.setSelected(!preferences.isUseStandbyMode());
|
||||||
|
avoidStandbyMode.setOnAction(e -> preferences.setUseStandbyMode(!avoidStandbyMode.isSelected()));
|
||||||
|
} else {
|
||||||
|
preferences.setUseStandbyMode(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void activateAutoConfirmPreferences() {
|
private void activateAutoConfirmPreferences() {
|
||||||
|
@ -190,6 +190,7 @@ public class P2PService implements SetupListener, MessageListener, ConnectionLis
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doShutDown() {
|
private void doShutDown() {
|
||||||
|
|
||||||
if (p2PDataStorage != null) {
|
if (p2PDataStorage != null) {
|
||||||
p2PDataStorage.shutDown();
|
p2PDataStorage.shutDown();
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,6 @@ import javax.inject.Singleton;
|
|||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.time.Clock;
|
import java.time.Clock;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@ -345,11 +344,7 @@ public class MailboxMessageService implements HashMapChangedListener, PersistedD
|
|||||||
.map(e -> (ProtectedMailboxStorageEntry) e)
|
.map(e -> (ProtectedMailboxStorageEntry) e)
|
||||||
.filter(e -> networkNode.getNodeAddress() != null)
|
.filter(e -> networkNode.getNodeAddress() != null)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
if (entries.size() > 1) {
|
|
||||||
threadedBatchProcessMailboxEntries(entries);
|
threadedBatchProcessMailboxEntries(entries);
|
||||||
} else if (entries.size() == 1) {
|
|
||||||
processSingleMailboxEntry(entries);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -375,14 +370,6 @@ public class MailboxMessageService implements HashMapChangedListener, PersistedD
|
|||||||
p2PDataStorage.addHashMapChangedListener(this);
|
p2PDataStorage.addHashMapChangedListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processSingleMailboxEntry(Collection<ProtectedMailboxStorageEntry> protectedMailboxStorageEntries) {
|
|
||||||
checkArgument(protectedMailboxStorageEntries.size() == 1);
|
|
||||||
var mailboxItems = new ArrayList<>(getMailboxItems(protectedMailboxStorageEntries));
|
|
||||||
if (mailboxItems.size() == 1) {
|
|
||||||
handleMailboxItem(mailboxItems.get(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We run the batch processing of all mailbox messages we have received at startup in a thread to not block the UI.
|
// We run the batch processing of all mailbox messages we have received at startup in a thread to not block the UI.
|
||||||
// For about 1000 messages decryption takes about 1 sec.
|
// For about 1000 messages decryption takes about 1 sec.
|
||||||
private void threadedBatchProcessMailboxEntries(Collection<ProtectedMailboxStorageEntry> protectedMailboxStorageEntries) {
|
private void threadedBatchProcessMailboxEntries(Collection<ProtectedMailboxStorageEntry> protectedMailboxStorageEntries) {
|
||||||
@ -390,7 +377,7 @@ public class MailboxMessageService implements HashMapChangedListener, PersistedD
|
|||||||
long ts = System.currentTimeMillis();
|
long ts = System.currentTimeMillis();
|
||||||
ListenableFuture<Set<MailboxItem>> future = executor.submit(() -> {
|
ListenableFuture<Set<MailboxItem>> future = executor.submit(() -> {
|
||||||
var mailboxItems = getMailboxItems(protectedMailboxStorageEntries);
|
var mailboxItems = getMailboxItems(protectedMailboxStorageEntries);
|
||||||
log.info("Batch processing of {} mailbox entries took {} ms",
|
log.trace("Batch processing of {} mailbox entries took {} ms",
|
||||||
protectedMailboxStorageEntries.size(),
|
protectedMailboxStorageEntries.size(),
|
||||||
System.currentTimeMillis() - ts);
|
System.currentTimeMillis() - ts);
|
||||||
return mailboxItems;
|
return mailboxItems;
|
||||||
|
@ -520,10 +520,9 @@ message Arbitrator {
|
|||||||
string registration_signature = 4;
|
string registration_signature = 4;
|
||||||
bytes registration_pub_key = 5;
|
bytes registration_pub_key = 5;
|
||||||
PubKeyRing pub_key_ring = 6;
|
PubKeyRing pub_key_ring = 6;
|
||||||
string xmr_address = 7;
|
string email_address = 7;
|
||||||
string email_address = 8;
|
string info = 8;
|
||||||
string info = 9;
|
map<string, string> extra_data = 9;
|
||||||
map<string, string> extra_data = 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message Mediator {
|
message Mediator {
|
||||||
|
Loading…
Reference in New Issue
Block a user