mirror of
https://github.com/retoaccess1/haveno-reto.git
synced 2024-09-20 04:46:24 +02:00
Update to v1.0.10
Update to v1.0.10
This commit is contained in:
commit
f2c9cf05eb
@ -26,7 +26,7 @@ See the [FAQ on our website](https://haveno.exchange/faq/) for more information.
|
|||||||
|
|
||||||
## Status of the project
|
## Status of the project
|
||||||
|
|
||||||
Haveno can be used on Monero's main network by using a third party Haveno network. We do not officially endorse any networks at this time, but they can be found online.
|
Haveno can be used on Monero's main network by using a third party Haveno network. We do not officially endorse any networks at this time.
|
||||||
|
|
||||||
A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the network.
|
A test network is also available for users to make test trades using Monero's stagenet. See the [instructions](https://github.com/haveno-dex/haveno/blob/master/docs/installing.md) to build Haveno and connect to the network.
|
||||||
|
|
||||||
|
@ -317,9 +317,6 @@ configure(project(':common')) {
|
|||||||
exclude(module: 'animal-sniffer-annotations')
|
exclude(module: 'animal-sniffer-annotations')
|
||||||
}
|
}
|
||||||
|
|
||||||
// override transitive dependency version from 1.5 to the same version just identified by commit number.
|
|
||||||
// Remove this if transitive dependency is changed to something else than 1.5
|
|
||||||
implementation(group: 'com.github.JesusMcCloud', name: 'jtorctl') { version { strictly "[9b5ba2036b]" } }
|
|
||||||
implementation "org.openjfx:javafx-base:$javafxVersion:$os"
|
implementation "org.openjfx:javafx-base:$javafxVersion:$os"
|
||||||
implementation "org.openjfx:javafx-graphics:$javafxVersion:$os"
|
implementation "org.openjfx:javafx-graphics:$javafxVersion:$os"
|
||||||
}
|
}
|
||||||
@ -610,7 +607,7 @@ configure(project(':desktop')) {
|
|||||||
apply plugin: 'com.github.johnrengelman.shadow'
|
apply plugin: 'com.github.johnrengelman.shadow'
|
||||||
apply from: 'package/package.gradle'
|
apply from: 'package/package.gradle'
|
||||||
|
|
||||||
version = '1.0.9-SNAPSHOT'
|
version = '1.0.10-SNAPSHOT'
|
||||||
|
|
||||||
jar.manifest.attributes(
|
jar.manifest.attributes(
|
||||||
"Implementation-Title": project.name,
|
"Implementation-Title": project.name,
|
||||||
|
@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument;
|
|||||||
public class Version {
|
public class Version {
|
||||||
// The application versions
|
// The application versions
|
||||||
// We use semantic versioning with major, minor and patch
|
// We use semantic versioning with major, minor and patch
|
||||||
public static final String VERSION = "1.0.9";
|
public static final String VERSION = "1.0.10";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds a list of the tagged resource files for optimizing the getData requests.
|
* Holds a list of the tagged resource files for optimizing the getData requests.
|
||||||
|
@ -77,6 +77,7 @@ public class Config {
|
|||||||
public static final String SEED_NODES = "seedNodes";
|
public static final String SEED_NODES = "seedNodes";
|
||||||
public static final String BAN_LIST = "banList";
|
public static final String BAN_LIST = "banList";
|
||||||
public static final String NODE_PORT = "nodePort";
|
public static final String NODE_PORT = "nodePort";
|
||||||
|
public static final String HIDDEN_SERVICE_ADDRESS = "hiddenServiceAddress";
|
||||||
public static final String USE_LOCALHOST_FOR_P2P = "useLocalhostForP2P";
|
public static final String USE_LOCALHOST_FOR_P2P = "useLocalhostForP2P";
|
||||||
public static final String MAX_CONNECTIONS = "maxConnections";
|
public static final String MAX_CONNECTIONS = "maxConnections";
|
||||||
public static final String SOCKS_5_PROXY_XMR_ADDRESS = "socks5ProxyXmrAddress";
|
public static final String SOCKS_5_PROXY_XMR_ADDRESS = "socks5ProxyXmrAddress";
|
||||||
@ -151,6 +152,7 @@ public class Config {
|
|||||||
public final File appDataDir;
|
public final File appDataDir;
|
||||||
public final int walletRpcBindPort;
|
public final int walletRpcBindPort;
|
||||||
public final int nodePort;
|
public final int nodePort;
|
||||||
|
public final String hiddenServiceAddress;
|
||||||
public final int maxMemory;
|
public final int maxMemory;
|
||||||
public final String logLevel;
|
public final String logLevel;
|
||||||
public final List<String> bannedXmrNodes;
|
public final List<String> bannedXmrNodes;
|
||||||
@ -286,6 +288,12 @@ public class Config {
|
|||||||
.ofType(Integer.class)
|
.ofType(Integer.class)
|
||||||
.defaultsTo(9999);
|
.defaultsTo(9999);
|
||||||
|
|
||||||
|
ArgumentAcceptingOptionSpec<String> hiddenServiceAddressOpt =
|
||||||
|
parser.accepts(HIDDEN_SERVICE_ADDRESS, "Hidden Service Address to listen on")
|
||||||
|
.withRequiredArg()
|
||||||
|
.ofType(String.class)
|
||||||
|
.defaultsTo("");
|
||||||
|
|
||||||
ArgumentAcceptingOptionSpec<Integer> walletRpcBindPortOpt =
|
ArgumentAcceptingOptionSpec<Integer> walletRpcBindPortOpt =
|
||||||
parser.accepts(WALLET_RPC_BIND_PORT, "Port to bind the wallet RPC on")
|
parser.accepts(WALLET_RPC_BIND_PORT, "Port to bind the wallet RPC on")
|
||||||
.withRequiredArg()
|
.withRequiredArg()
|
||||||
@ -670,6 +678,7 @@ public class Config {
|
|||||||
this.helpRequested = options.has(helpOpt);
|
this.helpRequested = options.has(helpOpt);
|
||||||
this.configFile = configFile;
|
this.configFile = configFile;
|
||||||
this.nodePort = options.valueOf(nodePortOpt);
|
this.nodePort = options.valueOf(nodePortOpt);
|
||||||
|
this.hiddenServiceAddress = options.valueOf(hiddenServiceAddressOpt);
|
||||||
this.walletRpcBindPort = options.valueOf(walletRpcBindPortOpt);
|
this.walletRpcBindPort = options.valueOf(walletRpcBindPortOpt);
|
||||||
this.maxMemory = options.valueOf(maxMemoryOpt);
|
this.maxMemory = options.valueOf(maxMemoryOpt);
|
||||||
this.logLevel = options.valueOf(logLevelOpt);
|
this.logLevel = options.valueOf(logLevelOpt);
|
||||||
|
@ -112,13 +112,13 @@ public class CoreDisputesService {
|
|||||||
|
|
||||||
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
|
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
|
||||||
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
|
// one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage.
|
||||||
disputeManager.sendDisputeOpenedMessage(dispute, false, trade.getSelf().getUpdatedMultisigHex(), resultHandler, faultHandler);
|
disputeManager.sendDisputeOpenedMessage(dispute, resultHandler, faultHandler);
|
||||||
tradeManager.requestPersistence();
|
tradeManager.requestPersistence();
|
||||||
}, trade.getId());
|
}, trade.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) {
|
public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
byte[] payoutTxSerialized = null;
|
byte[] payoutTxSerialized = null;
|
||||||
String payoutTxHashAsString = null;
|
String payoutTxHashAsString = null;
|
||||||
|
|
||||||
@ -163,7 +163,7 @@ public class CoreDisputesService {
|
|||||||
if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get();
|
if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get();
|
||||||
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
|
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
|
||||||
|
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// create dispute result
|
// create dispute result
|
||||||
@ -275,10 +275,12 @@ public class CoreDisputesService {
|
|||||||
disputeResult.summaryNotesProperty().get()
|
disputeResult.summaryNotesProperty().get()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (reason == DisputeResult.Reason.OPTION_TRADE &&
|
synchronized (dispute.getChatMessages()) {
|
||||||
|
if (reason == DisputeResult.Reason.OPTION_TRADE &&
|
||||||
dispute.getChatMessages().size() > 1 &&
|
dispute.getChatMessages().size() > 1 &&
|
||||||
dispute.getChatMessages().get(1).isSystemMessage()) {
|
dispute.getChatMessages().get(1).isSystemMessage()) {
|
||||||
textToSign += "\n" + dispute.getChatMessages().get(1).getMessage() + "\n";
|
textToSign += "\n" + dispute.getChatMessages().get(1).getMessage() + "\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign);
|
String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign);
|
||||||
|
@ -178,7 +178,7 @@ class CoreWalletsService {
|
|||||||
verifyWalletsAreAvailable();
|
verifyWalletsAreAvailable();
|
||||||
verifyEncryptedWalletIsUnlocked();
|
verifyEncryptedWalletIsUnlocked();
|
||||||
try {
|
try {
|
||||||
return xmrWalletService.getWallet().relayTx(metadata);
|
return xmrWalletService.relayTx(metadata);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("", ex);
|
log.error("", ex);
|
||||||
throw new IllegalStateException(ex);
|
throw new IllegalStateException(ex);
|
||||||
|
@ -101,6 +101,7 @@ public final class XmrConnectionService {
|
|||||||
private Long lastLogPollErrorTimestamp;
|
private Long lastLogPollErrorTimestamp;
|
||||||
private Long syncStartHeight = null;
|
private Long syncStartHeight = null;
|
||||||
private TaskLooper daemonPollLooper;
|
private TaskLooper daemonPollLooper;
|
||||||
|
private long lastRefreshPeriodMs = 0;
|
||||||
@Getter
|
@Getter
|
||||||
private boolean isShutDownStarted;
|
private boolean isShutDownStarted;
|
||||||
private List<MoneroConnectionManagerListener> listeners = new ArrayList<>();
|
private List<MoneroConnectionManagerListener> listeners = new ArrayList<>();
|
||||||
@ -273,7 +274,11 @@ public final class XmrConnectionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public synchronized boolean requestSwitchToNextBestConnection() {
|
public synchronized boolean requestSwitchToNextBestConnection() {
|
||||||
log.warn("Requesting switch to next best monerod, current monerod={}", getConnection() == null ? null : getConnection().getUri());
|
return requestSwitchToNextBestConnection(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
|
||||||
|
log.warn("Requesting switch to next best monerod, source monerod={}", sourceConnection == null ? getConnection() == null ? null : getConnection().getUri() : sourceConnection.getUri());
|
||||||
|
|
||||||
// skip if shut down started
|
// skip if shut down started
|
||||||
if (isShutDownStarted) {
|
if (isShutDownStarted) {
|
||||||
@ -281,9 +286,15 @@ public final class XmrConnectionService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// skip if connection is already switched
|
||||||
|
if (sourceConnection != null && sourceConnection != getConnection()) {
|
||||||
|
log.warn("Skipping switch to next best Monero connection because source connection is not current connection");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// skip if connection is fixed
|
// skip if connection is fixed
|
||||||
if (isFixedConnection() || !connectionManager.getAutoSwitch()) {
|
if (isFixedConnection() || !connectionManager.getAutoSwitch()) {
|
||||||
log.info("Skipping switch to next best Monero connection because connection is fixed or auto switch is disabled");
|
log.warn("Skipping switch to next best Monero connection because connection is fixed or auto switch is disabled");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,7 +354,11 @@ public final class XmrConnectionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public long getRefreshPeriodMs() {
|
public long getRefreshPeriodMs() {
|
||||||
return connectionList.getRefreshPeriod() > 0 ? connectionList.getRefreshPeriod() : getDefaultRefreshPeriodMs();
|
return connectionList.getRefreshPeriod() > 0 ? connectionList.getRefreshPeriod() : getDefaultRefreshPeriodMs(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getInternalRefreshPeriodMs() {
|
||||||
|
return connectionList.getRefreshPeriod() > 0 ? connectionList.getRefreshPeriod() : getDefaultRefreshPeriodMs(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void verifyConnection() {
|
public void verifyConnection() {
|
||||||
@ -413,12 +428,16 @@ public final class XmrConnectionService {
|
|||||||
return connection != null && HavenoUtils.isLocalHost(connection.getUri());
|
return connection != null && HavenoUtils.isLocalHost(connection.getUri());
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getDefaultRefreshPeriodMs() {
|
private long getDefaultRefreshPeriodMs(boolean internal) {
|
||||||
MoneroRpcConnection connection = getConnection();
|
MoneroRpcConnection connection = getConnection();
|
||||||
if (connection == null) return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS;
|
if (connection == null) return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS;
|
||||||
if (isConnectionLocalHost(connection)) {
|
if (isConnectionLocalHost(connection)) {
|
||||||
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
|
if (internal) return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS;
|
||||||
else return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing
|
if (lastInfo != null && (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight())) {
|
||||||
|
return REFRESH_PERIOD_HTTP_MS; // refresh slower if syncing or bootstrapped
|
||||||
|
} else {
|
||||||
|
return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing
|
||||||
|
}
|
||||||
} else if (isProxyApplied(connection)) {
|
} else if (isProxyApplied(connection)) {
|
||||||
return REFRESH_PERIOD_ONION_MS;
|
return REFRESH_PERIOD_ONION_MS;
|
||||||
} else {
|
} else {
|
||||||
@ -638,7 +657,7 @@ public final class XmrConnectionService {
|
|||||||
// update polling
|
// update polling
|
||||||
doPollDaemon();
|
doPollDaemon();
|
||||||
if (currentConnection != getConnection()) return; // polling can change connection
|
if (currentConnection != getConnection()) return; // polling can change connection
|
||||||
UserThread.runAfter(() -> updatePolling(), getRefreshPeriodMs() / 1000);
|
UserThread.runAfter(() -> updatePolling(), getInternalRefreshPeriodMs() / 1000);
|
||||||
|
|
||||||
// notify listeners in parallel
|
// notify listeners in parallel
|
||||||
log.info("XmrConnectionService.onConnectionChanged() uri={}, connected={}", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : isConnected);
|
log.info("XmrConnectionService.onConnectionChanged() uri={}, connected={}", currentConnection == null ? null : currentConnection.getUri(), currentConnection == null ? "false" : isConnected);
|
||||||
@ -658,7 +677,7 @@ public final class XmrConnectionService {
|
|||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
if (daemonPollLooper != null) daemonPollLooper.stop();
|
if (daemonPollLooper != null) daemonPollLooper.stop();
|
||||||
daemonPollLooper = new TaskLooper(() -> pollDaemon());
|
daemonPollLooper = new TaskLooper(() -> pollDaemon());
|
||||||
daemonPollLooper.start(getRefreshPeriodMs());
|
daemonPollLooper.start(getInternalRefreshPeriodMs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -715,6 +734,13 @@ public final class XmrConnectionService {
|
|||||||
// connected to daemon
|
// connected to daemon
|
||||||
isConnected = true;
|
isConnected = true;
|
||||||
|
|
||||||
|
// announce connection change if refresh period changes
|
||||||
|
if (getRefreshPeriodMs() != lastRefreshPeriodMs) {
|
||||||
|
lastRefreshPeriodMs = getRefreshPeriodMs();
|
||||||
|
onConnectionChanged(getConnection()); // causes new poll
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// update properties on user thread
|
// update properties on user thread
|
||||||
UserThread.execute(() -> {
|
UserThread.execute(() -> {
|
||||||
|
|
||||||
|
@ -132,35 +132,14 @@ public class WalletAppSetup {
|
|||||||
(numConnectionUpdates, walletDownloadPercentage, walletHeight, exception, errorMsg) -> {
|
(numConnectionUpdates, walletDownloadPercentage, walletHeight, exception, errorMsg) -> {
|
||||||
String result;
|
String result;
|
||||||
if (exception == null && errorMsg == null) {
|
if (exception == null && errorMsg == null) {
|
||||||
|
|
||||||
// update wallet sync progress
|
// update daemon sync progress
|
||||||
double walletDownloadPercentageD = (double) walletDownloadPercentage;
|
double chainDownloadPercentageD = xmrConnectionService.downloadPercentageProperty().doubleValue();
|
||||||
xmrWalletSyncProgress.set(walletDownloadPercentageD);
|
Long bestChainHeight = xmrConnectionService.chainHeightProperty().get();
|
||||||
Long bestWalletHeight = walletHeight == null ? null : (Long) walletHeight;
|
String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ? String.valueOf(bestChainHeight) : "";
|
||||||
String walletHeightAsString = bestWalletHeight != null && bestWalletHeight > 0 ? String.valueOf(bestWalletHeight) : "";
|
if (chainDownloadPercentageD < 1) {
|
||||||
if (walletDownloadPercentageD == 1) {
|
|
||||||
String synchronizedWith = Res.get("mainView.footer.xmrInfo.syncedWith", getXmrWalletNetworkAsString(), walletHeightAsString);
|
|
||||||
String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable
|
|
||||||
result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo);
|
|
||||||
getXmrSplashSyncIconId().set("image-connection-synced");
|
|
||||||
downloadCompleteHandler.run();
|
|
||||||
} else if (walletDownloadPercentageD > 0) {
|
|
||||||
String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWalletWith", getXmrWalletNetworkAsString(), walletHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(walletDownloadPercentageD));
|
|
||||||
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
|
|
||||||
getXmrSplashSyncIconId().set(""); // clear synced icon
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// update daemon sync progress
|
|
||||||
double chainDownloadPercentageD = xmrConnectionService.downloadPercentageProperty().doubleValue();
|
|
||||||
xmrDaemonSyncProgress.set(chainDownloadPercentageD);
|
xmrDaemonSyncProgress.set(chainDownloadPercentageD);
|
||||||
Long bestChainHeight = xmrConnectionService.chainHeightProperty().get();
|
if (chainDownloadPercentageD > 0.0) {
|
||||||
String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ? String.valueOf(bestChainHeight) : "";
|
|
||||||
if (chainDownloadPercentageD == 1) {
|
|
||||||
String synchronizedWith = Res.get("mainView.footer.xmrInfo.connectedTo", getXmrDaemonNetworkAsString(), chainHeightAsString);
|
|
||||||
String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable
|
|
||||||
result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo);
|
|
||||||
getXmrSplashSyncIconId().set("image-connection-synced");
|
|
||||||
} else if (chainDownloadPercentageD > 0.0) {
|
|
||||||
String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWith", getXmrDaemonNetworkAsString(), chainHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(chainDownloadPercentageD));
|
String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWith", getXmrDaemonNetworkAsString(), chainHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(chainDownloadPercentageD));
|
||||||
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
|
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
|
||||||
} else {
|
} else {
|
||||||
@ -168,6 +147,29 @@ public class WalletAppSetup {
|
|||||||
Res.get("mainView.footer.xmrInfo.connectingTo"),
|
Res.get("mainView.footer.xmrInfo.connectingTo"),
|
||||||
getXmrDaemonNetworkAsString());
|
getXmrDaemonNetworkAsString());
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// update wallet sync progress
|
||||||
|
double walletDownloadPercentageD = (double) walletDownloadPercentage;
|
||||||
|
xmrWalletSyncProgress.set(walletDownloadPercentageD);
|
||||||
|
Long bestWalletHeight = walletHeight == null ? null : (Long) walletHeight;
|
||||||
|
String walletHeightAsString = bestWalletHeight != null && bestWalletHeight > 0 ? String.valueOf(bestWalletHeight) : "";
|
||||||
|
if (walletDownloadPercentageD == 1) {
|
||||||
|
String synchronizedWith = Res.get("mainView.footer.xmrInfo.syncedWith", getXmrWalletNetworkAsString(), walletHeightAsString);
|
||||||
|
String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable
|
||||||
|
result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo);
|
||||||
|
getXmrSplashSyncIconId().set("image-connection-synced");
|
||||||
|
downloadCompleteHandler.run();
|
||||||
|
} else if (walletDownloadPercentageD >= 0) {
|
||||||
|
String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWalletWith", getXmrWalletNetworkAsString(), walletHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(walletDownloadPercentageD));
|
||||||
|
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
|
||||||
|
getXmrSplashSyncIconId().set(""); // clear synced icon
|
||||||
|
} else {
|
||||||
|
String synchronizedWith = Res.get("mainView.footer.xmrInfo.connectedTo", getXmrDaemonNetworkAsString(), chainHeightAsString);
|
||||||
|
String feeInfo = ""; // TODO: feeService.isFeeAvailable() returns true, disable
|
||||||
|
result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo);
|
||||||
|
getXmrSplashSyncIconId().set("image-connection-synced");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result = Res.get("mainView.footer.xmrInfo",
|
result = Res.get("mainView.footer.xmrInfo",
|
||||||
|
@ -114,16 +114,18 @@ public class DisputeMsgEvents {
|
|||||||
// We check at every new message if it might be a message sent after the dispute had been closed. If that is the
|
// We check at every new message if it might be a message sent after the dispute had been closed. If that is the
|
||||||
// case we revert the isClosed flag so that the UI can reopen the dispute and indicate that a new dispute
|
// case we revert the isClosed flag so that the UI can reopen the dispute and indicate that a new dispute
|
||||||
// message arrived.
|
// message arrived.
|
||||||
ObservableList<ChatMessage> chatMessages = dispute.getChatMessages();
|
synchronized (dispute.getChatMessages()) {
|
||||||
// If last message is not a result message we re-open as we might have received a new message from the
|
ObservableList<ChatMessage> chatMessages = dispute.getChatMessages();
|
||||||
// trader/mediator/arbitrator who has reopened the case
|
// If last message is not a result message we re-open as we might have received a new message from the
|
||||||
if (dispute.isClosed() && !chatMessages.isEmpty() && !chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute)) {
|
// trader/mediator/arbitrator who has reopened the case
|
||||||
dispute.reOpen();
|
if (dispute.isClosed() && !chatMessages.isEmpty() && !chatMessages.get(chatMessages.size() - 1).isResultMessage(dispute)) {
|
||||||
if (dispute.getSupportType() == SupportType.MEDIATION) {
|
dispute.reOpen();
|
||||||
mediationManager.requestPersistence();
|
if (dispute.getSupportType() == SupportType.MEDIATION) {
|
||||||
} else if (dispute.getSupportType() == SupportType.REFUND) {
|
mediationManager.requestPersistence();
|
||||||
refundManager.requestPersistence();
|
} else if (dispute.getSupportType() == SupportType.REFUND) {
|
||||||
|
refundManager.requestPersistence();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,13 +28,10 @@ import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
|||||||
import haveno.core.trade.HavenoUtils;
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.user.Preferences;
|
import haveno.core.user.Preferences;
|
||||||
import haveno.core.user.User;
|
import haveno.core.user.User;
|
||||||
import haveno.network.p2p.NodeAddress;
|
|
||||||
import haveno.network.p2p.P2PService;
|
import haveno.network.p2p.P2PService;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import javafx.collections.SetChangeListener;
|
import javafx.collections.SetChangeListener;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@ -218,7 +215,7 @@ public class OfferFilterService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasValidSignature(Offer offer) {
|
private boolean hasValidSignature(Offer offer) {
|
||||||
|
|
||||||
// get accepted arbitrator by address
|
// get accepted arbitrator by address
|
||||||
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner());
|
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner());
|
||||||
@ -230,9 +227,11 @@ public class OfferFilterService {
|
|||||||
if (thisArbitrator.getNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) arbitrator = thisArbitrator; // TODO: unnecessary to compare arbitrator and p2pservice address?
|
if (thisArbitrator.getNodeAddress().equals(p2PService.getNetworkNode().getNodeAddress())) arbitrator = thisArbitrator; // TODO: unnecessary to compare arbitrator and p2pservice address?
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// otherwise log warning that arbitrator is unregistered
|
// // otherwise log warning that arbitrator is unregistered
|
||||||
List<NodeAddress> arbitratorAddresses = user.getAcceptedArbitrators().stream().map(Arbitrator::getNodeAddress).collect(Collectors.toList());
|
// List<NodeAddress> arbitratorAddresses = user.getAcceptedArbitrators().stream().map(Arbitrator::getNodeAddress).collect(Collectors.toList());
|
||||||
log.warn("No arbitrator is registered with offer's signer. offerId={}, arbitrator signer={}, accepted arbitrators={}", offer.getId(), offer.getOfferPayload().getArbitratorSigner(), arbitratorAddresses);
|
// if (!arbitratorAddresses.isEmpty()) {
|
||||||
|
// log.warn("No arbitrator is registered with offer's signer. offerId={}, arbitrator signer={}, accepted arbitrators={}", offer.getId(), offer.getOfferPayload().getArbitratorSigner(), arbitratorAddresses);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -390,6 +390,8 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
null : new ArrayList<>(proto.getAcceptedBankIdsList());
|
null : new ArrayList<>(proto.getAcceptedBankIdsList());
|
||||||
List<String> acceptedCountryCodes = proto.getAcceptedCountryCodesList().isEmpty() ?
|
List<String> acceptedCountryCodes = proto.getAcceptedCountryCodesList().isEmpty() ?
|
||||||
null : new ArrayList<>(proto.getAcceptedCountryCodesList());
|
null : new ArrayList<>(proto.getAcceptedCountryCodesList());
|
||||||
|
List<String> reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ?
|
||||||
|
null : new ArrayList<>(proto.getReserveTxKeyImagesList());
|
||||||
String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge());
|
String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge());
|
||||||
Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
|
Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
|
||||||
null : proto.getExtraDataMap();
|
null : proto.getExtraDataMap();
|
||||||
@ -431,7 +433,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
|||||||
proto.getProtocolVersion(),
|
proto.getProtocolVersion(),
|
||||||
proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null,
|
proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null,
|
||||||
ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()),
|
ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()),
|
||||||
proto.getReserveTxKeyImagesList() == null ? null : new ArrayList<String>(proto.getReserveTxKeyImagesList()));
|
reserveTxKeyImages);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -110,6 +110,7 @@ import javafx.collections.FXCollections;
|
|||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import monero.common.MoneroRpcConnection;
|
||||||
import monero.daemon.model.MoneroKeyImageSpentStatus;
|
import monero.daemon.model.MoneroKeyImageSpentStatus;
|
||||||
import monero.daemon.model.MoneroTx;
|
import monero.daemon.model.MoneroTx;
|
||||||
import monero.wallet.model.MoneroIncomingTransfer;
|
import monero.wallet.model.MoneroIncomingTransfer;
|
||||||
@ -688,7 +689,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
addOpenOffer(editedOpenOffer);
|
addOpenOffer(editedOpenOffer);
|
||||||
|
|
||||||
if (editedOpenOffer.isAvailable())
|
if (editedOpenOffer.isAvailable())
|
||||||
republishOffer(editedOpenOffer);
|
maybeRepublishOffer(editedOpenOffer);
|
||||||
|
|
||||||
offersToBeEdited.remove(openOffer.getId());
|
offersToBeEdited.remove(openOffer.getId());
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
@ -863,8 +864,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler
|
TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler
|
||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
|
List<String> errorMessages = new ArrayList<String>();
|
||||||
synchronized (processOffersLock) {
|
synchronized (processOffersLock) {
|
||||||
List<String> errorMessages = new ArrayList<String>();
|
|
||||||
List<OpenOffer> openOffers = getOpenOffers();
|
List<OpenOffer> openOffers = getOpenOffers();
|
||||||
for (OpenOffer pendingOffer : openOffers) {
|
for (OpenOffer pendingOffer : openOffers) {
|
||||||
if (pendingOffer.getState() != OpenOffer.State.PENDING) continue;
|
if (pendingOffer.getState() != OpenOffer.State.PENDING) continue;
|
||||||
@ -887,12 +888,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
});
|
});
|
||||||
HavenoUtils.awaitLatch(latch);
|
HavenoUtils.awaitLatch(latch);
|
||||||
}
|
}
|
||||||
requestPersistence();
|
}
|
||||||
if (errorMessages.isEmpty()) {
|
requestPersistence();
|
||||||
if (resultHandler != null) resultHandler.handleResult(null);
|
if (errorMessages.isEmpty()) {
|
||||||
} else {
|
if (resultHandler != null) resultHandler.handleResult(null);
|
||||||
if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(errorMessages.toString());
|
} else {
|
||||||
}
|
if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(errorMessages.toString());
|
||||||
}
|
}
|
||||||
}, THREAD_ID);
|
}, THREAD_ID);
|
||||||
}
|
}
|
||||||
@ -962,9 +963,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// handle sufficient balance
|
// sign and post offer if enough funds
|
||||||
|
boolean hasFundsReserved = openOffer.getReserveTxHash() != null;
|
||||||
boolean hasSufficientBalance = xmrWalletService.getAvailableBalance().compareTo(amountNeeded) >= 0;
|
boolean hasSufficientBalance = xmrWalletService.getAvailableBalance().compareTo(amountNeeded) >= 0;
|
||||||
if (hasSufficientBalance) {
|
if (hasFundsReserved || hasSufficientBalance) {
|
||||||
signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler);
|
signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler);
|
||||||
return;
|
return;
|
||||||
} else if (openOffer.getScheduledTxHashes() == null) {
|
} else if (openOffer.getScheduledTxHashes() == null) {
|
||||||
@ -1083,11 +1085,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
BigInteger reserveAmount = openOffer.getOffer().getAmountNeeded();
|
BigInteger reserveAmount = openOffer.getOffer().getAmountNeeded();
|
||||||
xmrWalletService.swapAddressEntryToAvailable(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output(s)
|
xmrWalletService.swapAddressEntryToAvailable(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output(s)
|
||||||
MoneroTxWallet splitOutputTx = null;
|
MoneroTxWallet splitOutputTx = null;
|
||||||
synchronized (XmrWalletService.WALLET_LOCK) {
|
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
|
||||||
XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
|
XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
|
||||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||||
|
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||||
try {
|
try {
|
||||||
log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex());
|
log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex());
|
||||||
splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig()
|
splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig()
|
||||||
@ -1100,8 +1103,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds
|
if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds
|
||||||
log.warn("Error creating split output tx to fund offer, offerId={}, subaddress={}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
log.warn("Error creating split output tx to fund offer, offerId={}, subaddress={}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
||||||
|
xmrWalletService.handleWalletError(e, sourceConnection);
|
||||||
if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
if (xmrConnectionService.isConnected()) xmrWalletService.requestSwitchToNextBestConnection();
|
|
||||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1714,11 +1717,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
synchronized (openOffers) {
|
synchronized (openOffers) {
|
||||||
contained = openOffers.contains(openOffer);
|
contained = openOffers.contains(openOffer);
|
||||||
}
|
}
|
||||||
if (contained && openOffer.isAvailable()) {
|
if (contained) {
|
||||||
// TODO It is not clear yet if it is better for the node and the network to send out all add offer
|
// TODO It is not clear yet if it is better for the node and the network to send out all add offer
|
||||||
// messages in one go or to spread it over a delay. With power users who have 100-200 offers that can have
|
// messages in one go or to spread it over a delay. With power users who have 100-200 offers that can have
|
||||||
// some significant impact to user experience and the network
|
// some significant impact to user experience and the network
|
||||||
republishOffer(openOffer, () -> processListForRepublishOffers(list));
|
maybeRepublishOffer(openOffer, () -> processListForRepublishOffers(list));
|
||||||
|
|
||||||
/* republishOffer(openOffer,
|
/* republishOffer(openOffer,
|
||||||
() -> UserThread.runAfter(() -> processListForRepublishOffers(list),
|
() -> UserThread.runAfter(() -> processListForRepublishOffers(list),
|
||||||
@ -1730,13 +1733,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void republishOffer(OpenOffer openOffer) {
|
private void maybeRepublishOffer(OpenOffer openOffer) {
|
||||||
republishOffer(openOffer, null);
|
maybeRepublishOffer(openOffer, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void republishOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) {
|
private void maybeRepublishOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) {
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
|
|
||||||
|
// skip if prevented from publishing
|
||||||
|
if (preventedFromPublishing(openOffer)) {
|
||||||
|
if (completeHandler != null) completeHandler.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// determine if offer is valid
|
// determine if offer is valid
|
||||||
boolean isValid = true;
|
boolean isValid = true;
|
||||||
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner());
|
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(openOffer.getOffer().getOfferPayload().getArbitratorSigner());
|
||||||
@ -1747,7 +1756,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
log.warn("Offer {} has invalid arbitrator signature, reposting", openOffer.getId());
|
log.warn("Offer {} has invalid arbitrator signature, reposting", openOffer.getId());
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
if (openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null && (openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty())) {
|
if ((openOffer.getOffer().getOfferPayload().getReserveTxKeyImages() != null || openOffer.getOffer().getOfferPayload().getReserveTxKeyImages().isEmpty()) && (openOffer.getReserveTxHash() == null || openOffer.getReserveTxHash().isEmpty())) {
|
||||||
log.warn("Offer {} is missing reserve tx hash but has reserved key images, reposting", openOffer.getId());
|
log.warn("Offer {} is missing reserve tx hash but has reserved key images, reposting", openOffer.getId());
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
@ -1811,6 +1820,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
}, THREAD_ID);
|
}, THREAD_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean preventedFromPublishing(OpenOffer openOffer) {
|
||||||
|
return openOffer.isDeactivated() || openOffer.isCanceled();
|
||||||
|
}
|
||||||
|
|
||||||
private void startPeriodicRepublishOffersTimer() {
|
private void startPeriodicRepublishOffersTimer() {
|
||||||
stopped = false;
|
stopped = false;
|
||||||
if (periodicRepublishOffersTimer == null) {
|
if (periodicRepublishOffersTimer == null) {
|
||||||
@ -1843,8 +1856,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
final OpenOffer openOffer = openOffersList.get(i);
|
final OpenOffer openOffer = openOffersList.get(i);
|
||||||
UserThread.runAfterRandomDelay(() -> {
|
UserThread.runAfterRandomDelay(() -> {
|
||||||
// we need to check if in the meantime the offer has been removed
|
// we need to check if in the meantime the offer has been removed
|
||||||
if (openOffers.contains(openOffer) && openOffer.isAvailable())
|
boolean contained = false;
|
||||||
refreshOffer(openOffer, 0, 1);
|
synchronized (openOffers) {
|
||||||
|
contained = openOffers.contains(openOffer);
|
||||||
|
}
|
||||||
|
if (contained) maybeRefreshOffer(openOffer, 0, 1);
|
||||||
}, minDelay, maxDelay, TimeUnit.MILLISECONDS);
|
}, minDelay, maxDelay, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1857,13 +1873,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||||||
log.trace("periodicRefreshOffersTimer already stated");
|
log.trace("periodicRefreshOffersTimer already stated");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshOffer(OpenOffer openOffer, int numAttempts, int maxAttempts) {
|
private void maybeRefreshOffer(OpenOffer openOffer, int numAttempts, int maxAttempts) {
|
||||||
|
if (preventedFromPublishing(openOffer)) return;
|
||||||
offerBookService.refreshTTL(openOffer.getOffer().getOfferPayload(),
|
offerBookService.refreshTTL(openOffer.getOffer().getOfferPayload(),
|
||||||
() -> log.debug("Successful refreshed TTL for offer"),
|
() -> log.debug("Successful refreshed TTL for offer"),
|
||||||
(errorMessage) -> {
|
(errorMessage) -> {
|
||||||
log.warn(errorMessage);
|
log.warn(errorMessage);
|
||||||
if (numAttempts + 1 < maxAttempts) {
|
if (numAttempts + 1 < maxAttempts) {
|
||||||
UserThread.runAfter(() -> refreshOffer(openOffer, numAttempts + 1, maxAttempts), 10);
|
UserThread.runAfter(() -> maybeRefreshOffer(openOffer, numAttempts + 1, maxAttempts), 10);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -30,8 +30,8 @@ import haveno.core.offer.placeoffer.PlaceOfferModel;
|
|||||||
import haveno.core.trade.HavenoUtils;
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.trade.protocol.TradeProtocol;
|
import haveno.core.trade.protocol.TradeProtocol;
|
||||||
import haveno.core.xmr.model.XmrAddressEntry;
|
import haveno.core.xmr.model.XmrAddressEntry;
|
||||||
import haveno.core.xmr.wallet.XmrWalletService;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import monero.common.MoneroRpcConnection;
|
||||||
import monero.daemon.model.MoneroOutput;
|
import monero.daemon.model.MoneroOutput;
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
|
|
||||||
@ -59,11 +59,11 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// verify monero connection
|
// verify monero connection
|
||||||
model.getXmrWalletService().getConnectionService().verifyConnection();
|
model.getXmrWalletService().getXmrConnectionService().verifyConnection();
|
||||||
|
|
||||||
// create reserve tx
|
// create reserve tx
|
||||||
MoneroTxWallet reserveTx = null;
|
MoneroTxWallet reserveTx = null;
|
||||||
synchronized (XmrWalletService.WALLET_LOCK) {
|
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
|
||||||
|
|
||||||
// reset protocol timeout
|
// reset protocol timeout
|
||||||
verifyPending();
|
verifyPending();
|
||||||
@ -82,14 +82,16 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
|||||||
try {
|
try {
|
||||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||||
|
MoneroRpcConnection sourceConnection = model.getXmrWalletService().getXmrConnectionService().getConnection();
|
||||||
try {
|
try {
|
||||||
//if (true) throw new RuntimeException("Pretend error");
|
//if (true) throw new RuntimeException("Pretend error");
|
||||||
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
|
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, openOffer.getShortId(), e.getMessage());
|
log.warn("Error creating reserve tx, offerId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, openOffer.getShortId(), e.getMessage());
|
||||||
|
model.getXmrWalletService().handleWalletError(e, sourceConnection);
|
||||||
|
verifyPending();
|
||||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
model.getProtocol().startTimeoutTimer(); // reset protocol timeout
|
model.getProtocol().startTimeoutTimer(); // reset protocol timeout
|
||||||
if (model.getXmrWalletService().getConnectionService().isConnected()) model.getXmrWalletService().requestSwitchToNextBestConnection();
|
|
||||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,11 +104,8 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
|||||||
|
|
||||||
// reset state with wallet lock
|
// reset state with wallet lock
|
||||||
model.getXmrWalletService().resetAddressEntriesForOpenOffer(offer.getId());
|
model.getXmrWalletService().resetAddressEntriesForOpenOffer(offer.getId());
|
||||||
if (reserveTx != null) {
|
if (reserveTx != null) model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
|
||||||
model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
|
offer.getOfferPayload().setReserveTxKeyImages(null);
|
||||||
offer.getOfferPayload().setReserveTxKeyImages(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +131,11 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void verifyPending() {
|
private boolean isPending() {
|
||||||
if (!model.getOpenOffer().isPending()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled");
|
return model.getOpenOffer().isPending();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyPending() {
|
||||||
|
if (!isPending()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -186,24 +186,47 @@ public abstract class SupportManager {
|
|||||||
private void onAckMessage(AckMessage ackMessage) {
|
private void onAckMessage(AckMessage ackMessage) {
|
||||||
if (ackMessage.getSourceType() == getAckMessageSourceType()) {
|
if (ackMessage.getSourceType() == getAckMessageSourceType()) {
|
||||||
if (ackMessage.isSuccess()) {
|
if (ackMessage.isSuccess()) {
|
||||||
log.info("Received AckMessage for {} with tradeId {} and uid {}",
|
log.info("Received AckMessage for {} with tradeId {} and uid {}", ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
|
||||||
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
|
|
||||||
|
|
||||||
// ack message on chat message received when dispute is opened and closed
|
// ack message on chat message received when dispute is opened and closed
|
||||||
if (ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName())) {
|
if (ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName())) {
|
||||||
Trade trade = tradeManager.getTrade(ackMessage.getSourceId());
|
Trade trade = tradeManager.getTrade(ackMessage.getSourceId());
|
||||||
for (Dispute dispute : trade.getDisputes()) {
|
for (Dispute dispute : trade.getDisputes()) {
|
||||||
for (ChatMessage chatMessage : dispute.getChatMessages()) {
|
synchronized (dispute.getChatMessages()) {
|
||||||
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
|
for (ChatMessage chatMessage : dispute.getChatMessages()) {
|
||||||
if (dispute.isClosed()) trade.pollWalletNormallyForMs(30000); // sync to check for payout
|
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
|
||||||
else trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
if (trade.getDisputeState() == Trade.DisputeState.DISPUTE_REQUESTED) {
|
||||||
|
if (dispute.isClosed()) dispute.reOpen();
|
||||||
|
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
||||||
|
} else if (dispute.isClosed()) {
|
||||||
|
trade.pollWalletNormallyForMs(30000); // sync to check for payout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Received AckMessage with error state for {} with tradeId={}, sender={}, errorMessage={}",
|
||||||
|
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSenderNodeAddress(), ackMessage.getErrorMessage());
|
||||||
|
|
||||||
|
// nack message on chat message received when dispute closed message is nacked
|
||||||
|
if (ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName())) {
|
||||||
|
Trade trade = tradeManager.getTrade(ackMessage.getSourceId());
|
||||||
|
for (Dispute dispute : trade.getDisputes()) {
|
||||||
|
synchronized (dispute.getChatMessages()) {
|
||||||
|
for (ChatMessage chatMessage : dispute.getChatMessages()) {
|
||||||
|
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
|
||||||
|
if (trade.getDisputeState().isCloseRequested()) {
|
||||||
|
log.warn("DisputeCloseMessage was nacked. We close the dispute now. tradeId={}, nack sender={}", trade.getId(), ackMessage.getSenderNodeAddress());
|
||||||
|
dispute.setIsClosed();
|
||||||
|
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
log.warn("Received AckMessage with error state for {} with tradeId {} and errorMessage={}",
|
|
||||||
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllChatMessages(ackMessage.getSourceId()).stream()
|
getAllChatMessages(ackMessage.getSourceId()).stream()
|
||||||
|
@ -77,6 +77,10 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||||||
REOPENED,
|
REOPENED,
|
||||||
CLOSED;
|
CLOSED;
|
||||||
|
|
||||||
|
public boolean isOpen() {
|
||||||
|
return this == NEW || this == OPEN || this == REOPENED;
|
||||||
|
}
|
||||||
|
|
||||||
public static Dispute.State fromProto(protobuf.Dispute.State state) {
|
public static Dispute.State fromProto(protobuf.Dispute.State state) {
|
||||||
return ProtoUtil.enumFromProto(Dispute.State.class, state.name());
|
return ProtoUtil.enumFromProto(Dispute.State.class, state.name());
|
||||||
}
|
}
|
||||||
@ -349,10 +353,12 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public void addAndPersistChatMessage(ChatMessage chatMessage) {
|
public void addAndPersistChatMessage(ChatMessage chatMessage) {
|
||||||
if (!chatMessages.contains(chatMessage)) {
|
synchronized (chatMessages) {
|
||||||
chatMessages.add(chatMessage);
|
if (!chatMessages.contains(chatMessage)) {
|
||||||
} else {
|
chatMessages.add(chatMessage);
|
||||||
log.error("disputeDirectMessage already exists");
|
} else {
|
||||||
|
log.error("disputeDirectMessage already exists");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,13 +367,15 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean removeAllChatMessages() {
|
public boolean removeAllChatMessages() {
|
||||||
if (chatMessages.size() > 1) {
|
synchronized (chatMessages) {
|
||||||
// removes all chat except the initial guidelines message.
|
if (chatMessages.size() > 1) {
|
||||||
String firstMessageUid = chatMessages.get(0).getUid();
|
// removes all chat except the initial guidelines message.
|
||||||
chatMessages.removeIf((msg) -> !msg.getUid().equals(firstMessageUid));
|
String firstMessageUid = chatMessages.get(0).getUid();
|
||||||
return true;
|
chatMessages.removeIf((msg) -> !msg.getUid().equals(firstMessageUid));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void maybeClearSensitiveData() {
|
public void maybeClearSensitiveData() {
|
||||||
|
@ -90,12 +90,14 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
|
|||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public void cleanupDisputes(@Nullable Consumer<String> closedDisputeHandler) {
|
public void cleanupDisputes(@Nullable Consumer<String> closedDisputeHandler) {
|
||||||
disputeList.stream().forEach(dispute -> {
|
synchronized (disputeList.getObservableList()) {
|
||||||
String tradeId = dispute.getTradeId();
|
disputeList.stream().forEach(dispute -> {
|
||||||
if (dispute.isClosed() && closedDisputeHandler != null) {
|
String tradeId = dispute.getTradeId();
|
||||||
closedDisputeHandler.accept(tradeId);
|
if (dispute.isClosed() && closedDisputeHandler != null) {
|
||||||
}
|
closedDisputeHandler.accept(tradeId);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -130,7 +132,9 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
|
|||||||
}
|
}
|
||||||
|
|
||||||
ObservableList<Dispute> getObservableList() {
|
ObservableList<Dispute> getObservableList() {
|
||||||
return disputeList.getObservableList();
|
synchronized (disputeList.getObservableList()) {
|
||||||
|
return disputeList.getObservableList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -151,10 +155,12 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
|
|||||||
isAlerting -> {
|
isAlerting -> {
|
||||||
// We get the event before the list gets updated, so we execute on next frame
|
// We get the event before the list gets updated, so we execute on next frame
|
||||||
UserThread.execute(() -> {
|
UserThread.execute(() -> {
|
||||||
int numAlerts = (int) disputeList.getList().stream()
|
synchronized (disputeList.getObservableList()) {
|
||||||
.mapToLong(x -> x.getBadgeCountProperty().getValue())
|
int numAlerts = (int) disputeList.getList().stream()
|
||||||
.sum();
|
.mapToLong(x -> x.getBadgeCountProperty().getValue())
|
||||||
numOpenDisputes.set(numAlerts);
|
.sum();
|
||||||
|
numOpenDisputes.set(numAlerts);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
disputedTradeIds.add(dispute.getTradeId());
|
disputedTradeIds.add(dispute.getTradeId());
|
||||||
|
@ -157,6 +157,11 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
disputeListService.requestPersistence();
|
disputeListService.requestPersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void requestPersistence(Trade trade) {
|
||||||
|
trade.requestPersistence();
|
||||||
|
disputeListService.requestPersistence();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public NodeAddress getPeerNodeAddress(ChatMessage message) {
|
public NodeAddress getPeerNodeAddress(ChatMessage message) {
|
||||||
Optional<Dispute> disputeOptional = findDispute(message);
|
Optional<Dispute> disputeOptional = findDispute(message);
|
||||||
@ -182,10 +187,12 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ChatMessage> getAllChatMessages(String tradeId) {
|
public List<ChatMessage> getAllChatMessages(String tradeId) {
|
||||||
return getDisputeList().stream()
|
synchronized (getDisputeList().getObservableList()) {
|
||||||
.filter(dispute -> dispute.getTradeId().equals(tradeId))
|
return getDisputeList().stream()
|
||||||
.flatMap(dispute -> dispute.getChatMessages().stream())
|
.filter(dispute -> dispute.getTradeId().equals(tradeId))
|
||||||
.collect(Collectors.toList());
|
.flatMap(dispute -> dispute.getChatMessages().stream())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -234,7 +241,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ObservableList<Dispute> getDisputesAsObservableList() {
|
public ObservableList<Dispute> getDisputesAsObservableList() {
|
||||||
synchronized(disputeListService.getDisputeList()) {
|
synchronized(disputeListService.getDisputeList().getObservableList()) {
|
||||||
return disputeListService.getObservableList();
|
return disputeListService.getObservableList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -244,7 +251,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected T getDisputeList() {
|
protected T getDisputeList() {
|
||||||
synchronized(disputeListService.getDisputeList()) {
|
synchronized(disputeListService.getDisputeList().getObservableList()) {
|
||||||
return disputeListService.getDisputeList();
|
return disputeListService.getDisputeList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -321,15 +328,21 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
|
|
||||||
// trader sends message to arbitrator to open dispute
|
// trader sends message to arbitrator to open dispute
|
||||||
public void sendDisputeOpenedMessage(Dispute dispute,
|
public void sendDisputeOpenedMessage(Dispute dispute,
|
||||||
boolean reOpen,
|
|
||||||
String updatedMultisigHex,
|
|
||||||
ResultHandler resultHandler,
|
ResultHandler resultHandler,
|
||||||
FaultHandler faultHandler) {
|
FaultHandler faultHandler) {
|
||||||
|
|
||||||
// get trade
|
// get trade
|
||||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||||
if (trade == null) {
|
if (trade == null) {
|
||||||
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
String errorMsg = "Dispute trade does not exist, tradeId=" + dispute.getTradeId();
|
||||||
|
faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// arbitrator cannot open disputes
|
||||||
|
if (trade.isArbitrator()) {
|
||||||
|
String errorMsg = "Arbitrators cannot open disputes.";
|
||||||
|
faultHandler.handleFault(errorMsg, new IllegalStateException(errorMsg));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,7 +356,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized (disputeList) {
|
synchronized (disputeList.getObservableList()) {
|
||||||
if (disputeList.contains(dispute)) {
|
if (disputeList.contains(dispute)) {
|
||||||
String msg = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId();
|
String msg = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId();
|
||||||
log.warn(msg);
|
log.warn(msg);
|
||||||
@ -352,7 +365,16 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
}
|
}
|
||||||
|
|
||||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||||
|
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed();
|
||||||
if (!storedDisputeOptional.isPresent() || reOpen) {
|
if (!storedDisputeOptional.isPresent() || reOpen) {
|
||||||
|
|
||||||
|
// add or re-open dispute
|
||||||
|
if (reOpen) {
|
||||||
|
dispute = storedDisputeOptional.get();
|
||||||
|
} else {
|
||||||
|
disputeList.add(dispute);
|
||||||
|
}
|
||||||
|
|
||||||
String disputeInfo = getDisputeInfo(dispute);
|
String disputeInfo = getDisputeInfo(dispute);
|
||||||
String sysMsg = dispute.isSupportTicket() ?
|
String sysMsg = dispute.isSupportTicket() ?
|
||||||
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) :
|
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) :
|
||||||
@ -367,17 +389,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
p2PService.getAddress());
|
p2PService.getAddress());
|
||||||
chatMessage.setSystemMessage(true);
|
chatMessage.setSystemMessage(true);
|
||||||
dispute.addAndPersistChatMessage(chatMessage);
|
dispute.addAndPersistChatMessage(chatMessage);
|
||||||
if (!reOpen) {
|
|
||||||
disputeList.add(dispute);
|
|
||||||
}
|
|
||||||
|
|
||||||
// create dispute opened message
|
// create dispute opened message
|
||||||
|
trade.exportMultisigHex();
|
||||||
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
|
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
|
||||||
DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
|
DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
|
||||||
p2PService.getAddress(),
|
p2PService.getAddress(),
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
getSupportType(),
|
getSupportType(),
|
||||||
updatedMultisigHex,
|
trade.getSelf().getUpdatedMultisigHex(),
|
||||||
trade.getArbitrator().getPaymentSentMessage());
|
trade.getArbitrator().getPaymentSentMessage());
|
||||||
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||||
"chatMessage.uid={}",
|
"chatMessage.uid={}",
|
||||||
@ -387,6 +407,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName());
|
recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName());
|
||||||
|
|
||||||
// send dispute opened message
|
// send dispute opened message
|
||||||
|
trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
|
||||||
mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress,
|
mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress,
|
||||||
dispute.getAgentPubKeyRing(),
|
dispute.getAgentPubKeyRing(),
|
||||||
disputeOpenedMessage,
|
disputeOpenedMessage,
|
||||||
@ -420,7 +441,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||||
// the state, as that is displayed to the user and we only persist that msg
|
// the state, as that is displayed to the user and we only persist that msg
|
||||||
chatMessage.setStoredInMailbox(true);
|
chatMessage.setStoredInMailbox(true);
|
||||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
|
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
resultHandler.handleResult();
|
resultHandler.handleResult();
|
||||||
}
|
}
|
||||||
@ -437,6 +457,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||||
// the state, as that is displayed to the user and we only persist that msg
|
// the state, as that is displayed to the user and we only persist that msg
|
||||||
chatMessage.setSendMessageError(errorMessage);
|
chatMessage.setSendMessageError(errorMessage);
|
||||||
|
trade.setDisputeState(Trade.DisputeState.NO_DISPUTE);
|
||||||
requestPersistence();
|
requestPersistence();
|
||||||
faultHandler.handleFault("Sending dispute message failed: " +
|
faultHandler.handleFault("Sending dispute message failed: " +
|
||||||
errorMessage, new DisputeMessageDeliveryFailedException());
|
errorMessage, new DisputeMessageDeliveryFailedException());
|
||||||
@ -455,18 +476,32 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
|
|
||||||
// arbitrator receives dispute opened message from opener, opener's peer receives from arbitrator
|
// arbitrator receives dispute opened message from opener, opener's peer receives from arbitrator
|
||||||
protected void handleDisputeOpenedMessage(DisputeOpenedMessage message) {
|
protected void handleDisputeOpenedMessage(DisputeOpenedMessage message) {
|
||||||
Dispute dispute = message.getDispute();
|
Dispute msgDispute = message.getDispute();
|
||||||
log.info("Processing {} with trade {}, dispute {}", message.getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
|
log.info("Processing {} with trade {}, dispute {}", message.getClass().getSimpleName(), msgDispute.getTradeId(), msgDispute.getId());
|
||||||
|
|
||||||
// get trade
|
// get trade
|
||||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
Trade trade = tradeManager.getTrade(msgDispute.getTradeId());
|
||||||
if (trade == null) {
|
if (trade == null) {
|
||||||
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
log.warn("Dispute trade {} does not exist", msgDispute.getTradeId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (trade.isPayoutPublished()) {
|
||||||
|
log.warn("Dispute trade {} payout already published", msgDispute.getTradeId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// find existing dispute
|
||||||
|
Optional<Dispute> storedDisputeOptional = findDispute(msgDispute);
|
||||||
|
|
||||||
|
// determine if re-opening dispute
|
||||||
|
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed();
|
||||||
|
|
||||||
|
// use existing dispute or create new
|
||||||
|
Dispute dispute = reOpen ? storedDisputeOptional.get() : msgDispute;
|
||||||
|
|
||||||
|
// process on trade thread
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
String errorMessage = null;
|
String errorMessage = null;
|
||||||
PubKeyRing senderPubKeyRing = null;
|
PubKeyRing senderPubKeyRing = null;
|
||||||
try {
|
try {
|
||||||
@ -503,14 +538,20 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get sender
|
// get sender
|
||||||
senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
|
TradePeer sender;
|
||||||
TradePeer sender = trade.getTradePeer(senderPubKeyRing);
|
if (reOpen) { // re-open can come from either peer
|
||||||
|
sender = trade.isArbitrator() ? trade.getTradePeer(message.getSenderNodeAddress()) : trade.getArbitrator();
|
||||||
|
senderPubKeyRing = sender.getPubKeyRing();
|
||||||
|
} else {
|
||||||
|
senderPubKeyRing = trade.isArbitrator() ? (dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing()) : trade.getArbitrator().getPubKeyRing();
|
||||||
|
sender = trade.getTradePeer(senderPubKeyRing);
|
||||||
|
}
|
||||||
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
|
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
|
||||||
|
|
||||||
// update sender node address
|
// update sender node address
|
||||||
sender.setNodeAddress(message.getSenderNodeAddress());
|
sender.setNodeAddress(message.getSenderNodeAddress());
|
||||||
|
|
||||||
// message to trader is expected from arbitrator
|
// verify message to trader is expected from arbitrator
|
||||||
if (!trade.isArbitrator() && sender != trade.getArbitrator()) {
|
if (!trade.isArbitrator() && sender != trade.getArbitrator()) {
|
||||||
throw new RuntimeException(message.getClass().getSimpleName() + " to trader is expected only from arbitrator");
|
throw new RuntimeException(message.getClass().getSimpleName() + " to trader is expected only from arbitrator");
|
||||||
}
|
}
|
||||||
@ -528,16 +569,29 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
// add chat message with price info
|
// add chat message with price info
|
||||||
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
|
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
|
||||||
|
|
||||||
// add dispute
|
// add or re-open dispute
|
||||||
synchronized (disputeList) {
|
synchronized (disputeList) {
|
||||||
if (!disputeList.contains(dispute)) {
|
if (!disputeList.contains(msgDispute)) {
|
||||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
if (!storedDisputeOptional.isPresent() || reOpen) {
|
||||||
if (!storedDisputeOptional.isPresent()) {
|
|
||||||
disputeList.add(dispute);
|
|
||||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
|
||||||
|
|
||||||
// send dispute opened message to peer if arbitrator
|
// update trade state
|
||||||
if (trade.isArbitrator()) sendDisputeOpenedMessageToPeer(dispute, contract, dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(), trade.getSelf().getUpdatedMultisigHex());
|
if (reOpen) {
|
||||||
|
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
||||||
|
} else {
|
||||||
|
disputeList.add(dispute);
|
||||||
|
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset buyer and seller unsigned payout tx hex
|
||||||
|
trade.getBuyer().setUnsignedPayoutTxHex(null);
|
||||||
|
trade.getSeller().setUnsignedPayoutTxHex(null);
|
||||||
|
|
||||||
|
// send dispute opened message to other peer if arbitrator
|
||||||
|
if (trade.isArbitrator()) {
|
||||||
|
TradePeer senderPeer = sender == trade.getMaker() ? trade.getTaker() : trade.getMaker();
|
||||||
|
if (senderPeer != trade.getMaker() && senderPeer != trade.getTaker()) throw new RuntimeException("Sender peer is not maker or taker, address=" + senderPeer.getNodeAddress());
|
||||||
|
sendDisputeOpenedMessageToPeer(dispute, contract, senderPeer.getPubKeyRing(), trade.getSelf().getUpdatedMultisigHex());
|
||||||
|
}
|
||||||
tradeManager.requestPersistence();
|
tradeManager.requestPersistence();
|
||||||
errorMessage = null;
|
errorMessage = null;
|
||||||
} else {
|
} else {
|
||||||
@ -548,7 +602,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
// add chat message with mediation info if applicable
|
// add chat message with mediation info if applicable
|
||||||
addMediationResultMessage(dispute);
|
addMediationResultMessage(dispute);
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException("We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId());
|
throw new RuntimeException("We got a dispute msg that we have already stored. TradeId = " + msgDispute.getTradeId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -561,7 +615,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
// use chat message instead of open dispute message for the ack
|
// use chat message instead of open dispute message for the ack
|
||||||
ObservableList<ChatMessage> messages = message.getDispute().getChatMessages();
|
ObservableList<ChatMessage> messages = message.getDispute().getChatMessages();
|
||||||
if (!messages.isEmpty()) {
|
if (!messages.isEmpty()) {
|
||||||
ChatMessage msg = messages.get(0);
|
ChatMessage msg = messages.get(messages.size() - 1); // send ack to sender of last chat message
|
||||||
sendAckMessage(msg, senderPubKeyRing, errorMessage == null, errorMessage);
|
sendAckMessage(msg, senderPubKeyRing, errorMessage == null, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -575,7 +629,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
Contract contractFromOpener,
|
Contract contractFromOpener,
|
||||||
PubKeyRing pubKeyRing,
|
PubKeyRing pubKeyRing,
|
||||||
String updatedMultisigHex) {
|
String updatedMultisigHex) {
|
||||||
log.info("{}.sendPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), disputeFromOpener.getTradeId(), disputeFromOpener.getId());
|
log.info("{} sendPeerOpenedDisputeMessage() with trade {}, dispute {}", getClass().getSimpleName(), disputeFromOpener.getTradeId(), disputeFromOpener.getId());
|
||||||
// We delay a bit for sending the message to the peer to allow that a openDispute message from the peer is
|
// We delay a bit for sending the message to the peer to allow that a openDispute message from the peer is
|
||||||
// being used as the valid msg. If dispute agent was offline and both peer requested we want to see the correct
|
// being used as the valid msg. If dispute agent was offline and both peer requested we want to see the correct
|
||||||
// message and not skip the system message of the peer as it would be the case if we have created the system msg
|
// message and not skip the system message of the peer as it would be the case if we have created the system msg
|
||||||
@ -597,6 +651,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create mirrored dispute
|
||||||
Dispute dispute = new Dispute(new Date().getTime(),
|
Dispute dispute = new Dispute(new Date().getTime(),
|
||||||
disputeFromOpener.getTradeId(),
|
disputeFromOpener.getTradeId(),
|
||||||
pubKeyRing.hashCode(),
|
pubKeyRing.hashCode(),
|
||||||
@ -622,10 +677,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
|
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
|
||||||
dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx());
|
dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx());
|
||||||
|
|
||||||
|
// skip if dispute already open
|
||||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||||
|
if (storedDisputeOptional.isPresent() && !storedDisputeOptional.get().isClosed()) {
|
||||||
// Valid case if both have opened a dispute and agent was not online.
|
|
||||||
if (storedDisputeOptional.isPresent()) {
|
|
||||||
log.info("We got a dispute already open for that trade and trading peer. TradeId = {}", dispute.getTradeId());
|
log.info("We got a dispute already open for that trade and trading peer. TradeId = {}", dispute.getTradeId());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -647,8 +701,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
|
|
||||||
addPriceInfoMessage(dispute, 0);
|
addPriceInfoMessage(dispute, 0);
|
||||||
|
|
||||||
synchronized (disputeList) {
|
// add or re-open dispute
|
||||||
disputeList.add(dispute);
|
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed();
|
||||||
|
if (reOpen) {
|
||||||
|
dispute = storedDisputeOptional.get();
|
||||||
|
dispute.reOpen();
|
||||||
|
} else {
|
||||||
|
synchronized (disputeList) {
|
||||||
|
disputeList.add(dispute);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// get trade
|
// get trade
|
||||||
@ -658,10 +719,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// We mirrored dispute already!
|
// create dispute opened message with peer dispute
|
||||||
Contract contract = dispute.getContract();
|
TradePeer peer = trade.getTradePeer(pubKeyRing);
|
||||||
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
|
PubKeyRing peersPubKeyRing = peer.getPubKeyRing();
|
||||||
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress();
|
NodeAddress peersNodeAddress = peer.getNodeAddress();
|
||||||
DisputeOpenedMessage peerOpenedDisputeMessage = new DisputeOpenedMessage(dispute,
|
DisputeOpenedMessage peerOpenedDisputeMessage = new DisputeOpenedMessage(dispute,
|
||||||
p2PService.getAddress(),
|
p2PService.getAddress(),
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
@ -749,7 +810,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
dispute.addAndPersistChatMessage(chatMessage);
|
dispute.addAndPersistChatMessage(chatMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// create dispute payout tx once per trader if we have their updated multisig hex
|
// create dispute payout tx
|
||||||
TradePeer receiver = trade.getTradePeer(dispute.getTraderPubKeyRing());
|
TradePeer receiver = trade.getTradePeer(dispute.getTraderPubKeyRing());
|
||||||
if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null && receiver.getUnsignedPayoutTxHex() == null) {
|
if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null && receiver.getUnsignedPayoutTxHex() == null) {
|
||||||
createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true);
|
createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true);
|
||||||
@ -792,7 +853,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
disputeResult.getChatMessage().setArrived(true);
|
disputeResult.getChatMessage().setArrived(true);
|
||||||
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG);
|
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG);
|
||||||
trade.pollWalletNormallyForMs(30000);
|
trade.pollWalletNormallyForMs(30000);
|
||||||
requestPersistence();
|
requestPersistence(trade);
|
||||||
resultHandler.handleResult();
|
resultHandler.handleResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -811,7 +872,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
disputeResult.getChatMessage().setStoredInMailbox(true);
|
disputeResult.getChatMessage().setStoredInMailbox(true);
|
||||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||||
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG);
|
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG);
|
||||||
requestPersistence();
|
requestPersistence(trade);
|
||||||
resultHandler.handleResult();
|
resultHandler.handleResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -828,13 +889,13 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
// the state, as that is displayed to the user and we only persist that msg
|
// the state, as that is displayed to the user and we only persist that msg
|
||||||
disputeResult.getChatMessage().setSendMessageError(errorMessage);
|
disputeResult.getChatMessage().setSendMessageError(errorMessage);
|
||||||
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG);
|
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG);
|
||||||
requestPersistence();
|
requestPersistence(trade);
|
||||||
faultHandler.handleFault(errorMessage, new RuntimeException(errorMessage));
|
faultHandler.handleFault(errorMessage, new RuntimeException(errorMessage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG);
|
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG);
|
||||||
requestPersistence();
|
requestPersistence(trade);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
faultHandler.handleFault(e.getMessage(), e);
|
faultHandler.handleFault(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
@ -900,11 +961,11 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
// update trade state
|
// update trade state
|
||||||
if (updateState) {
|
if (updateState) {
|
||||||
trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
|
trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
|
||||||
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());
|
|
||||||
trade.updatePayout(payoutTx);
|
trade.updatePayout(payoutTx);
|
||||||
if (trade.getBuyer().getUpdatedMultisigHex() != null && trade.getBuyer().getUnsignedPayoutTxHex() == null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
if (trade.getBuyer().getUpdatedMultisigHex() != null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||||
if (trade.getSeller().getUpdatedMultisigHex() != null && trade.getSeller().getUnsignedPayoutTxHex() == null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
if (trade.getSeller().getUpdatedMultisigHex() != null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||||
}
|
}
|
||||||
|
trade.requestPersistence();
|
||||||
return payoutTx;
|
return payoutTx;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
trade.syncAndPollWallet();
|
trade.syncAndPollWallet();
|
||||||
@ -937,21 +998,21 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||||||
return keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing());
|
return keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<Dispute> findDispute(Dispute dispute) {
|
public Optional<Dispute> findDispute(Dispute dispute) {
|
||||||
return findDispute(dispute.getTradeId(), dispute.getTraderId());
|
return findDispute(dispute.getTradeId(), dispute.getTraderId());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Optional<Dispute> findDispute(DisputeResult disputeResult) {
|
public Optional<Dispute> findDispute(DisputeResult disputeResult) {
|
||||||
ChatMessage chatMessage = disputeResult.getChatMessage();
|
ChatMessage chatMessage = disputeResult.getChatMessage();
|
||||||
checkNotNull(chatMessage, "chatMessage must not be null");
|
checkNotNull(chatMessage, "chatMessage must not be null");
|
||||||
return findDispute(disputeResult.getTradeId(), disputeResult.getTraderId());
|
return findDispute(disputeResult.getTradeId(), disputeResult.getTraderId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Optional<Dispute> findDispute(ChatMessage message) {
|
public Optional<Dispute> findDispute(ChatMessage message) {
|
||||||
return findDispute(message.getTradeId(), message.getTraderId());
|
return findDispute(message.getTradeId(), message.getTraderId());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Optional<Dispute> findDispute(String tradeId, int traderId) {
|
public Optional<Dispute> findDispute(String tradeId, int traderId) {
|
||||||
T disputeList = getDisputeList();
|
T disputeList = getDisputeList();
|
||||||
if (disputeList == null) {
|
if (disputeList == null) {
|
||||||
log.warn("disputes is null");
|
log.warn("disputes is null");
|
||||||
|
@ -78,6 +78,7 @@ import haveno.network.p2p.P2PService;
|
|||||||
import haveno.network.p2p.network.Connection;
|
import haveno.network.p2p.network.Connection;
|
||||||
import haveno.network.p2p.network.MessageListener;
|
import haveno.network.p2p.network.MessageListener;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import monero.common.MoneroRpcConnection;
|
||||||
import monero.wallet.MoneroWallet;
|
import monero.wallet.MoneroWallet;
|
||||||
import monero.wallet.model.MoneroDestination;
|
import monero.wallet.model.MoneroDestination;
|
||||||
import monero.wallet.model.MoneroMultisigSignResult;
|
import monero.wallet.model.MoneroMultisigSignResult;
|
||||||
@ -87,9 +88,11 @@ import monero.wallet.model.MoneroTxWallet;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
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 static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
@ -168,7 +171,28 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cleanupDisputes() {
|
public void cleanupDisputes() {
|
||||||
// no action
|
|
||||||
|
// remove disputes opened by arbitrator, which is not allowed
|
||||||
|
Set<Dispute> toRemoves = new HashSet<>();
|
||||||
|
List<Dispute> disputes = getDisputeList().getList();
|
||||||
|
for (Dispute dispute : disputes) {
|
||||||
|
|
||||||
|
// get dispute's trade
|
||||||
|
final Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||||
|
if (trade == null) {
|
||||||
|
log.warn("Dispute trade {} does not exist", dispute.getTradeId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect dispute if owned by arbitrator
|
||||||
|
if (dispute.getTraderPubKeyRing().equals(trade.getArbitrator().getPubKeyRing())) {
|
||||||
|
toRemoves.add(dispute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Dispute toRemove : toRemoves) {
|
||||||
|
log.warn("Removing invalid dispute opened by arbitrator, disputeId={}", toRemove.getTradeId(), toRemove.getId());
|
||||||
|
getDisputeList().remove(toRemove);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -216,7 +240,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
ChatMessage chatMessage = null;
|
ChatMessage chatMessage = null;
|
||||||
Dispute dispute = null;
|
Dispute dispute = null;
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
try {
|
try {
|
||||||
DisputeResult disputeResult = disputeClosedMessage.getDisputeResult();
|
DisputeResult disputeResult = disputeClosedMessage.getDisputeResult();
|
||||||
chatMessage = disputeResult.getChatMessage();
|
chatMessage = disputeResult.getChatMessage();
|
||||||
@ -252,7 +276,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
|
|
||||||
// save dispute closed message for reprocessing
|
// save dispute closed message for reprocessing
|
||||||
trade.getArbitrator().setDisputeClosedMessage(disputeClosedMessage);
|
trade.getArbitrator().setDisputeClosedMessage(disputeClosedMessage);
|
||||||
requestPersistence();
|
requestPersistence(trade);
|
||||||
|
|
||||||
// verify arbitrator does not receive DisputeClosedMessage
|
// verify arbitrator does not receive DisputeClosedMessage
|
||||||
if (keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing())) {
|
if (keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing())) {
|
||||||
@ -263,10 +287,12 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
|
|
||||||
// set dispute state
|
// set dispute state
|
||||||
cleanupRetryMap(uid);
|
cleanupRetryMap(uid);
|
||||||
if (!dispute.getChatMessages().contains(chatMessage)) {
|
synchronized (dispute.getChatMessages()) {
|
||||||
dispute.addAndPersistChatMessage(chatMessage);
|
if (!dispute.getChatMessages().contains(chatMessage)) {
|
||||||
} else {
|
dispute.addAndPersistChatMessage(chatMessage);
|
||||||
log.warn("We got a dispute mail msg that we have already stored. TradeId = " + chatMessage.getTradeId());
|
} else {
|
||||||
|
log.warn("We got a dispute mail msg that we have already stored. TradeId = " + chatMessage.getTradeId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
dispute.setIsClosed();
|
dispute.setIsClosed();
|
||||||
if (dispute.disputeResultProperty().get() != null) {
|
if (dispute.disputeResultProperty().get() != null) {
|
||||||
@ -308,7 +334,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
if (trade.isPayoutPublished()) {
|
if (trade.isPayoutPublished()) {
|
||||||
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||||
} else {
|
} else {
|
||||||
if (e instanceof IllegalArgumentException) throw e;
|
if (e instanceof IllegalArgumentException || e instanceof IllegalStateException) throw e;
|
||||||
else throw new RuntimeException("Failed to sign and publish dispute payout tx from arbitrator for " + trade.getClass().getSimpleName() + " " + tradeId + ": " + e.getMessage(), e);
|
else throw new RuntimeException("Failed to sign and publish dispute payout tx from arbitrator for " + trade.getClass().getSimpleName() + " " + tradeId + ": " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -326,17 +352,21 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
// We use the chatMessage as we only persist those not the DisputeClosedMessage.
|
// We use the chatMessage as we only persist those not the DisputeClosedMessage.
|
||||||
// If we would use the DisputeClosedMessage we could not lookup for the msg when we receive the AckMessage.
|
// If we would use the DisputeClosedMessage we could not lookup for the msg when we receive the AckMessage.
|
||||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
|
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
|
||||||
requestPersistence();
|
requestPersistence(trade);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Error processing dispute closed message: " + e.getMessage());
|
log.warn("Error processing dispute closed message: {}", e.getMessage());
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
requestPersistence();
|
requestPersistence(trade);
|
||||||
|
|
||||||
// nack bad message and do not reprocess
|
// nack bad message and do not reprocess
|
||||||
if (e instanceof IllegalArgumentException) {
|
if (e instanceof IllegalArgumentException || e instanceof IllegalStateException) {
|
||||||
trade.getArbitrator().setDisputeClosedMessage(null); // message is processed
|
trade.getArbitrator().setDisputeClosedMessage(null); // message is processed
|
||||||
|
trade.setDisputeState(Trade.DisputeState.DISPUTE_CLOSED);
|
||||||
|
String warningMsg = "Error processing dispute closed message: " + e.getMessage() + "\n\nOpen another dispute to try again (ctrl+o).";
|
||||||
|
trade.prependErrorMessage(warningMsg);
|
||||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), false, e.getMessage());
|
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), false, e.getMessage());
|
||||||
requestPersistence();
|
HavenoUtils.havenoSetup.getTopErrorMsg().set(warningMsg);
|
||||||
|
requestPersistence(trade);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,7 +386,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
public void maybeReprocessDisputeClosedMessage(Trade trade, boolean reprocessOnError) {
|
public void maybeReprocessDisputeClosedMessage(Trade trade, boolean reprocessOnError) {
|
||||||
if (trade.isShutDownStarted()) return;
|
if (trade.isShutDownStarted()) return;
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
|
|
||||||
// skip if no need to reprocess
|
// skip if no need to reprocess
|
||||||
if (trade.isArbitrator() || trade.getArbitrator().getDisputeClosedMessage() == null || trade.getArbitrator().getDisputeClosedMessage().getUnsignedPayoutTxHex() == null || trade.getDisputeState().ordinal() >= Trade.DisputeState.DISPUTE_CLOSED.ordinal()) {
|
if (trade.isArbitrator() || trade.getArbitrator().getDisputeClosedMessage() == null || trade.getArbitrator().getDisputeClosedMessage().getUnsignedPayoutTxHex() == null || trade.getDisputeState().ordinal() >= Trade.DisputeState.DISPUTE_CLOSED.ordinal()) {
|
||||||
@ -442,12 +472,16 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
|
|
||||||
// sign arbitrator-signed payout tx
|
// sign arbitrator-signed payout tx
|
||||||
if (trade.getPayoutTxHex() == null) {
|
if (trade.getPayoutTxHex() == null) {
|
||||||
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex);
|
try {
|
||||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex);
|
||||||
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
||||||
disputeTxSet.setMultisigTxHex(signedMultisigTxHex);
|
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
||||||
trade.setPayoutTxHex(signedMultisigTxHex);
|
disputeTxSet.setMultisigTxHex(signedMultisigTxHex);
|
||||||
requestPersistence();
|
trade.setPayoutTxHex(signedMultisigTxHex);
|
||||||
|
requestPersistence(trade);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// verify mining fee is within tolerance by recreating payout tx
|
// verify mining 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?
|
||||||
@ -470,6 +504,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
|
|
||||||
// submit fully signed payout tx to the network
|
// submit fully signed payout tx to the network
|
||||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||||
|
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||||
try {
|
try {
|
||||||
List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex());
|
List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex());
|
||||||
disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
|
disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
|
||||||
@ -478,7 +513,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
if (trade.isPayoutPublished()) throw new IllegalStateException("Payout tx already published for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
if (trade.isPayoutPublished()) throw new IllegalStateException("Payout tx already published for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
||||||
log.warn("Failed to submit dispute payout tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
log.warn("Failed to submit dispute payout tx, tradeId={}, attempt={}/{}, error={}", trade.getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
||||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
if (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection();
|
if (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection(sourceConnection);
|
||||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -487,6 +522,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||||||
trade.updatePayout(disputeTxSet.getTxs().get(0));
|
trade.updatePayout(disputeTxSet.getTxs().get(0));
|
||||||
trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
|
trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
|
||||||
dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash());
|
dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash());
|
||||||
|
requestPersistence(trade);
|
||||||
return disputeTxSet;
|
return disputeTxSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +28,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.api.XmrConnectionService;
|
||||||
import haveno.core.app.HavenoSetup;
|
import haveno.core.app.HavenoSetup;
|
||||||
import haveno.core.offer.OfferPayload;
|
import haveno.core.offer.OfferPayload;
|
||||||
import haveno.core.offer.OpenOfferManager;
|
import haveno.core.offer.OpenOfferManager;
|
||||||
@ -106,6 +107,7 @@ public class HavenoUtils {
|
|||||||
public static HavenoSetup havenoSetup;
|
public static HavenoSetup havenoSetup;
|
||||||
public static ArbitrationManager arbitrationManager;
|
public static ArbitrationManager arbitrationManager;
|
||||||
public static XmrWalletService xmrWalletService;
|
public static XmrWalletService xmrWalletService;
|
||||||
|
public static XmrConnectionService xmrConnectionService;
|
||||||
public static OpenOfferManager openOfferManager;
|
public static OpenOfferManager openOfferManager;
|
||||||
|
|
||||||
public static boolean isSeedNode() {
|
public static boolean isSeedNode() {
|
||||||
@ -502,4 +504,20 @@ public class HavenoUtils {
|
|||||||
else if (Config.baseCurrencyNetwork().isStagenet()) return 38081;
|
else if (Config.baseCurrencyNetwork().isStagenet()) return 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setTopError(String msg) {
|
||||||
|
havenoSetup.getTopErrorMsg().set(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isConnectionRefused(Exception e) {
|
||||||
|
return e != null && e.getMessage().contains("Connection refused");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isReadTimeout(Exception e) {
|
||||||
|
return e != null && e.getMessage().contains("Read timed out");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isUnresponsive(Exception e) {
|
||||||
|
return isConnectionRefused(e) || isReadTimeout(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,6 @@ import haveno.common.crypto.PubKeyRing;
|
|||||||
import haveno.common.proto.ProtoUtil;
|
import haveno.common.proto.ProtoUtil;
|
||||||
import haveno.common.taskrunner.Model;
|
import haveno.common.taskrunner.Model;
|
||||||
import haveno.common.util.Utilities;
|
import haveno.common.util.Utilities;
|
||||||
import haveno.core.api.XmrConnectionService;
|
|
||||||
import haveno.core.monetary.Price;
|
import haveno.core.monetary.Price;
|
||||||
import haveno.core.monetary.Volume;
|
import haveno.core.monetary.Volume;
|
||||||
import haveno.core.network.MessageState;
|
import haveno.core.network.MessageState;
|
||||||
@ -69,6 +68,7 @@ import haveno.core.trade.protocol.TradeProtocol;
|
|||||||
import haveno.core.trade.statistics.TradeStatistics3;
|
import haveno.core.trade.statistics.TradeStatistics3;
|
||||||
import haveno.core.util.VolumeUtil;
|
import haveno.core.util.VolumeUtil;
|
||||||
import haveno.core.xmr.model.XmrAddressEntry;
|
import haveno.core.xmr.model.XmrAddressEntry;
|
||||||
|
import haveno.core.xmr.wallet.XmrWalletBase;
|
||||||
import haveno.core.xmr.wallet.XmrWalletService;
|
import haveno.core.xmr.wallet.XmrWalletService;
|
||||||
import haveno.network.p2p.AckMessage;
|
import haveno.network.p2p.AckMessage;
|
||||||
import haveno.network.p2p.NodeAddress;
|
import haveno.network.p2p.NodeAddress;
|
||||||
@ -76,14 +76,12 @@ import haveno.network.p2p.P2PService;
|
|||||||
import haveno.network.p2p.network.TorNetworkNode;
|
import haveno.network.p2p.network.TorNetworkNode;
|
||||||
import javafx.beans.property.DoubleProperty;
|
import javafx.beans.property.DoubleProperty;
|
||||||
import javafx.beans.property.IntegerProperty;
|
import javafx.beans.property.IntegerProperty;
|
||||||
import javafx.beans.property.LongProperty;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyStringProperty;
|
import javafx.beans.property.ReadOnlyStringProperty;
|
||||||
import javafx.beans.property.SimpleDoubleProperty;
|
import javafx.beans.property.SimpleDoubleProperty;
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
import javafx.beans.property.SimpleLongProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.property.StringProperty;
|
import javafx.beans.property.StringProperty;
|
||||||
@ -135,19 +133,18 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
|||||||
* stored in the task model.
|
* stored in the task model.
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public abstract class Trade implements Tradable, Model {
|
public abstract class Trade extends XmrWalletBase implements Tradable, Model {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public final Object lock = new Object();
|
||||||
private static final String MONERO_TRADE_WALLET_PREFIX = "xmr_trade_";
|
private static final String MONERO_TRADE_WALLET_PREFIX = "xmr_trade_";
|
||||||
private static final long SHUTDOWN_TIMEOUT_MS = 60000;
|
private static final long SHUTDOWN_TIMEOUT_MS = 60000;
|
||||||
private static final long SYNC_EVERY_NUM_BLOCKS = 360; // ~1/2 day
|
private static final long SYNC_EVERY_NUM_BLOCKS = 360; // ~1/2 day
|
||||||
private static final long DELETE_AFTER_NUM_BLOCKS = 2; // if deposit requested but not published
|
private static final long DELETE_AFTER_NUM_BLOCKS = 2; // if deposit requested but not published
|
||||||
private static final long EXTENDED_RPC_TIMEOUT = 600000; // 10 minutes
|
private static final long EXTENDED_RPC_TIMEOUT = 600000; // 10 minutes
|
||||||
private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS;
|
private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS;
|
||||||
private final Object walletLock = new Object();
|
protected final Object pollLock = new Object();
|
||||||
private final Object pollLock = new Object();
|
protected static final Object importMultisigLock = new Object();
|
||||||
private final LongProperty walletHeight = new SimpleLongProperty(0);
|
|
||||||
private MoneroWallet wallet;
|
|
||||||
private boolean wasWalletSynced;
|
|
||||||
private boolean pollInProgress;
|
private boolean pollInProgress;
|
||||||
private boolean restartInProgress;
|
private boolean restartInProgress;
|
||||||
private Subscription protocolErrorStateSubscription;
|
private Subscription protocolErrorStateSubscription;
|
||||||
@ -321,7 +318,11 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isOpen() {
|
public boolean isOpen() {
|
||||||
return this == DisputeState.DISPUTE_OPENED;
|
return isRequested() && !isClosed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCloseRequested() {
|
||||||
|
return this.ordinal() >= DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG.ordinal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isClosed() {
|
public boolean isClosed() {
|
||||||
@ -409,9 +410,6 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
// Immutable
|
// Immutable
|
||||||
@Getter
|
@Getter
|
||||||
transient final private XmrWalletService xmrWalletService;
|
transient final private XmrWalletService xmrWalletService;
|
||||||
@Getter
|
|
||||||
transient final private XmrConnectionService xmrConnectionService;
|
|
||||||
|
|
||||||
transient final private DoubleProperty initProgressProperty = new SimpleDoubleProperty(0.0);
|
transient final private DoubleProperty initProgressProperty = new SimpleDoubleProperty(0.0);
|
||||||
transient final private ObjectProperty<State> stateProperty = new SimpleObjectProperty<>(state);
|
transient final private ObjectProperty<State> stateProperty = new SimpleObjectProperty<>(state);
|
||||||
transient final private ObjectProperty<Phase> phaseProperty = new SimpleObjectProperty<>(state.phase);
|
transient final private ObjectProperty<Phase> phaseProperty = new SimpleObjectProperty<>(state.phase);
|
||||||
@ -437,10 +435,6 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
@Getter
|
@Getter
|
||||||
transient private boolean isInitialized;
|
transient private boolean isInitialized;
|
||||||
transient private boolean isFullyInitialized;
|
transient private boolean isFullyInitialized;
|
||||||
@Getter
|
|
||||||
transient private boolean isShutDownStarted;
|
|
||||||
@Getter
|
|
||||||
transient private boolean isShutDown;
|
|
||||||
|
|
||||||
// Added in v1.2.0
|
// Added in v1.2.0
|
||||||
transient private ObjectProperty<BigInteger> tradeAmountProperty;
|
transient private ObjectProperty<BigInteger> tradeAmountProperty;
|
||||||
@ -507,11 +501,12 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
@Nullable NodeAddress makerNodeAddress,
|
@Nullable NodeAddress makerNodeAddress,
|
||||||
@Nullable NodeAddress takerNodeAddress,
|
@Nullable NodeAddress takerNodeAddress,
|
||||||
@Nullable NodeAddress arbitratorNodeAddress) {
|
@Nullable NodeAddress arbitratorNodeAddress) {
|
||||||
|
super();
|
||||||
this.offer = offer;
|
this.offer = offer;
|
||||||
this.amount = tradeAmount.longValueExact();
|
this.amount = tradeAmount.longValueExact();
|
||||||
this.price = tradePrice;
|
this.price = tradePrice;
|
||||||
this.xmrWalletService = xmrWalletService;
|
this.xmrWalletService = xmrWalletService;
|
||||||
this.xmrConnectionService = xmrWalletService.getConnectionService();
|
this.xmrConnectionService = xmrWalletService.getXmrConnectionService();
|
||||||
this.processModel = processModel;
|
this.processModel = processModel;
|
||||||
this.uid = uid;
|
this.uid = uid;
|
||||||
this.takeOfferDate = new Date().getTime();
|
this.takeOfferDate = new Date().getTime();
|
||||||
@ -846,8 +841,8 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean requestSwitchToNextBestConnection() {
|
public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
|
||||||
if (xmrConnectionService.requestSwitchToNextBestConnection()) {
|
if (xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection)) {
|
||||||
onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread
|
onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -891,10 +886,6 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isReadTimeoutError(String errMsg) {
|
|
||||||
return errMsg.contains("Read timed out");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: checking error strings isn't robust, but the library doesn't provide a way to check if multisig hex is invalid. throw IllegalArgumentException from library on invalid multisig hex?
|
// TODO: checking error strings isn't robust, but the library doesn't provide a way to check if multisig hex is invalid. throw IllegalArgumentException from library on invalid multisig hex?
|
||||||
private boolean isInvalidImportError(String errMsg) {
|
private boolean isInvalidImportError(String errMsg) {
|
||||||
return errMsg.contains("Failed to parse hex") || errMsg.contains("Multisig info is for a different account");
|
return errMsg.contains("Failed to parse hex") || errMsg.contains("Multisig info is for a different account");
|
||||||
@ -1058,26 +1049,38 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
public MoneroTxWallet createTx(MoneroTxConfig txConfig) {
|
public MoneroTxWallet createTx(MoneroTxConfig txConfig) {
|
||||||
synchronized (walletLock) {
|
synchronized (walletLock) {
|
||||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||||
return wallet.createTx(txConfig);
|
MoneroTxWallet tx = wallet.createTx(txConfig);
|
||||||
|
exportMultisigHex();
|
||||||
|
requestSaveWallet();
|
||||||
|
return tx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void exportMultisigHex() {
|
||||||
|
synchronized (walletLock) {
|
||||||
|
getSelf().setUpdatedMultisigHex(wallet.exportMultisigHex());
|
||||||
|
requestPersistence();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void importMultisigHex() {
|
public void importMultisigHex() {
|
||||||
synchronized (walletLock) {
|
synchronized (walletLock) {
|
||||||
synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh
|
synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh
|
||||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
synchronized (importMultisigLock) {
|
||||||
try {
|
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||||
doImportMultisigHex();
|
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||||
break;
|
try {
|
||||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
doImportMultisigHex();
|
||||||
throw e;
|
break;
|
||||||
} catch (Exception e) {
|
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||||
log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
throw e;
|
||||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
} catch (Exception e) {
|
||||||
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
|
log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
||||||
if (isReadTimeoutError(e.getMessage())) forceRestartTradeWallet(); // wallet can be stuck a while
|
handleWalletError(e, sourceConnection);
|
||||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
|
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1154,6 +1157,12 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size());
|
log.info("Done importing multisig hexes for {} {} in {} ms, count={}", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime, multisigHexes.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleWalletError(Exception e, MoneroRpcConnection sourceConnection) {
|
||||||
|
if (HavenoUtils.isUnresponsive(e)) forceCloseWallet(); // wallet can be stuck a while
|
||||||
|
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection);
|
||||||
|
getWallet(); // re-open wallet
|
||||||
|
}
|
||||||
|
|
||||||
private String getMultisigHexRole(String multisigHex) {
|
private String getMultisigHexRole(String multisigHex) {
|
||||||
if (multisigHex.equals(getArbitrator().getUpdatedMultisigHex())) return "arbitrator";
|
if (multisigHex.equals(getArbitrator().getUpdatedMultisigHex())) return "arbitrator";
|
||||||
if (multisigHex.equals(getBuyer().getUpdatedMultisigHex())) return "buyer";
|
if (multisigHex.equals(getBuyer().getUpdatedMultisigHex())) return "buyer";
|
||||||
@ -1175,14 +1184,15 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
synchronized (walletLock) {
|
synchronized (walletLock) {
|
||||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||||
|
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||||
try {
|
try {
|
||||||
return doCreatePayoutTx();
|
return doCreatePayoutTx();
|
||||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Failed to create payout tx, tradeId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
|
log.warn("Failed to create payout tx, tradeId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
|
||||||
|
handleWalletError(e, sourceConnection);
|
||||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
|
|
||||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1220,13 +1230,11 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY));
|
.setPriority(XmrWalletService.PROTOCOL_FEE_PRIORITY));
|
||||||
|
|
||||||
// update state
|
// update state
|
||||||
saveWallet();
|
|
||||||
BigInteger payoutTxFeeSplit = payoutTx.getFee().divide(BigInteger.valueOf(2));
|
BigInteger payoutTxFeeSplit = payoutTx.getFee().divide(BigInteger.valueOf(2));
|
||||||
getBuyer().setPayoutTxFee(payoutTxFeeSplit);
|
getBuyer().setPayoutTxFee(payoutTxFeeSplit);
|
||||||
getBuyer().setPayoutAmount(HavenoUtils.getDestination(buyerPayoutAddress, payoutTx).getAmount());
|
getBuyer().setPayoutAmount(HavenoUtils.getDestination(buyerPayoutAddress, payoutTx).getAmount());
|
||||||
getSeller().setPayoutTxFee(payoutTxFeeSplit);
|
getSeller().setPayoutTxFee(payoutTxFeeSplit);
|
||||||
getSeller().setPayoutAmount(HavenoUtils.getDestination(sellerPayoutAddress, payoutTx).getAmount());
|
getSeller().setPayoutAmount(HavenoUtils.getDestination(sellerPayoutAddress, payoutTx).getAmount());
|
||||||
getSelf().setUpdatedMultisigHex(wallet.exportMultisigHex());
|
|
||||||
return payoutTx;
|
return payoutTx;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1234,6 +1242,7 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
synchronized (walletLock) {
|
synchronized (walletLock) {
|
||||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||||
|
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||||
try {
|
try {
|
||||||
if (wallet.isMultisigImportNeeded()) throw new IllegalStateException("Cannot create dispute payout tx because multisig import is needed for " + getClass().getSimpleName() + " " + getShortId());
|
if (wallet.isMultisigImportNeeded()) throw new IllegalStateException("Cannot create dispute payout tx because multisig import is needed for " + getClass().getSimpleName() + " " + getShortId());
|
||||||
return createTx(txConfig);
|
return createTx(txConfig);
|
||||||
@ -1242,8 +1251,8 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee");
|
if (e.getMessage().contains("not possible")) throw new IllegalArgumentException("Loser payout is too small to cover the mining fee");
|
||||||
log.warn("Failed to create dispute payout tx, tradeId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
|
log.warn("Failed to create dispute payout tx, tradeId={}, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
|
||||||
|
handleWalletError(e, sourceConnection);
|
||||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
|
|
||||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1263,6 +1272,7 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
synchronized (walletLock) {
|
synchronized (walletLock) {
|
||||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||||
|
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||||
try {
|
try {
|
||||||
doProcessPayoutTx(payoutTxHex, sign, publish);
|
doProcessPayoutTx(payoutTxHex, sign, publish);
|
||||||
break;
|
break;
|
||||||
@ -1270,9 +1280,12 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
throw e;
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Failed to process payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
|
log.warn("Failed to process payout tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, getShortId(), e.getMessage());
|
||||||
|
handleWalletError(e, sourceConnection);
|
||||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
|
|
||||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||||
|
} finally {
|
||||||
|
requestSaveWallet();
|
||||||
|
requestPersistence();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1492,13 +1505,13 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
|
|
||||||
// repeatedly acquire lock to clear tasks
|
// repeatedly acquire lock to clear tasks
|
||||||
for (int i = 0; i < 20; i++) {
|
for (int i = 0; i < 20; i++) {
|
||||||
synchronized (this) {
|
synchronized (getLock()) {
|
||||||
HavenoUtils.waitFor(10);
|
HavenoUtils.waitFor(10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// shut down trade threads
|
// shut down trade threads
|
||||||
synchronized (this) {
|
synchronized (getLock()) {
|
||||||
isInitialized = false;
|
isInitialized = false;
|
||||||
isShutDown = true;
|
isShutDown = true;
|
||||||
List<Runnable> shutDownThreads = new ArrayList<>();
|
List<Runnable> shutDownThreads = new ArrayList<>();
|
||||||
@ -2299,7 +2312,7 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
private void doPublishTradeStatistics() {
|
private void doPublishTradeStatistics() {
|
||||||
String referralId = processModel.getReferralIdService().getOptionalReferralId().orElse(null);
|
String referralId = processModel.getReferralIdService().getOptionalReferralId().orElse(null);
|
||||||
boolean isTorNetworkNode = getProcessModel().getP2PService().getNetworkNode() instanceof TorNetworkNode;
|
boolean isTorNetworkNode = getProcessModel().getP2PService().getNetworkNode() instanceof TorNetworkNode;
|
||||||
TradeStatistics3 tradeStatistics = TradeStatistics3.from(this, referralId, isTorNetworkNode);
|
TradeStatistics3 tradeStatistics = TradeStatistics3.from(this, referralId, isTorNetworkNode, true);
|
||||||
if (tradeStatistics.isValid()) {
|
if (tradeStatistics.isValid()) {
|
||||||
log.info("Publishing trade statistics for {} {}", getClass().getSimpleName(), getId());
|
log.info("Publishing trade statistics for {} {}", getClass().getSimpleName(), getId());
|
||||||
processModel.getP2PService().addPersistableNetworkPayload(tradeStatistics, true);
|
processModel.getP2PService().addPersistableNetworkPayload(tradeStatistics, true);
|
||||||
@ -2337,7 +2350,10 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
// check if ignored
|
// check if ignored
|
||||||
if (isShutDownStarted) return;
|
if (isShutDownStarted) return;
|
||||||
if (getWallet() == null) return;
|
if (getWallet() == null) return;
|
||||||
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return;
|
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) {
|
||||||
|
updatePollPeriod();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// set daemon connection (must restart monero-wallet-rpc if proxy uri changed)
|
// set daemon connection (must restart monero-wallet-rpc if proxy uri changed)
|
||||||
String oldProxyUri = wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri();
|
String oldProxyUri = wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri();
|
||||||
@ -2397,14 +2413,17 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void syncWallet(boolean pollWallet) {
|
private void syncWallet(boolean pollWallet) {
|
||||||
|
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||||
try {
|
try {
|
||||||
if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId());
|
synchronized (walletLock) {
|
||||||
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() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId());
|
||||||
if (isWalletBehind()) {
|
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(), getShortId());
|
if (isWalletBehind()) {
|
||||||
long startTime = System.currentTimeMillis();
|
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getShortId());
|
||||||
syncWalletIfBehind();
|
long startTime = System.currentTimeMillis();
|
||||||
log.info("Done syncing wallet for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime);
|
syncWalletIfBehind();
|
||||||
|
log.info("Done syncing wallet for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply tor after wallet synced depending on configuration
|
// apply tor after wallet synced depending on configuration
|
||||||
@ -2417,7 +2436,7 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
|
|
||||||
if (pollWallet) pollWallet();
|
if (pollWallet) pollWallet();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ThreadUtils.execute(() -> requestSwitchToNextBestConnection(), getId());
|
ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId());
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2546,11 +2565,12 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
|
|
||||||
// rescan spent outputs to detect unconfirmed payout tx
|
// rescan spent outputs to detect unconfirmed payout tx
|
||||||
if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) {
|
if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) {
|
||||||
|
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||||
try {
|
try {
|
||||||
wallet.rescanSpent();
|
wallet.rescanSpent();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Failed to rescan spent outputs for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage());
|
log.warn("Failed to rescan spent outputs for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage());
|
||||||
ThreadUtils.execute(() -> requestSwitchToNextBestConnection(), getId()); // do not block polling thread
|
ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId()); // do not block polling thread
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2595,12 +2615,11 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused");
|
if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet();
|
||||||
if (isConnectionRefused) forceRestartTradeWallet();
|
|
||||||
else {
|
else {
|
||||||
boolean isWalletConnected = isWalletConnectedToDaemon();
|
boolean isWalletConnected = isWalletConnectedToDaemon();
|
||||||
if (wallet != null && !isShutDownStarted && isWalletConnected) {
|
if (wallet != null && !isShutDownStarted && isWalletConnected) {
|
||||||
log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection());
|
log.warn("Error polling trade wallet for {} {}, errorMessage={}. Monerod={}", getClass().getSimpleName(), getShortId(), e.getMessage(), wallet.getDaemonConnection());
|
||||||
//e.printStackTrace();
|
//e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2613,9 +2632,15 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void syncWalletIfBehind() {
|
private void syncWalletIfBehind() {
|
||||||
if (isWalletBehind()) {
|
synchronized (walletLock) {
|
||||||
synchronized (walletLock) {
|
if (isWalletBehind()) {
|
||||||
xmrWalletService.syncWallet(wallet);
|
|
||||||
|
// TODO: local tests have timing failures unless sync called directly
|
||||||
|
if (xmrConnectionService.getTargetHeight() - walletHeight.get() < XmrWalletBase.DIRECT_SYNC_WITHIN_BLOCKS) {
|
||||||
|
xmrWalletService.syncWallet(wallet);
|
||||||
|
} else {
|
||||||
|
syncWithProgress();
|
||||||
|
}
|
||||||
walletHeight.set(wallet.getHeight());
|
walletHeight.set(wallet.getHeight());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2660,7 +2685,8 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
log.warn("Rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId());
|
log.warn("Rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId());
|
||||||
wallet.rescanBlockchain();
|
wallet.rescanBlockchain();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (isReadTimeoutError(e.getMessage())) forceRestartTradeWallet(); // wallet can be stuck a while
|
log.warn("Error rescanning blockchain for {} {}, errorMessage={}", getClass().getSimpleName(), getShortId(), e.getMessage());
|
||||||
|
if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet(); // wallet can be stuck a while
|
||||||
throw e;
|
throw e;
|
||||||
} finally {
|
} finally {
|
||||||
|
|
||||||
@ -2790,7 +2816,7 @@ public abstract class Trade implements Tradable, Model {
|
|||||||
if (!isInitialized || isShutDownStarted) return;
|
if (!isInitialized || isShutDownStarted) return;
|
||||||
if (isWalletConnectedToDaemon()) {
|
if (isWalletConnectedToDaemon()) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
log.warn("Error polling idle trade for {} {}: {}. Monerod={}", getClass().getSimpleName(), getId(), e.getMessage(), getXmrWalletService().getConnectionService().getConnection());
|
log.warn("Error polling idle trade for {} {}: {}. Monerod={}", getClass().getSimpleName(), getId(), e.getMessage(), getXmrWalletService().getXmrConnectionService().getConnection());
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, getId());
|
}, getId());
|
||||||
|
@ -45,7 +45,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
|
|||||||
public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) {
|
public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) {
|
||||||
System.out.println("ArbitratorProtocol.handleInitTradeRequest()");
|
System.out.println("ArbitratorProtocol.handleInitTradeRequest()");
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
latchTrade();
|
latchTrade();
|
||||||
this.errorMessageHandler = errorMessageHandler;
|
this.errorMessageHandler = errorMessageHandler;
|
||||||
processModel.setTradeMessage(message); // TODO (woodser): confirm these are null without being set
|
processModel.setTradeMessage(message); // TODO (woodser): confirm these are null without being set
|
||||||
@ -80,7 +80,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
|
|||||||
public void handleDepositRequest(DepositRequest request, NodeAddress sender) {
|
public void handleDepositRequest(DepositRequest request, NodeAddress sender) {
|
||||||
System.out.println("ArbitratorProtocol.handleDepositRequest() " + trade.getId());
|
System.out.println("ArbitratorProtocol.handleDepositRequest() " + trade.getId());
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
latchTrade();
|
latchTrade();
|
||||||
Validator.checkTradeId(processModel.getOfferId(), request);
|
Validator.checkTradeId(processModel.getOfferId(), request);
|
||||||
processModel.setTradeMessage(request);
|
processModel.setTradeMessage(request);
|
||||||
|
@ -62,7 +62,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
|
|||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
latchTrade();
|
latchTrade();
|
||||||
this.errorMessageHandler = errorMessageHandler;
|
this.errorMessageHandler = errorMessageHandler;
|
||||||
expect(phase(Trade.Phase.INIT)
|
expect(phase(Trade.Phase.INIT)
|
||||||
|
@ -70,7 +70,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
|
|||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
System.out.println(getClass().getSimpleName() + ".onTakeOffer()");
|
System.out.println(getClass().getSimpleName() + ".onTakeOffer()");
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
latchTrade();
|
latchTrade();
|
||||||
this.tradeResultHandler = tradeResultHandler;
|
this.tradeResultHandler = tradeResultHandler;
|
||||||
this.errorMessageHandler = errorMessageHandler;
|
this.errorMessageHandler = errorMessageHandler;
|
||||||
@ -101,7 +101,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
|
|||||||
NodeAddress peer) {
|
NodeAddress peer) {
|
||||||
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
latchTrade();
|
latchTrade();
|
||||||
expect(phase(Trade.Phase.INIT)
|
expect(phase(Trade.Phase.INIT)
|
||||||
.with(message)
|
.with(message)
|
||||||
|
@ -75,7 +75,7 @@ public class BuyerProtocol extends DisputeProtocol {
|
|||||||
// re-send payment sent message if not acked
|
// re-send payment sent message if not acked
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
|
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
|
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
|
||||||
if (trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal() && trade.getState().ordinal() < Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal()) {
|
if (trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal() && trade.getState().ordinal() < Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal()) {
|
||||||
latchTrade();
|
latchTrade();
|
||||||
@ -121,7 +121,7 @@ public class BuyerProtocol extends DisputeProtocol {
|
|||||||
public void onPaymentSent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
public void onPaymentSent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||||
System.out.println("BuyerProtocol.onPaymentSent()");
|
System.out.println("BuyerProtocol.onPaymentSent()");
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
latchTrade();
|
latchTrade();
|
||||||
this.errorMessageHandler = errorMessageHandler;
|
this.errorMessageHandler = errorMessageHandler;
|
||||||
BuyerEvent event = BuyerEvent.PAYMENT_SENT;
|
BuyerEvent event = BuyerEvent.PAYMENT_SENT;
|
||||||
|
@ -67,7 +67,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
|
|||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
latchTrade();
|
latchTrade();
|
||||||
this.errorMessageHandler = errorMessageHandler;
|
this.errorMessageHandler = errorMessageHandler;
|
||||||
expect(phase(Trade.Phase.INIT)
|
expect(phase(Trade.Phase.INIT)
|
||||||
|
@ -70,7 +70,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
|
|||||||
ErrorMessageHandler errorMessageHandler) {
|
ErrorMessageHandler errorMessageHandler) {
|
||||||
System.out.println(getClass().getSimpleName() + ".onTakeOffer()");
|
System.out.println(getClass().getSimpleName() + ".onTakeOffer()");
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
latchTrade();
|
latchTrade();
|
||||||
this.tradeResultHandler = tradeResultHandler;
|
this.tradeResultHandler = tradeResultHandler;
|
||||||
this.errorMessageHandler = errorMessageHandler;
|
this.errorMessageHandler = errorMessageHandler;
|
||||||
@ -101,7 +101,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
|
|||||||
NodeAddress peer) {
|
NodeAddress peer) {
|
||||||
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
|
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
latchTrade();
|
latchTrade();
|
||||||
expect(phase(Trade.Phase.INIT)
|
expect(phase(Trade.Phase.INIT)
|
||||||
.with(message)
|
.with(message)
|
||||||
|
@ -70,7 +70,7 @@ public class SellerProtocol extends DisputeProtocol {
|
|||||||
// re-send payment received message if payout not published
|
// re-send payment received message if payout not published
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
|
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
|
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
|
||||||
if (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !trade.isPayoutPublished()) {
|
if (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !trade.isPayoutPublished()) {
|
||||||
latchTrade();
|
latchTrade();
|
||||||
@ -117,7 +117,7 @@ public class SellerProtocol extends DisputeProtocol {
|
|||||||
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||||
log.info("SellerProtocol.onPaymentReceived()");
|
log.info("SellerProtocol.onPaymentReceived()");
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
latchTrade();
|
latchTrade();
|
||||||
this.errorMessageHandler = errorMessageHandler;
|
this.errorMessageHandler = errorMessageHandler;
|
||||||
SellerEvent event = SellerEvent.PAYMENT_RECEIVED;
|
SellerEvent event = SellerEvent.PAYMENT_RECEIVED;
|
||||||
|
@ -243,7 +243,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
if (!trade.isCompleted()) processModel.getP2PService().addDecryptedDirectMessageListener(this);
|
if (!trade.isCompleted()) processModel.getP2PService().addDecryptedDirectMessageListener(this);
|
||||||
|
|
||||||
// initialize trade
|
// initialize trade
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
trade.initialize(processModel.getProvider());
|
trade.initialize(processModel.getProvider());
|
||||||
|
|
||||||
// process mailbox messages
|
// process mailbox messages
|
||||||
@ -261,7 +261,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished() || depositsConfirmedTasksCalled) return;
|
if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished() || depositsConfirmedTasksCalled) return;
|
||||||
depositsConfirmedTasksCalled = true;
|
depositsConfirmedTasksCalled = true;
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down
|
if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down
|
||||||
latchTrade();
|
latchTrade();
|
||||||
expect(new Condition(trade))
|
expect(new Condition(trade))
|
||||||
@ -282,7 +282,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) {
|
public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) {
|
||||||
if (trade.isShutDownStarted()) return;
|
if (trade.isShutDownStarted()) return;
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
|
|
||||||
// skip if no need to reprocess
|
// skip if no need to reprocess
|
||||||
if (trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) {
|
if (trade.isSeller() || trade.getSeller().getPaymentReceivedMessage() == null || (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.isPayoutPublished())) {
|
||||||
@ -299,7 +299,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
||||||
trade.addInitProgressStep();
|
trade.addInitProgressStep();
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
|
|
||||||
// check trade
|
// check trade
|
||||||
if (trade.hasFailed()) {
|
if (trade.hasFailed()) {
|
||||||
@ -335,7 +335,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
||||||
System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
|
|
||||||
// check trade
|
// check trade
|
||||||
if (trade.hasFailed()) {
|
if (trade.hasFailed()) {
|
||||||
@ -379,7 +379,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
System.out.println(getClass().getSimpleName() + ".handleSignContractResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
System.out.println(getClass().getSimpleName() + ".handleSignContractResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
||||||
trade.addInitProgressStep();
|
trade.addInitProgressStep();
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
|
|
||||||
// check trade
|
// check trade
|
||||||
if (trade.hasFailed()) {
|
if (trade.hasFailed()) {
|
||||||
@ -425,7 +425,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
System.out.println(getClass().getSimpleName() + ".handleDepositResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
System.out.println(getClass().getSimpleName() + ".handleDepositResponse() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
||||||
trade.addInitProgressStep();
|
trade.addInitProgressStep();
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
Validator.checkTradeId(processModel.getOfferId(), response);
|
Validator.checkTradeId(processModel.getOfferId(), response);
|
||||||
latchTrade();
|
latchTrade();
|
||||||
processModel.setTradeMessage(response);
|
processModel.setTradeMessage(response);
|
||||||
@ -455,7 +455,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
System.out.println(getClass().getSimpleName() + ".handle(DepositsConfirmedMessage) from " + sender + " for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
System.out.println(getClass().getSimpleName() + ".handle(DepositsConfirmedMessage) from " + sender + " for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
||||||
if (!trade.isInitialized() || trade.isShutDown()) return;
|
if (!trade.isInitialized() || trade.isShutDown()) return;
|
||||||
ThreadUtils.execute(() -> {
|
ThreadUtils.execute(() -> {
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
if (!trade.isInitialized() || trade.isShutDown()) return;
|
if (!trade.isInitialized() || trade.isShutDown()) return;
|
||||||
latchTrade();
|
latchTrade();
|
||||||
this.errorMessageHandler = null;
|
this.errorMessageHandler = null;
|
||||||
@ -493,7 +493,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
// a mailbox message with PaymentSentMessage.
|
// a mailbox message with PaymentSentMessage.
|
||||||
// TODO A better fix would be to add a listener for the wallet sync state and process
|
// TODO A better fix would be to add a listener for the wallet sync state and process
|
||||||
// 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.getLock()) {
|
||||||
if (!trade.isInitialized() || trade.isShutDown()) return;
|
if (!trade.isInitialized() || trade.isShutDown()) return;
|
||||||
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) {
|
if (trade.getPhase().ordinal() >= Trade.Phase.PAYMENT_SENT.ordinal()) {
|
||||||
log.warn("Received another PaymentSentMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId());
|
log.warn("Received another PaymentSentMessage which was already processed for {} {}, ACKing", trade.getClass().getSimpleName(), trade.getId());
|
||||||
@ -542,7 +542,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator");
|
log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
synchronized (trade) {
|
synchronized (trade.getLock()) {
|
||||||
if (!trade.isInitialized() || trade.isShutDown()) return;
|
if (!trade.isInitialized() || trade.isShutDown()) return;
|
||||||
latchTrade();
|
latchTrade();
|
||||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||||
@ -817,7 +817,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
|||||||
}
|
}
|
||||||
|
|
||||||
void handleTaskRunnerFault(NodeAddress ackReceiver, @Nullable TradeMessage message, String source, String errorMessage) {
|
void handleTaskRunnerFault(NodeAddress ackReceiver, @Nullable TradeMessage message, String source, String errorMessage) {
|
||||||
log.error("Task runner failed with error {}. Triggered from {}. Monerod={}" , errorMessage, source, trade.getXmrWalletService().getConnectionService().getConnection());
|
log.error("Task runner failed with error {}. Triggered from {}. Monerod={}" , errorMessage, source, trade.getXmrWalletService().getXmrConnectionService().getConnection());
|
||||||
|
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
sendAckMessage(ackReceiver, message, false, errorMessage);
|
sendAckMessage(ackReceiver, message, false, errorMessage);
|
||||||
|
@ -64,9 +64,9 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
|
|||||||
|
|
||||||
// skip if payout tx already created
|
// skip if payout tx already created
|
||||||
if (trade.getSelf().getUnsignedPayoutTxHex() != null) {
|
if (trade.getSelf().getUnsignedPayoutTxHex() != null) {
|
||||||
log.warn("Skipping preparation of payment sent message because payout tx is already created for {} {}", trade.getClass().getSimpleName(), trade.getShortId());
|
log.warn("Skipping preparation of payment sent message because payout tx is already created for {} {}", trade.getClass().getSimpleName(), trade.getShortId());
|
||||||
complete();
|
complete();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate state
|
// validate state
|
||||||
@ -87,6 +87,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
|
|||||||
MoneroTxWallet payoutTx = trade.createPayoutTx();
|
MoneroTxWallet payoutTx = trade.createPayoutTx();
|
||||||
trade.updatePayout(payoutTx);
|
trade.updatePayout(payoutTx);
|
||||||
trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||||
|
trade.requestPersistence();
|
||||||
}
|
}
|
||||||
|
|
||||||
complete();
|
complete();
|
||||||
@ -107,101 +108,100 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
|
|||||||
*/
|
*/
|
||||||
public static class Pair<F, S> {
|
public static class Pair<F, S> {
|
||||||
|
|
||||||
private F first;
|
private F first;
|
||||||
private S second;
|
private S second;
|
||||||
|
|
||||||
public Pair(F first, S second) {
|
public Pair(F first, S second) {
|
||||||
super();
|
super();
|
||||||
this.first = first;
|
this.first = first;
|
||||||
this.second = second;
|
this.second = second;
|
||||||
}
|
}
|
||||||
|
|
||||||
public F getFirst() {
|
public F getFirst() {
|
||||||
return first;
|
return first;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFirst(F first) {
|
public void setFirst(F first) {
|
||||||
this.first = first;
|
this.first = first;
|
||||||
}
|
}
|
||||||
|
|
||||||
public S getSecond() {
|
public S getSecond() {
|
||||||
return second;
|
return second;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSecond(S second) {
|
public void setSecond(S second) {
|
||||||
this.second = second;
|
this.second = second;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void printBalances(MoneroWallet wallet) {
|
public static void printBalances(MoneroWallet wallet) {
|
||||||
|
|
||||||
// collect info about subaddresses
|
// collect info about subaddresses
|
||||||
List<Pair<String, List<Object>>> pairs = new ArrayList<Pair<String, List<Object>>>();
|
List<Pair<String, List<Object>>> pairs = new ArrayList<Pair<String, List<Object>>>();
|
||||||
//if (wallet == null) wallet = TestUtils.getWalletJni();
|
//if (wallet == null) wallet = TestUtils.getWalletJni();
|
||||||
BigInteger balance = wallet.getBalance();
|
BigInteger balance = wallet.getBalance();
|
||||||
BigInteger unlockedBalance = wallet.getUnlockedBalance();
|
BigInteger unlockedBalance = wallet.getUnlockedBalance();
|
||||||
List<MoneroAccount> accounts = wallet.getAccounts(true);
|
List<MoneroAccount> accounts = wallet.getAccounts(true);
|
||||||
System.out.println("Wallet balance: " + balance);
|
System.out.println("Wallet balance: " + balance);
|
||||||
System.out.println("Wallet unlocked balance: " + unlockedBalance);
|
System.out.println("Wallet unlocked balance: " + unlockedBalance);
|
||||||
for (MoneroAccount account : accounts) {
|
for (MoneroAccount account : accounts) {
|
||||||
add(pairs, "ACCOUNT", account.getIndex());
|
add(pairs, "ACCOUNT", account.getIndex());
|
||||||
add(pairs, "SUBADDRESS", "");
|
add(pairs, "SUBADDRESS", "");
|
||||||
add(pairs, "LABEL", "");
|
add(pairs, "LABEL", "");
|
||||||
add(pairs, "ADDRESS", "");
|
add(pairs, "ADDRESS", "");
|
||||||
add(pairs, "BALANCE", account.getBalance());
|
add(pairs, "BALANCE", account.getBalance());
|
||||||
add(pairs, "UNLOCKED", account.getUnlockedBalance());
|
add(pairs, "UNLOCKED", account.getUnlockedBalance());
|
||||||
for (MoneroSubaddress subaddress : account.getSubaddresses()) {
|
for (MoneroSubaddress subaddress : account.getSubaddresses()) {
|
||||||
add(pairs, "ACCOUNT", account.getIndex());
|
add(pairs, "ACCOUNT", account.getIndex());
|
||||||
add(pairs, "SUBADDRESS", subaddress.getIndex());
|
add(pairs, "SUBADDRESS", subaddress.getIndex());
|
||||||
add(pairs, "LABEL", subaddress.getLabel());
|
add(pairs, "LABEL", subaddress.getLabel());
|
||||||
add(pairs, "ADDRESS", subaddress.getAddress());
|
add(pairs, "ADDRESS", subaddress.getAddress());
|
||||||
add(pairs, "BALANCE", subaddress.getBalance());
|
add(pairs, "BALANCE", subaddress.getBalance());
|
||||||
add(pairs, "UNLOCKED", subaddress.getUnlockedBalance());
|
add(pairs, "UNLOCKED", subaddress.getUnlockedBalance());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// convert info to csv
|
// convert info to csv
|
||||||
Integer length = null;
|
Integer length = null;
|
||||||
for (Pair<String, List<Object>> pair : pairs) {
|
for (Pair<String, List<Object>> pair : pairs) {
|
||||||
if (length == null) length = pair.getSecond().size();
|
if (length == null)
|
||||||
}
|
length = pair.getSecond().size();
|
||||||
|
}
|
||||||
|
|
||||||
System.out.println(pairsToCsv(pairs));
|
System.out.println(pairsToCsv(pairs));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void add(List<Pair<String, List<Object>>> pairs, String header, Object value) {
|
private static void add(List<Pair<String, List<Object>>> pairs, String header, Object value) {
|
||||||
if (value == null) value = "";
|
if (value == null) value = "";
|
||||||
Pair<String, List<Object>> pair = null;
|
Pair<String, List<Object>> pair = null;
|
||||||
for (Pair<String, List<Object>> aPair : pairs) {
|
for (Pair<String, List<Object>> aPair : pairs) {
|
||||||
if (aPair.getFirst().equals(header)) {
|
if (aPair.getFirst().equals(header)) {
|
||||||
pair = aPair;
|
pair = aPair;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if (pair == null) {
|
||||||
if (pair == null) {
|
List<Object> vals = new ArrayList<Object>();
|
||||||
List<Object> vals = new ArrayList<Object>();
|
pair = new Pair<String, List<Object>>(header, vals);
|
||||||
pair = new Pair<String, List<Object>>(header, vals);
|
pairs.add(pair);
|
||||||
pairs.add(pair);
|
}
|
||||||
}
|
pair.getSecond().add(value);
|
||||||
pair.getSecond().add(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String pairsToCsv(List<Pair<String, List<Object>>> pairs) {
|
private static String pairsToCsv(List<Pair<String, List<Object>>> pairs) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (int i = 0; i < pairs.size(); i++) {
|
for (int i = 0; i < pairs.size(); i++) {
|
||||||
sb.append(pairs.get(i).getFirst());
|
sb.append(pairs.get(i).getFirst());
|
||||||
if (i < pairs.size() - 1) sb.append(',');
|
if (i < pairs.size() - 1) sb.append(',');
|
||||||
else sb.append('\n');
|
else sb.append('\n');
|
||||||
}
|
|
||||||
for (int i = 0; i < pairs.get(0).getSecond().size(); i++) {
|
|
||||||
for (int j = 0; j < pairs.size(); j++) {
|
|
||||||
sb.append(pairs.get(j).getSecond().get(i));
|
|
||||||
if (j < pairs.size() - 1) sb.append(',');
|
|
||||||
else sb.append('\n');
|
|
||||||
}
|
}
|
||||||
}
|
for (int i = 0; i < pairs.get(0).getSecond().size(); i++) {
|
||||||
return sb.toString();
|
for (int j = 0; j < pairs.size(); j++) {
|
||||||
|
sb.append(pairs.get(j).getSecond().get(i));
|
||||||
|
if (j < pairs.size() - 1) sb.append(',');
|
||||||
|
else sb.append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,9 +28,9 @@ import haveno.core.trade.Trade.State;
|
|||||||
import haveno.core.trade.messages.SignContractRequest;
|
import haveno.core.trade.messages.SignContractRequest;
|
||||||
import haveno.core.trade.protocol.TradeProtocol;
|
import haveno.core.trade.protocol.TradeProtocol;
|
||||||
import haveno.core.xmr.model.XmrAddressEntry;
|
import haveno.core.xmr.model.XmrAddressEntry;
|
||||||
import haveno.core.xmr.wallet.XmrWalletService;
|
|
||||||
import haveno.network.p2p.SendDirectMessageListener;
|
import haveno.network.p2p.SendDirectMessageListener;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import monero.common.MoneroRpcConnection;
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@ -77,7 +77,7 @@ public class MaybeSendSignContractRequest extends TradeTask {
|
|||||||
|
|
||||||
// create deposit tx and freeze inputs
|
// create deposit tx and freeze inputs
|
||||||
MoneroTxWallet depositTx = null;
|
MoneroTxWallet depositTx = null;
|
||||||
synchronized (XmrWalletService.WALLET_LOCK) {
|
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
|
||||||
|
|
||||||
// check for timeout
|
// check for timeout
|
||||||
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create deposit tx, tradeId=" + trade.getShortId());
|
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create deposit tx, tradeId=" + trade.getShortId());
|
||||||
@ -100,12 +100,14 @@ public class MaybeSendSignContractRequest extends TradeTask {
|
|||||||
try {
|
try {
|
||||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||||
|
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
|
||||||
try {
|
try {
|
||||||
depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
|
depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Error creating deposit tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
|
log.warn("Error creating deposit tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
|
||||||
|
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
|
||||||
|
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating deposit tx, tradeId=" + trade.getShortId());
|
||||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
if (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection();
|
|
||||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +58,6 @@ public class ProcessPaymentSentMessage extends TradeTask {
|
|||||||
|
|
||||||
// if seller, decrypt buyer's payment account payload
|
// if seller, decrypt buyer's payment account payload
|
||||||
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
|
if (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
|
||||||
trade.requestPersistence();
|
|
||||||
|
|
||||||
// update state
|
// update state
|
||||||
trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
|
trade.advanceState(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG);
|
||||||
|
@ -90,7 +90,7 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
|
|||||||
for (Dispute dispute : trade.getDisputes()) dispute.setIsClosed();
|
for (Dispute dispute : trade.getDisputes()) dispute.setIsClosed();
|
||||||
}
|
}
|
||||||
|
|
||||||
processModel.getTradeManager().requestPersistence();
|
trade.requestPersistence();
|
||||||
complete();
|
complete();
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
failed(t);
|
failed(t);
|
||||||
|
@ -76,8 +76,7 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas
|
|||||||
|
|
||||||
// export multisig hex once
|
// export multisig hex once
|
||||||
if (trade.getSelf().getUpdatedMultisigHex() == null) {
|
if (trade.getSelf().getUpdatedMultisigHex() == null) {
|
||||||
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());
|
trade.exportMultisigHex();
|
||||||
processModel.getTradeManager().requestPersistence();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We do not use a real unique ID here as we want to be able to re-send the exact same message in case the
|
// We do not use a real unique ID here as we want to be able to re-send the exact same message in case the
|
||||||
|
@ -24,8 +24,8 @@ import haveno.core.trade.TakerTrade;
|
|||||||
import haveno.core.trade.Trade;
|
import haveno.core.trade.Trade;
|
||||||
import haveno.core.trade.protocol.TradeProtocol;
|
import haveno.core.trade.protocol.TradeProtocol;
|
||||||
import haveno.core.xmr.model.XmrAddressEntry;
|
import haveno.core.xmr.model.XmrAddressEntry;
|
||||||
import haveno.core.xmr.wallet.XmrWalletService;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import monero.common.MoneroRpcConnection;
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@ -49,7 +49,7 @@ public class TakerReserveTradeFunds extends TradeTask {
|
|||||||
|
|
||||||
// create reserve tx
|
// create reserve tx
|
||||||
MoneroTxWallet reserveTx = null;
|
MoneroTxWallet reserveTx = null;
|
||||||
synchronized (XmrWalletService.WALLET_LOCK) {
|
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
|
||||||
|
|
||||||
// check for timeout
|
// check for timeout
|
||||||
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId());
|
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while getting lock to create reserve tx, tradeId=" + trade.getShortId());
|
||||||
@ -66,12 +66,14 @@ public class TakerReserveTradeFunds extends TradeTask {
|
|||||||
try {
|
try {
|
||||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||||
|
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
|
||||||
try {
|
try {
|
||||||
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
|
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Error creating reserve tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
|
log.warn("Error creating reserve tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
|
||||||
|
trade.getXmrWalletService().handleWalletError(e, sourceConnection);
|
||||||
|
if (isTimedOut()) throw new RuntimeException("Trade protocol has timed out while creating reserve tx, tradeId=" + trade.getShortId());
|
||||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
if (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection();
|
|
||||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,10 +69,13 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
|
|||||||
|
|
||||||
@JsonExclude
|
@JsonExclude
|
||||||
private transient static final ZoneId ZONE_ID = ZoneId.systemDefault();
|
private transient static final ZoneId ZONE_ID = ZoneId.systemDefault();
|
||||||
|
private static final double FUZZ_AMOUNT_PCT = 0.05;
|
||||||
|
private static final int FUZZ_DATE_HOURS = 24;
|
||||||
|
|
||||||
public static TradeStatistics3 from(Trade trade,
|
public static TradeStatistics3 from(Trade trade,
|
||||||
@Nullable String referralId,
|
@Nullable String referralId,
|
||||||
boolean isTorNetworkNode) {
|
boolean isTorNetworkNode,
|
||||||
|
boolean isFuzzed) {
|
||||||
Map<String, String> extraDataMap = new HashMap<>();
|
Map<String, String> extraDataMap = new HashMap<>();
|
||||||
if (referralId != null) {
|
if (referralId != null) {
|
||||||
extraDataMap.put(OfferPayload.REFERRAL_ID, referralId);
|
extraDataMap.put(OfferPayload.REFERRAL_ID, referralId);
|
||||||
@ -90,9 +93,9 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
|
|||||||
Offer offer = checkNotNull(trade.getOffer());
|
Offer offer = checkNotNull(trade.getOffer());
|
||||||
return new TradeStatistics3(offer.getCurrencyCode(),
|
return new TradeStatistics3(offer.getCurrencyCode(),
|
||||||
trade.getPrice().getValue(),
|
trade.getPrice().getValue(),
|
||||||
fuzzTradeAmountReproducibly(trade),
|
isFuzzed ? fuzzTradeAmountReproducibly(trade) : trade.getAmount().longValueExact(),
|
||||||
offer.getPaymentMethod().getId(),
|
offer.getPaymentMethod().getId(),
|
||||||
fuzzTradeDateReproducibly(trade),
|
isFuzzed ? fuzzTradeDateReproducibly(trade) : trade.getTakeOfferDate().getTime(),
|
||||||
truncatedArbitratorNodeAddress,
|
truncatedArbitratorNodeAddress,
|
||||||
extraDataMap);
|
extraDataMap);
|
||||||
}
|
}
|
||||||
@ -101,8 +104,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
|
|||||||
long originalTimestamp = trade.getTakeOfferDate().getTime();
|
long originalTimestamp = trade.getTakeOfferDate().getTime();
|
||||||
long exactAmount = trade.getAmount().longValueExact();
|
long exactAmount = trade.getAmount().longValueExact();
|
||||||
Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp
|
Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp
|
||||||
long adjustedAmount = (long) random.nextDouble(
|
long adjustedAmount = (long) random.nextDouble(exactAmount * (1.0 - FUZZ_AMOUNT_PCT), exactAmount * (1 + FUZZ_AMOUNT_PCT));
|
||||||
exactAmount * 0.95, exactAmount * 1.05);
|
|
||||||
log.debug("trade {} fuzzed trade amount for tradeStatistics is {}", trade.getShortId(), adjustedAmount);
|
log.debug("trade {} fuzzed trade amount for tradeStatistics is {}", trade.getShortId(), adjustedAmount);
|
||||||
return adjustedAmount;
|
return adjustedAmount;
|
||||||
}
|
}
|
||||||
@ -110,8 +112,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
|
|||||||
private static long fuzzTradeDateReproducibly(Trade trade) { // randomize completed trade info #1099
|
private static long fuzzTradeDateReproducibly(Trade trade) { // randomize completed trade info #1099
|
||||||
long originalTimestamp = trade.getTakeOfferDate().getTime();
|
long originalTimestamp = trade.getTakeOfferDate().getTime();
|
||||||
Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp
|
Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp
|
||||||
long adjustedTimestamp = random.nextLong(
|
long adjustedTimestamp = random.nextLong(originalTimestamp - TimeUnit.HOURS.toMillis(FUZZ_DATE_HOURS), originalTimestamp);
|
||||||
originalTimestamp-TimeUnit.HOURS.toMillis(24), originalTimestamp);
|
|
||||||
log.debug("trade {} fuzzed trade datestamp for tradeStatistics is {}", trade.getShortId(), new Date(adjustedTimestamp));
|
log.debug("trade {} fuzzed trade datestamp for tradeStatistics is {}", trade.getShortId(), new Date(adjustedTimestamp));
|
||||||
return adjustedTimestamp;
|
return adjustedTimestamp;
|
||||||
}
|
}
|
||||||
|
@ -110,35 +110,54 @@ public class TradeStatisticsManager {
|
|||||||
maybeDumpStatistics();
|
maybeDumpStatistics();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void deduplicateEarlyTradeStatistics(Set<TradeStatistics3> set) {
|
private void deduplicateEarlyTradeStatistics(Set<TradeStatistics3> tradeStats) {
|
||||||
|
|
||||||
// collect trades before May 31, 2024
|
// collect trades before August 7, 2024
|
||||||
Set<TradeStatistics3> tradesBeforeMay31_24 = set.stream()
|
Set<TradeStatistics3> earlyTrades = tradeStats.stream()
|
||||||
.filter(e -> e.getDate().toInstant().isBefore(Instant.parse("2024-05-31T00:00:00Z")))
|
.filter(e -> e.getDate().toInstant().isBefore(Instant.parse("2024-08-07T00:00:00Z")))
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
// collect duplicated trades
|
// collect duplicated trades
|
||||||
Set<TradeStatistics3> duplicated = new HashSet<TradeStatistics3>();
|
Set<TradeStatistics3> duplicates = new HashSet<TradeStatistics3>();
|
||||||
Set<TradeStatistics3> deduplicated = new HashSet<TradeStatistics3>();
|
Set<TradeStatistics3> deduplicates = new HashSet<TradeStatistics3>();
|
||||||
for (TradeStatistics3 tradeStatistics : tradesBeforeMay31_24) {
|
Set<TradeStatistics3> usedAsDuplicate = new HashSet<TradeStatistics3>();
|
||||||
if (hasLenientDuplicate(tradeStatistics, deduplicated)) duplicated.add(tradeStatistics);
|
for (TradeStatistics3 tradeStatistic : earlyTrades) {
|
||||||
else deduplicated.add(tradeStatistics);
|
TradeStatistics3 fuzzyDuplicate = findFuzzyDuplicate(tradeStatistic, deduplicates, usedAsDuplicate);
|
||||||
|
if (fuzzyDuplicate == null) deduplicates.add(tradeStatistic);
|
||||||
|
else {
|
||||||
|
duplicates.add(tradeStatistic);
|
||||||
|
usedAsDuplicate.add(fuzzyDuplicate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove duplicated trades
|
// remove duplicated trades
|
||||||
set.removeAll(duplicated);
|
tradeStats.removeAll(duplicates);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasLenientDuplicate(TradeStatistics3 tradeStatistics, Set<TradeStatistics3> set) {
|
private TradeStatistics3 findFuzzyDuplicate(TradeStatistics3 tradeStatistics, Set<TradeStatistics3> set, Set<TradeStatistics3> excluded) {
|
||||||
return set.stream().anyMatch(e -> isLenientDuplicate(tradeStatistics, e));
|
return set.stream().filter(e -> !excluded.contains(e)).filter(e -> isFuzzyDuplicate(tradeStatistics, e)).findFirst().orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isLenientDuplicate(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) {
|
private boolean isFuzzyDuplicate(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) {
|
||||||
boolean isWithin2Minutes = Math.abs(tradeStatistics1.getDate().getTime() - tradeStatistics2.getDate().getTime()) < 120000;
|
if (!tradeStatistics1.getPaymentMethodId().equals(tradeStatistics2.getPaymentMethodId())) return false;
|
||||||
return isWithin2Minutes &&
|
if (!tradeStatistics1.getCurrency().equals(tradeStatistics2.getCurrency())) return false;
|
||||||
tradeStatistics1.getCurrency().equals(tradeStatistics2.getCurrency()) &&
|
if (tradeStatistics1.getPrice() != tradeStatistics2.getPrice()) return false;
|
||||||
tradeStatistics1.getAmount() == tradeStatistics2.getAmount() &&
|
return isFuzzyDuplicateV1(tradeStatistics1, tradeStatistics2) || isFuzzyDuplicateV2(tradeStatistics1, tradeStatistics2);
|
||||||
tradeStatistics1.getPrice() == tradeStatistics2.getPrice();
|
}
|
||||||
|
|
||||||
|
// bug caused all peers to publish same trade with similar timestamps
|
||||||
|
private boolean isFuzzyDuplicateV1(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) {
|
||||||
|
boolean isWithin2Minutes = Math.abs(tradeStatistics1.getDate().getTime() - tradeStatistics2.getDate().getTime()) <= TimeUnit.MINUTES.toMillis(2);
|
||||||
|
return isWithin2Minutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bug caused sellers to re-publish their trades with randomized amounts
|
||||||
|
private static final double FUZZ_AMOUNT_PCT = 0.05;
|
||||||
|
private static final int FUZZ_DATE_HOURS = 24;
|
||||||
|
private boolean isFuzzyDuplicateV2(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) {
|
||||||
|
boolean isWithinFuzzedHours = Math.abs(tradeStatistics1.getDate().getTime() - tradeStatistics2.getDate().getTime()) <= TimeUnit.HOURS.toMillis(FUZZ_DATE_HOURS);
|
||||||
|
boolean isWithinFuzzedAmount = Math.abs(tradeStatistics1.getAmount() - tradeStatistics2.getAmount()) <= FUZZ_AMOUNT_PCT * tradeStatistics1.getAmount();
|
||||||
|
return isWithinFuzzedHours && isWithinFuzzedAmount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableSet<TradeStatistics3> getObservableTradeStatisticsSet() {
|
public ObservableSet<TradeStatistics3> getObservableTradeStatisticsSet() {
|
||||||
@ -206,13 +225,23 @@ public class TradeStatisticsManager {
|
|||||||
|
|
||||||
TradeStatistics3 tradeStatistics3 = null;
|
TradeStatistics3 tradeStatistics3 = null;
|
||||||
try {
|
try {
|
||||||
tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode);
|
tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode, false);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage());
|
log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TradeStatistics3 tradeStatistics3Fuzzed = null;
|
||||||
|
try {
|
||||||
|
tradeStatistics3Fuzzed = TradeStatistics3.from(trade, referralId, isTorNetworkNode, true);
|
||||||
|
} 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) {
|
boolean hasTradeStatistics3Fuzzed = hashes.contains(new P2PDataStorage.ByteArray(tradeStatistics3Fuzzed.getHash()));
|
||||||
|
if (hasTradeStatistics3 || hasTradeStatistics3Fuzzed) {
|
||||||
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.",
|
||||||
trade.getShortId());
|
trade.getShortId());
|
||||||
return;
|
return;
|
||||||
|
@ -44,6 +44,7 @@ import haveno.core.offer.OpenOfferManager;
|
|||||||
import haveno.core.support.dispute.Dispute;
|
import haveno.core.support.dispute.Dispute;
|
||||||
import haveno.core.support.dispute.refund.RefundManager;
|
import haveno.core.support.dispute.refund.RefundManager;
|
||||||
import haveno.core.trade.ClosedTradableManager;
|
import haveno.core.trade.ClosedTradableManager;
|
||||||
|
import haveno.core.trade.HavenoUtils;
|
||||||
import haveno.core.trade.MakerTrade;
|
import haveno.core.trade.MakerTrade;
|
||||||
import haveno.core.trade.Trade;
|
import haveno.core.trade.Trade;
|
||||||
import haveno.core.trade.TradeManager;
|
import haveno.core.trade.TradeManager;
|
||||||
@ -124,7 +125,7 @@ public class Balances {
|
|||||||
|
|
||||||
private void doUpdateBalances() {
|
private void doUpdateBalances() {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
synchronized (XmrWalletService.WALLET_LOCK) {
|
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
|
||||||
|
|
||||||
// get wallet balances
|
// get wallet balances
|
||||||
BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getBalance();
|
BigInteger balance = xmrWalletService.getWallet() == null ? BigInteger.ZERO : xmrWalletService.getBalance();
|
||||||
|
173
core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java
Normal file
173
core/src/main/java/haveno/core/xmr/wallet/XmrWalletBase.java
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
package haveno.core.xmr.wallet;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import haveno.common.Timer;
|
||||||
|
import haveno.common.UserThread;
|
||||||
|
import haveno.core.api.XmrConnectionService;
|
||||||
|
import haveno.core.trade.HavenoUtils;
|
||||||
|
import haveno.core.xmr.setup.DownloadListener;
|
||||||
|
import javafx.beans.property.LongProperty;
|
||||||
|
import javafx.beans.property.SimpleLongProperty;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import monero.common.TaskLooper;
|
||||||
|
import monero.daemon.model.MoneroTx;
|
||||||
|
import monero.wallet.MoneroWallet;
|
||||||
|
import monero.wallet.MoneroWalletFull;
|
||||||
|
import monero.wallet.model.MoneroWalletListener;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class XmrWalletBase {
|
||||||
|
|
||||||
|
// constants
|
||||||
|
public static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 60;
|
||||||
|
public static final int DIRECT_SYNC_WITHIN_BLOCKS = 100;
|
||||||
|
|
||||||
|
// inherited
|
||||||
|
protected MoneroWallet wallet;
|
||||||
|
@Getter
|
||||||
|
protected final Object walletLock = new Object();
|
||||||
|
@Getter
|
||||||
|
protected XmrConnectionService xmrConnectionService;
|
||||||
|
protected boolean wasWalletSynced;
|
||||||
|
protected final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
|
||||||
|
protected boolean isClosingWallet;
|
||||||
|
protected boolean isSyncingWithProgress;
|
||||||
|
protected Long syncStartHeight;
|
||||||
|
protected TaskLooper syncProgressLooper;
|
||||||
|
protected CountDownLatch syncProgressLatch;
|
||||||
|
protected Exception syncProgressError;
|
||||||
|
protected Timer syncProgressTimeout;
|
||||||
|
protected final DownloadListener downloadListener = new DownloadListener();
|
||||||
|
protected final LongProperty walletHeight = new SimpleLongProperty(0);
|
||||||
|
@Getter
|
||||||
|
protected boolean isShutDownStarted;
|
||||||
|
@Getter
|
||||||
|
protected boolean isShutDown;
|
||||||
|
|
||||||
|
// private
|
||||||
|
private boolean testReconnectOnStartup = false; // test reconnecting on startup while syncing so the wallet is blocked
|
||||||
|
private String testReconnectMonerod1 = "http://node.community.rino.io:18081";
|
||||||
|
private String testReconnectMonerod2 = "http://nodex.monerujo.io:18081";
|
||||||
|
|
||||||
|
public XmrWalletBase() {
|
||||||
|
this.xmrConnectionService = HavenoUtils.xmrConnectionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void syncWithProgress() {
|
||||||
|
syncWithProgress(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void syncWithProgress(boolean repeatSyncToLatestHeight) {
|
||||||
|
synchronized (walletLock) {
|
||||||
|
|
||||||
|
// set initial state
|
||||||
|
isSyncingWithProgress = true;
|
||||||
|
syncProgressError = null;
|
||||||
|
long targetHeightAtStart = xmrConnectionService.getTargetHeight();
|
||||||
|
syncStartHeight = walletHeight.get();
|
||||||
|
updateSyncProgress(syncStartHeight, targetHeightAtStart);
|
||||||
|
|
||||||
|
// test connection changing on startup before wallet synced
|
||||||
|
if (testReconnectOnStartup) {
|
||||||
|
UserThread.runAfter(() -> {
|
||||||
|
log.warn("Testing connection change on startup before wallet synced");
|
||||||
|
if (xmrConnectionService.getConnection().getUri().equals(testReconnectMonerod1)) xmrConnectionService.setConnection(testReconnectMonerod2);
|
||||||
|
else xmrConnectionService.setConnection(testReconnectMonerod1);
|
||||||
|
}, 1);
|
||||||
|
testReconnectOnStartup = false; // only run once
|
||||||
|
}
|
||||||
|
|
||||||
|
// native wallet provides sync notifications
|
||||||
|
if (wallet instanceof MoneroWalletFull) {
|
||||||
|
if (testReconnectOnStartup) HavenoUtils.waitFor(1000); // delay sync to test
|
||||||
|
wallet.sync(new MoneroWalletListener() {
|
||||||
|
@Override
|
||||||
|
public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) {
|
||||||
|
long appliedTargetHeight = repeatSyncToLatestHeight ? xmrConnectionService.getTargetHeight() : targetHeightAtStart;
|
||||||
|
updateSyncProgress(height, appliedTargetHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setWalletSyncedWithProgress();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// start polling wallet for progress
|
||||||
|
syncProgressLatch = new CountDownLatch(1);
|
||||||
|
syncProgressLooper = new TaskLooper(() -> {
|
||||||
|
if (wallet == null) return;
|
||||||
|
long height;
|
||||||
|
try {
|
||||||
|
height = wallet.getHeight(); // can get read timeout while syncing
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error getting wallet height while syncing with progress: " + e.getMessage());
|
||||||
|
if (wallet != null && !isShutDownStarted) e.printStackTrace();
|
||||||
|
|
||||||
|
// stop polling and release latch
|
||||||
|
syncProgressError = e;
|
||||||
|
syncProgressLatch.countDown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long appliedTargetHeight = repeatSyncToLatestHeight ? xmrConnectionService.getTargetHeight() : targetHeightAtStart;
|
||||||
|
updateSyncProgress(height, appliedTargetHeight);
|
||||||
|
if (height >= appliedTargetHeight) {
|
||||||
|
setWalletSyncedWithProgress();
|
||||||
|
syncProgressLatch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs());
|
||||||
|
syncProgressLooper.start(1000);
|
||||||
|
|
||||||
|
// wait for sync to complete
|
||||||
|
HavenoUtils.awaitLatch(syncProgressLatch);
|
||||||
|
|
||||||
|
// stop polling
|
||||||
|
syncProgressLooper.stop();
|
||||||
|
syncProgressTimeout.stop();
|
||||||
|
if (wallet != null) wallet.stopSyncing(); // can become null if interrupted by force close
|
||||||
|
isSyncingWithProgress = false;
|
||||||
|
if (syncProgressError != null) throw new RuntimeException(syncProgressError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSyncProgress(long height, long targetHeight) {
|
||||||
|
resetSyncProgressTimeout();
|
||||||
|
UserThread.execute(() -> {
|
||||||
|
|
||||||
|
// set wallet height
|
||||||
|
walletHeight.set(height);
|
||||||
|
|
||||||
|
// new wallet reports height 1 before synced
|
||||||
|
if (height == 1) {
|
||||||
|
downloadListener.progress(0, targetHeight - height, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set progress
|
||||||
|
long blocksLeft = targetHeight - walletHeight.get();
|
||||||
|
if (syncStartHeight == null) syncStartHeight = walletHeight.get();
|
||||||
|
double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) walletHeight.get() - syncStartHeight) / (double) (targetHeight - syncStartHeight));
|
||||||
|
downloadListener.progress(percent, blocksLeft, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private synchronized void resetSyncProgressTimeout() {
|
||||||
|
if (syncProgressTimeout != null) syncProgressTimeout.stop();
|
||||||
|
syncProgressTimeout = UserThread.runAfter(() -> {
|
||||||
|
if (isShutDownStarted) return;
|
||||||
|
syncProgressError = new RuntimeException("Sync progress timeout called");
|
||||||
|
syncProgressLatch.countDown();
|
||||||
|
}, SYNC_PROGRESS_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setWalletSyncedWithProgress() {
|
||||||
|
wasWalletSynced = true;
|
||||||
|
isSyncingWithProgress = false;
|
||||||
|
syncProgressTimeout.stop();
|
||||||
|
}
|
||||||
|
}
|
@ -24,7 +24,6 @@ import com.google.inject.name.Named;
|
|||||||
|
|
||||||
import common.utils.JsonUtils;
|
import common.utils.JsonUtils;
|
||||||
import haveno.common.ThreadUtils;
|
import haveno.common.ThreadUtils;
|
||||||
import haveno.common.Timer;
|
|
||||||
import haveno.common.UserThread;
|
import haveno.common.UserThread;
|
||||||
import haveno.common.config.Config;
|
import haveno.common.config.Config;
|
||||||
import haveno.common.file.FileUtil;
|
import haveno.common.file.FileUtil;
|
||||||
@ -43,7 +42,6 @@ import haveno.core.user.User;
|
|||||||
import haveno.core.xmr.listeners.XmrBalanceListener;
|
import haveno.core.xmr.listeners.XmrBalanceListener;
|
||||||
import haveno.core.xmr.model.XmrAddressEntry;
|
import haveno.core.xmr.model.XmrAddressEntry;
|
||||||
import haveno.core.xmr.model.XmrAddressEntryList;
|
import haveno.core.xmr.model.XmrAddressEntryList;
|
||||||
import haveno.core.xmr.setup.DownloadListener;
|
|
||||||
import haveno.core.xmr.setup.MoneroWalletRpcManager;
|
import haveno.core.xmr.setup.MoneroWalletRpcManager;
|
||||||
import haveno.core.xmr.setup.WalletsSetup;
|
import haveno.core.xmr.setup.WalletsSetup;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -55,26 +53,22 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.TreeSet;
|
import java.util.TreeSet;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.CopyOnWriteArraySet;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import javafx.beans.property.LongProperty;
|
import javafx.beans.property.LongProperty;
|
||||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||||
import javafx.beans.property.SimpleLongProperty;
|
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import lombok.Getter;
|
||||||
import monero.common.MoneroError;
|
import monero.common.MoneroError;
|
||||||
import monero.common.MoneroRpcConnection;
|
import monero.common.MoneroRpcConnection;
|
||||||
import monero.common.MoneroRpcError;
|
import monero.common.MoneroRpcError;
|
||||||
@ -103,14 +97,13 @@ import monero.wallet.model.MoneroTxPriority;
|
|||||||
import monero.wallet.model.MoneroTxQuery;
|
import monero.wallet.model.MoneroTxQuery;
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
import monero.wallet.model.MoneroWalletConfig;
|
import monero.wallet.model.MoneroWalletConfig;
|
||||||
import monero.wallet.model.MoneroWalletListener;
|
|
||||||
import monero.wallet.model.MoneroWalletListenerI;
|
import monero.wallet.model.MoneroWalletListenerI;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
||||||
public class XmrWalletService {
|
public class XmrWalletService extends XmrWalletBase {
|
||||||
private static final Logger log = LoggerFactory.getLogger(XmrWalletService.class);
|
private static final Logger log = LoggerFactory.getLogger(XmrWalletService.class);
|
||||||
|
|
||||||
// monero configuration
|
// monero configuration
|
||||||
@ -134,15 +127,13 @@ public class XmrWalletService {
|
|||||||
private static final String THREAD_ID = XmrWalletService.class.getSimpleName();
|
private static final String THREAD_ID = XmrWalletService.class.getSimpleName();
|
||||||
private static final long SHUTDOWN_TIMEOUT_MS = 60000;
|
private static final long SHUTDOWN_TIMEOUT_MS = 60000;
|
||||||
private static final long NUM_BLOCKS_BEHIND_TOLERANCE = 5;
|
private static final long NUM_BLOCKS_BEHIND_TOLERANCE = 5;
|
||||||
|
private static final long POLL_TXS_TOLERANCE_MS = 1000 * 60 * 3; // request connection switch if txs not updated within 3 minutes
|
||||||
|
|
||||||
private final User user;
|
private final User user;
|
||||||
private final Preferences preferences;
|
private final Preferences preferences;
|
||||||
private final CoreAccountService accountService;
|
private final CoreAccountService accountService;
|
||||||
private final XmrConnectionService xmrConnectionService;
|
|
||||||
private final XmrAddressEntryList xmrAddressEntryList;
|
private final XmrAddressEntryList xmrAddressEntryList;
|
||||||
private final WalletsSetup walletsSetup;
|
private final WalletsSetup walletsSetup;
|
||||||
private final DownloadListener downloadListener = new DownloadListener();
|
|
||||||
private final LongProperty walletHeight = new SimpleLongProperty(0);
|
|
||||||
|
|
||||||
private final File walletDir;
|
private final File walletDir;
|
||||||
private final File xmrWalletFile;
|
private final File xmrWalletFile;
|
||||||
@ -153,24 +144,15 @@ public class XmrWalletService {
|
|||||||
|
|
||||||
private ChangeListener<? super Number> walletInitListener;
|
private ChangeListener<? super Number> walletInitListener;
|
||||||
private TradeManager tradeManager;
|
private TradeManager tradeManager;
|
||||||
private MoneroWallet wallet;
|
|
||||||
public static final Object WALLET_LOCK = new Object();
|
|
||||||
private boolean wasWalletSynced;
|
|
||||||
private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
|
|
||||||
private boolean isClosingWallet;
|
|
||||||
private boolean isShutDownStarted;
|
|
||||||
private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type
|
private ExecutorService syncWalletThreadPool = Executors.newFixedThreadPool(10); // TODO: adjust based on connection type
|
||||||
private Long syncStartHeight;
|
|
||||||
private TaskLooper syncProgressLooper;
|
|
||||||
private CountDownLatch syncProgressLatch;
|
|
||||||
private Timer syncProgressTimeout;
|
|
||||||
private static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 45;
|
|
||||||
|
|
||||||
// wallet polling and cache
|
@Getter
|
||||||
|
public final Object lock = new Object();
|
||||||
private TaskLooper pollLooper;
|
private TaskLooper pollLooper;
|
||||||
private boolean pollInProgress;
|
private boolean pollInProgress;
|
||||||
private Long pollPeriodMs;
|
private Long pollPeriodMs;
|
||||||
private Long lastLogPollErrorTimestamp;
|
private long lastLogPollErrorTimestamp;
|
||||||
|
private long lastPollTxsTimestamp;
|
||||||
private final Object pollLock = new Object();
|
private final Object pollLock = new Object();
|
||||||
private Long cachedHeight;
|
private Long cachedHeight;
|
||||||
private BigInteger cachedBalance;
|
private BigInteger cachedBalance;
|
||||||
@ -178,7 +160,6 @@ public class XmrWalletService {
|
|||||||
private List<MoneroSubaddress> cachedSubaddresses;
|
private List<MoneroSubaddress> cachedSubaddresses;
|
||||||
private List<MoneroOutputWallet> cachedOutputs;
|
private List<MoneroOutputWallet> cachedOutputs;
|
||||||
private List<MoneroTxWallet> cachedTxs;
|
private List<MoneroTxWallet> cachedTxs;
|
||||||
private boolean runReconnectTestOnStartup = false; // test reconnecting on startup while syncing so the wallet is blocked
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@Inject
|
@Inject
|
||||||
@ -194,7 +175,6 @@ public class XmrWalletService {
|
|||||||
this.user = user;
|
this.user = user;
|
||||||
this.preferences = preferences;
|
this.preferences = preferences;
|
||||||
this.accountService = accountService;
|
this.accountService = accountService;
|
||||||
this.xmrConnectionService = xmrConnectionService;
|
|
||||||
this.walletsSetup = walletsSetup;
|
this.walletsSetup = walletsSetup;
|
||||||
this.xmrAddressEntryList = xmrAddressEntryList;
|
this.xmrAddressEntryList = xmrAddressEntryList;
|
||||||
this.walletDir = walletDir;
|
this.walletDir = walletDir;
|
||||||
@ -202,6 +182,8 @@ public class XmrWalletService {
|
|||||||
this.useNativeXmrWallet = useNativeXmrWallet;
|
this.useNativeXmrWallet = useNativeXmrWallet;
|
||||||
this.xmrWalletFile = new File(walletDir, MONERO_WALLET_NAME);
|
this.xmrWalletFile = new File(walletDir, MONERO_WALLET_NAME);
|
||||||
HavenoUtils.xmrWalletService = this;
|
HavenoUtils.xmrWalletService = this;
|
||||||
|
HavenoUtils.xmrConnectionService = xmrConnectionService;
|
||||||
|
this.xmrConnectionService = xmrConnectionService; // TODO: super's is null unless set here from injection
|
||||||
|
|
||||||
// set monero logging
|
// set monero logging
|
||||||
if (MONERO_LOG_LEVEL >= 0) MoneroUtils.setLogLevel(MONERO_LOG_LEVEL);
|
if (MONERO_LOG_LEVEL >= 0) MoneroUtils.setLogLevel(MONERO_LOG_LEVEL);
|
||||||
@ -316,10 +298,6 @@ public class XmrWalletService {
|
|||||||
return xmrConnectionService.getDaemon();
|
return xmrConnectionService.getDaemon();
|
||||||
}
|
}
|
||||||
|
|
||||||
public XmrConnectionService getConnectionService() {
|
|
||||||
return xmrConnectionService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isProxyApplied() {
|
public boolean isProxyApplied() {
|
||||||
return isProxyApplied(wasWalletSynced);
|
return isProxyApplied(wasWalletSynced);
|
||||||
}
|
}
|
||||||
@ -420,6 +398,10 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void forceCloseWallet(MoneroWallet wallet, String path) {
|
public void forceCloseWallet(MoneroWallet wallet, String path) {
|
||||||
|
if (wallet == null) {
|
||||||
|
log.warn("Ignoring force close wallet because wallet is null, path={}", path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (wallet instanceof MoneroWalletRpc) {
|
if (wallet instanceof MoneroWalletRpc) {
|
||||||
MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) wallet, path, true);
|
MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) wallet, path, true);
|
||||||
} else {
|
} else {
|
||||||
@ -456,7 +438,7 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public MoneroTxWallet createTx(MoneroTxConfig txConfig) {
|
public MoneroTxWallet createTx(MoneroTxConfig txConfig) {
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||||
MoneroTxWallet tx = wallet.createTx(txConfig);
|
MoneroTxWallet tx = wallet.createTx(txConfig);
|
||||||
if (Boolean.TRUE.equals(txConfig.getRelay())) {
|
if (Boolean.TRUE.equals(txConfig.getRelay())) {
|
||||||
@ -469,6 +451,14 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String relayTx(String metadata) {
|
||||||
|
synchronized (walletLock) {
|
||||||
|
String txId = wallet.relayTx(metadata);
|
||||||
|
requestSaveMainWallet();
|
||||||
|
return txId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
|
public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
|
||||||
MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));
|
MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));
|
||||||
//printTxs("XmrWalletService.createTx", tx);
|
//printTxs("XmrWalletService.createTx", tx);
|
||||||
@ -479,7 +469,7 @@ public class XmrWalletService {
|
|||||||
* Freeze reserved outputs and thaw unreserved outputs.
|
* Freeze reserved outputs and thaw unreserved outputs.
|
||||||
*/
|
*/
|
||||||
public void fixReservedOutputs() {
|
public void fixReservedOutputs() {
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
|
|
||||||
// collect reserved outputs
|
// collect reserved outputs
|
||||||
Set<String> reservedKeyImages = new HashSet<String>();
|
Set<String> reservedKeyImages = new HashSet<String>();
|
||||||
@ -498,7 +488,7 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void freezeReservedOutputs(Set<String> reservedKeyImages) {
|
private void freezeReservedOutputs(Set<String> reservedKeyImages) {
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
|
|
||||||
// ensure wallet is open
|
// ensure wallet is open
|
||||||
if (wallet == null) {
|
if (wallet == null) {
|
||||||
@ -522,7 +512,7 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void thawUnreservedOutputs(Set<String> reservedKeyImages) {
|
private void thawUnreservedOutputs(Set<String> reservedKeyImages) {
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
|
|
||||||
// ensure wallet is open
|
// ensure wallet is open
|
||||||
if (wallet == null) {
|
if (wallet == null) {
|
||||||
@ -552,7 +542,7 @@ public class XmrWalletService {
|
|||||||
*/
|
*/
|
||||||
public void freezeOutputs(Collection<String> keyImages) {
|
public void freezeOutputs(Collection<String> keyImages) {
|
||||||
if (keyImages == null || keyImages.isEmpty()) return;
|
if (keyImages == null || keyImages.isEmpty()) return;
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
|
|
||||||
// collect outputs to freeze
|
// collect outputs to freeze
|
||||||
List<String> unfrozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(false).setIsSpent(false)).stream()
|
List<String> unfrozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(false).setIsSpent(false)).stream()
|
||||||
@ -574,7 +564,7 @@ public class XmrWalletService {
|
|||||||
*/
|
*/
|
||||||
public void thawOutputs(Collection<String> keyImages) {
|
public void thawOutputs(Collection<String> keyImages) {
|
||||||
if (keyImages == null || keyImages.isEmpty()) return;
|
if (keyImages == null || keyImages.isEmpty()) return;
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
|
|
||||||
// collect outputs to thaw
|
// collect outputs to thaw
|
||||||
List<String> frozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)).stream()
|
List<String> frozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)).stream()
|
||||||
@ -627,7 +617,7 @@ public class XmrWalletService {
|
|||||||
* @return the reserve tx
|
* @return the reserve tx
|
||||||
*/
|
*/
|
||||||
public MoneroTxWallet createReserveTx(BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendTradeAmount, BigInteger securityDeposit, String returnAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
|
public MoneroTxWallet createReserveTx(BigInteger penaltyFee, BigInteger tradeFee, BigInteger sendTradeAmount, BigInteger securityDeposit, String returnAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||||
log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress);
|
log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress);
|
||||||
long time = System.currentTimeMillis();
|
long time = System.currentTimeMillis();
|
||||||
@ -648,7 +638,7 @@ public class XmrWalletService {
|
|||||||
* @return MoneroTxWallet the multisig deposit tx
|
* @return MoneroTxWallet the multisig deposit tx
|
||||||
*/
|
*/
|
||||||
public MoneroTxWallet createDepositTx(Trade trade, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
|
public MoneroTxWallet createDepositTx(Trade trade, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||||
BigInteger feeAmount = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee();
|
BigInteger feeAmount = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee();
|
||||||
String feeAddress = trade.getProcessModel().getTradeFeeAddress();
|
String feeAddress = trade.getProcessModel().getTradeFeeAddress();
|
||||||
@ -666,7 +656,7 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private MoneroTxWallet createTradeTx(BigInteger feeAmount, String feeAddress, BigInteger sendAmount, String sendAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
|
private MoneroTxWallet createTradeTx(BigInteger feeAmount, String feeAddress, BigInteger sendAmount, String sendAddress, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
MoneroWallet wallet = getWallet();
|
MoneroWallet wallet = getWallet();
|
||||||
|
|
||||||
// create a list of subaddresses to attempt spending from in preferred order
|
// create a list of subaddresses to attempt spending from in preferred order
|
||||||
@ -903,7 +893,7 @@ public class XmrWalletService {
|
|||||||
Runnable shutDownTask = () -> {
|
Runnable shutDownTask = () -> {
|
||||||
|
|
||||||
// remove listeners
|
// remove listeners
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
if (wallet != null) {
|
if (wallet != null) {
|
||||||
for (MoneroWalletListenerI listener : new HashSet<>(wallet.getListeners())) {
|
for (MoneroWalletListenerI listener : new HashSet<>(wallet.getListeners())) {
|
||||||
wallet.removeListener(listener);
|
wallet.removeListener(listener);
|
||||||
@ -913,7 +903,7 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// shut down threads
|
// shut down threads
|
||||||
synchronized (this) {
|
synchronized (getLock()) {
|
||||||
List<Runnable> shutDownThreads = new ArrayList<>();
|
List<Runnable> shutDownThreads = new ArrayList<>();
|
||||||
shutDownThreads.add(() -> ThreadUtils.shutDown(THREAD_ID));
|
shutDownThreads.add(() -> ThreadUtils.shutDown(THREAD_ID));
|
||||||
ThreadUtils.awaitTasks(shutDownThreads);
|
ThreadUtils.awaitTasks(shutDownThreads);
|
||||||
@ -1285,9 +1275,19 @@ public class XmrWalletService {
|
|||||||
else log.info(appliedMsg);
|
else log.info(appliedMsg);
|
||||||
|
|
||||||
// listen for connection changes
|
// listen for connection changes
|
||||||
xmrConnectionService.addConnectionListener(connection -> ThreadUtils.execute(() -> {
|
xmrConnectionService.addConnectionListener(connection -> {
|
||||||
onConnectionChanged(connection);
|
if (wasWalletSynced && !isSyncingWithProgress) {
|
||||||
}, THREAD_ID));
|
ThreadUtils.execute(() -> {
|
||||||
|
onConnectionChanged(connection);
|
||||||
|
}, THREAD_ID);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// force restart main wallet if connection changed while syncing
|
||||||
|
log.warn("Force restarting main wallet because connection changed while syncing");
|
||||||
|
forceRestartMainWallet();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// initialize main wallet when daemon synced
|
// initialize main wallet when daemon synced
|
||||||
walletInitListener = (obs, oldVal, newVal) -> initMainWalletIfConnected();
|
walletInitListener = (obs, oldVal, newVal) -> initMainWalletIfConnected();
|
||||||
@ -1306,11 +1306,20 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void maybeInitMainWallet(boolean sync, int numAttempts) {
|
private void maybeInitMainWallet(boolean sync, int numAttempts) {
|
||||||
ThreadUtils.execute(() -> doMaybeInitMainWallet(sync, numAttempts), THREAD_ID);
|
ThreadUtils.execute(() -> {
|
||||||
|
try {
|
||||||
|
doMaybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error initializing main wallet: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
|
HavenoUtils.setTopError(e.getMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}, THREAD_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doMaybeInitMainWallet(boolean sync, int numAttempts) {
|
private void doMaybeInitMainWallet(boolean sync, int numAttempts) {
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
if (isShutDownStarted) return;
|
if (isShutDownStarted) return;
|
||||||
|
|
||||||
// open or create wallet main wallet
|
// open or create wallet main wallet
|
||||||
@ -1327,6 +1336,7 @@ public class XmrWalletService {
|
|||||||
long date = localDateTime.toEpochSecond(ZoneOffset.UTC);
|
long date = localDateTime.toEpochSecond(ZoneOffset.UTC);
|
||||||
user.setWalletCreationDate(date);
|
user.setWalletCreationDate(date);
|
||||||
}
|
}
|
||||||
|
walletHeight.set(wallet.getHeight());
|
||||||
isClosingWallet = false;
|
isClosingWallet = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1335,6 +1345,7 @@ public class XmrWalletService {
|
|||||||
log.info("Monero wallet path={}", wallet.getPath());
|
log.info("Monero wallet path={}", wallet.getPath());
|
||||||
|
|
||||||
// sync main wallet if applicable
|
// sync main wallet if applicable
|
||||||
|
// TODO: error handling and re-initialization is jenky, refactor
|
||||||
if (sync && numAttempts > 0) {
|
if (sync && numAttempts > 0) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@ -1347,7 +1358,16 @@ public class XmrWalletService {
|
|||||||
// sync main wallet
|
// sync main wallet
|
||||||
log.info("Syncing main wallet");
|
log.info("Syncing main wallet");
|
||||||
long time = System.currentTimeMillis();
|
long time = System.currentTimeMillis();
|
||||||
syncWithProgress(); // blocking
|
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||||
|
try {
|
||||||
|
syncWithProgress(true); // repeat sync to latest target height
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error syncing wallet with progress on startup: " + e.getMessage());
|
||||||
|
forceCloseMainWallet();
|
||||||
|
requestSwitchToNextBestConnection(sourceConnection);
|
||||||
|
maybeInitMainWallet(true, numAttempts - 1); // re-initialize wallet and sync again
|
||||||
|
return;
|
||||||
|
}
|
||||||
log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms");
|
log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms");
|
||||||
|
|
||||||
// poll wallet
|
// poll wallet
|
||||||
@ -1410,7 +1430,7 @@ public class XmrWalletService {
|
|||||||
if (baseAddresses.size() > 1 || (baseAddresses.size() == 1 && !baseAddresses.get(0).getAddressString().equals(wallet.getPrimaryAddress()))) {
|
if (baseAddresses.size() > 1 || (baseAddresses.size() == 1 && !baseAddresses.get(0).getAddressString().equals(wallet.getPrimaryAddress()))) {
|
||||||
String warningMsg = "New Monero wallet detected. Resetting internal state.";
|
String warningMsg = "New Monero wallet detected. Resetting internal state.";
|
||||||
if (!tradeManager.getOpenTrades().isEmpty()) warningMsg += "\n\nWARNING: Your open trades will settle to the payout address in the OLD wallet!"; // TODO: allow payout address to be updated in PaymentSentMessage, PaymentReceivedMessage, and DisputeOpenedMessage?
|
if (!tradeManager.getOpenTrades().isEmpty()) warningMsg += "\n\nWARNING: Your open trades will settle to the payout address in the OLD wallet!"; // TODO: allow payout address to be updated in PaymentSentMessage, PaymentReceivedMessage, and DisputeOpenedMessage?
|
||||||
HavenoUtils.havenoSetup.getTopErrorMsg().set(warningMsg);
|
HavenoUtils.setTopError(warningMsg);
|
||||||
|
|
||||||
// reset address entries
|
// reset address entries
|
||||||
xmrAddressEntryList.clear();
|
xmrAddressEntryList.clear();
|
||||||
@ -1421,94 +1441,6 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void syncWithProgress() {
|
|
||||||
|
|
||||||
// start sync progress timeout
|
|
||||||
resetSyncProgressTimeout();
|
|
||||||
|
|
||||||
// show sync progress
|
|
||||||
updateSyncProgress(wallet.getHeight());
|
|
||||||
|
|
||||||
// test connection changing on startup before wallet synced
|
|
||||||
if (runReconnectTestOnStartup) {
|
|
||||||
UserThread.runAfter(() -> {
|
|
||||||
log.warn("Testing connection change on startup before wallet synced");
|
|
||||||
xmrConnectionService.setConnection("http://node.community.rino.io:18081"); // TODO: needs to be online
|
|
||||||
}, 1);
|
|
||||||
runReconnectTestOnStartup = false; // only run once
|
|
||||||
}
|
|
||||||
|
|
||||||
// get sync notifications from native wallet
|
|
||||||
if (wallet instanceof MoneroWalletFull) {
|
|
||||||
if (runReconnectTestOnStartup) HavenoUtils.waitFor(1000); // delay sync to test
|
|
||||||
wallet.sync(new MoneroWalletListener() {
|
|
||||||
@Override
|
|
||||||
public void onSyncProgress(long height, long startHeight, long endHeight, double percentDone, String message) {
|
|
||||||
updateSyncProgress(height);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
wasWalletSynced = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// poll wallet for progress
|
|
||||||
wallet.startSyncing(xmrConnectionService.getRefreshPeriodMs());
|
|
||||||
syncProgressLatch = new CountDownLatch(1);
|
|
||||||
syncProgressLooper = new TaskLooper(() -> {
|
|
||||||
if (wallet == null) return;
|
|
||||||
long height = 0;
|
|
||||||
try {
|
|
||||||
height = wallet.getHeight(); // can get read timeout while syncing
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (!isShutDownStarted) e.printStackTrace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (height < xmrConnectionService.getTargetHeight()) updateSyncProgress(height);
|
|
||||||
else {
|
|
||||||
syncProgressLooper.stop();
|
|
||||||
wasWalletSynced = true;
|
|
||||||
updateSyncProgress(height);
|
|
||||||
syncProgressLatch.countDown();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
syncProgressLooper.start(1000);
|
|
||||||
HavenoUtils.awaitLatch(syncProgressLatch);
|
|
||||||
wallet.stopSyncing();
|
|
||||||
if (!wasWalletSynced) throw new IllegalStateException("Failed to sync wallet with progress");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateSyncProgress(long height) {
|
|
||||||
UserThread.execute(() -> {
|
|
||||||
walletHeight.set(height);
|
|
||||||
resetSyncProgressTimeout();
|
|
||||||
|
|
||||||
// new wallet reports height 1 before synced
|
|
||||||
if (height == 1) {
|
|
||||||
downloadListener.progress(.0001, xmrConnectionService.getTargetHeight() - height, null); // >0% shows progress bar
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set progress
|
|
||||||
long targetHeight = xmrConnectionService.getTargetHeight();
|
|
||||||
long blocksLeft = targetHeight - walletHeight.get();
|
|
||||||
if (syncStartHeight == null) syncStartHeight = walletHeight.get();
|
|
||||||
double percent = Math.min(1.0, targetHeight == syncStartHeight ? 1.0 : ((double) Math.max(1, (double) walletHeight.get() - syncStartHeight) / (double) (targetHeight - syncStartHeight))); // grant at least 1 block to show progress
|
|
||||||
downloadListener.progress(percent, blocksLeft, null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void resetSyncProgressTimeout() {
|
|
||||||
if (syncProgressTimeout != null) syncProgressTimeout.stop();
|
|
||||||
syncProgressTimeout = UserThread.runAfter(() -> {
|
|
||||||
if (isShutDownStarted || wasWalletSynced) return;
|
|
||||||
log.warn("Sync progress timeout called");
|
|
||||||
forceCloseMainWallet();
|
|
||||||
requestSwitchToNextBestConnection();
|
|
||||||
maybeInitMainWallet(true);
|
|
||||||
resetSyncProgressTimeout();
|
|
||||||
}, SYNC_PROGRESS_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private MoneroWalletFull createWalletFull(MoneroWalletConfig config) {
|
private MoneroWalletFull createWalletFull(MoneroWalletConfig config) {
|
||||||
|
|
||||||
// must be connected to daemon
|
// must be connected to daemon
|
||||||
@ -1529,7 +1461,7 @@ public class XmrWalletService {
|
|||||||
return walletFull;
|
return walletFull;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
if (walletFull != null) forceCloseMainWallet();
|
if (walletFull != null) forceCloseWallet(walletFull, config.getPath());
|
||||||
throw new IllegalStateException("Could not create wallet '" + config.getPath() + "'");
|
throw new IllegalStateException("Could not create wallet '" + config.getPath() + "'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1661,27 +1593,22 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onConnectionChanged(MoneroRpcConnection connection) {
|
private void onConnectionChanged(MoneroRpcConnection connection) {
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
|
|
||||||
// use current connection
|
// use current connection
|
||||||
connection = xmrConnectionService.getConnection();
|
connection = xmrConnectionService.getConnection();
|
||||||
|
|
||||||
// check if ignored
|
// check if ignored
|
||||||
if (wallet == null || isShutDownStarted) return;
|
if (wallet == null || isShutDownStarted) return;
|
||||||
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return;
|
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) {
|
||||||
String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri();
|
updatePollPeriod();
|
||||||
String newProxyUri = connection == null ? null : connection.getProxyUri();
|
|
||||||
log.info("Setting daemon connection for main wallet, monerod={}, proxyUri={}", connection == null ? null : connection.getUri(), newProxyUri);
|
|
||||||
|
|
||||||
// force restart main wallet if connection changed before synced
|
|
||||||
if (!wasWalletSynced) {
|
|
||||||
if (!Boolean.TRUE.equals(xmrConnectionService.isConnected())) return;
|
|
||||||
log.warn("Force restarting main wallet because connection changed before inital sync");
|
|
||||||
forceRestartMainWallet();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update connection
|
// update connection
|
||||||
|
String oldProxyUri = wallet == null || wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri();
|
||||||
|
String newProxyUri = connection == null ? null : connection.getProxyUri();
|
||||||
|
log.info("Setting daemon connection for main wallet, monerod={}, proxyUri={}", connection == null ? null : connection.getUri(), newProxyUri);
|
||||||
if (wallet instanceof MoneroWalletRpc) {
|
if (wallet instanceof MoneroWalletRpc) {
|
||||||
if (StringUtils.equals(oldProxyUri, newProxyUri)) {
|
if (StringUtils.equals(oldProxyUri, newProxyUri)) {
|
||||||
wallet.setDaemonConnection(connection);
|
wallet.setDaemonConnection(connection);
|
||||||
@ -1689,7 +1616,7 @@ public class XmrWalletService {
|
|||||||
log.info("Restarting main wallet because proxy URI has changed, old={}, new={}", oldProxyUri, newProxyUri); // TODO: set proxy without restarting wallet
|
log.info("Restarting main wallet because proxy URI has changed, old={}, new={}", oldProxyUri, newProxyUri); // TODO: set proxy without restarting wallet
|
||||||
closeMainWallet(true);
|
closeMainWallet(true);
|
||||||
doMaybeInitMainWallet(false, MAX_SYNC_ATTEMPTS);
|
doMaybeInitMainWallet(false, MAX_SYNC_ATTEMPTS);
|
||||||
return; // wallet is re-initialized
|
return; // wallet re-initializes off thread
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
wallet.setDaemonConnection(connection);
|
wallet.setDaemonConnection(connection);
|
||||||
@ -1736,14 +1663,14 @@ public class XmrWalletService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// excute tasks in parallel
|
// execute tasks in parallel
|
||||||
ThreadUtils.awaitTasks(tasks, Math.min(10, 1 + trades.size()));
|
ThreadUtils.awaitTasks(tasks, Math.min(10, 1 + trades.size()));
|
||||||
log.info("Done changing all wallet passwords");
|
log.info("Done changing all wallet passwords");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void closeMainWallet(boolean save) {
|
private void closeMainWallet(boolean save) {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
try {
|
try {
|
||||||
if (wallet != null) {
|
if (wallet != null) {
|
||||||
isClosingWallet = true;
|
isClosingWallet = true;
|
||||||
@ -1758,19 +1685,28 @@ public class XmrWalletService {
|
|||||||
|
|
||||||
private void forceCloseMainWallet() {
|
private void forceCloseMainWallet() {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
isClosingWallet = true;
|
if (wallet != null && !isClosingWallet) {
|
||||||
forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME));
|
isClosingWallet = true;
|
||||||
wallet = null;
|
forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME));
|
||||||
|
wallet = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void forceRestartMainWallet() {
|
public void forceRestartMainWallet() {
|
||||||
log.warn("Force restarting main wallet");
|
log.warn("Force restarting main wallet");
|
||||||
|
if (isClosingWallet) return;
|
||||||
forceCloseMainWallet();
|
forceCloseMainWallet();
|
||||||
maybeInitMainWallet(true);
|
maybeInitMainWallet(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void handleWalletError(Exception e, MoneroRpcConnection sourceConnection) {
|
||||||
|
if (HavenoUtils.isUnresponsive(e)) forceCloseMainWallet(); // wallet can be stuck a while
|
||||||
|
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection(sourceConnection);
|
||||||
|
getWallet(); // re-open wallet
|
||||||
|
}
|
||||||
|
|
||||||
private void startPolling() {
|
private void startPolling() {
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
if (isShutDownStarted || isPolling()) return;
|
if (isShutDownStarted || isPolling()) return;
|
||||||
updatePollPeriod();
|
updatePollPeriod();
|
||||||
pollLooper = new TaskLooper(() -> pollWallet());
|
pollLooper = new TaskLooper(() -> pollWallet());
|
||||||
@ -1791,15 +1727,15 @@ public class XmrWalletService {
|
|||||||
|
|
||||||
public void updatePollPeriod() {
|
public void updatePollPeriod() {
|
||||||
if (isShutDownStarted) return;
|
if (isShutDownStarted) return;
|
||||||
setPollPeriod(getPollPeriod());
|
setPollPeriodMs(getPollPeriodMs());
|
||||||
}
|
}
|
||||||
|
|
||||||
private long getPollPeriod() {
|
private long getPollPeriodMs() {
|
||||||
return xmrConnectionService.getRefreshPeriodMs();
|
return xmrConnectionService.getRefreshPeriodMs();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPollPeriod(long pollPeriodMs) {
|
private void setPollPeriodMs(long pollPeriodMs) {
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
if (this.isShutDownStarted) return;
|
if (this.isShutDownStarted) return;
|
||||||
if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return;
|
if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return;
|
||||||
this.pollPeriodMs = pollPeriodMs;
|
this.pollPeriodMs = pollPeriodMs;
|
||||||
@ -1834,32 +1770,36 @@ public class XmrWalletService {
|
|||||||
log.warn("Monero daemon is not synced within tolerance, height={}, targetHeight={}", xmrConnectionService.chainHeightProperty().get(), xmrConnectionService.getTargetHeight());
|
log.warn("Monero daemon is not synced within tolerance, height={}, targetHeight={}", xmrConnectionService.chainHeightProperty().get(), xmrConnectionService.getTargetHeight());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// switch to best connection if wallet is too far behind
|
|
||||||
if (wasWalletSynced && walletHeight.get() < xmrConnectionService.getTargetHeight() - NUM_BLOCKS_BEHIND_TOLERANCE && !Config.baseCurrencyNetwork().isTestnet()) {
|
|
||||||
log.warn("Updating connection because main wallet is {} blocks behind monerod, wallet height={}, monerod height={}", xmrConnectionService.getTargetHeight() - walletHeight.get(), walletHeight.get(), lastInfo.getHeight());
|
|
||||||
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
// sync wallet if behind daemon
|
// sync wallet if behind daemon
|
||||||
if (walletHeight.get() < xmrConnectionService.getTargetHeight()) {
|
if (walletHeight.get() < xmrConnectionService.getTargetHeight()) {
|
||||||
synchronized (WALLET_LOCK) { // avoid long sync from blocking other operations
|
synchronized (walletLock) { // avoid long sync from blocking other operations
|
||||||
syncMainWallet();
|
|
||||||
|
// TODO: local tests have timing failures unless sync called directly
|
||||||
|
if (xmrConnectionService.getTargetHeight() - walletHeight.get() < XmrWalletBase.DIRECT_SYNC_WITHIN_BLOCKS) {
|
||||||
|
syncMainWallet();
|
||||||
|
} else {
|
||||||
|
syncWithProgress();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch transactions from pool and store to cache
|
// fetch transactions from pool and store to cache
|
||||||
// TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs?
|
// TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs?
|
||||||
if (updateTxs) {
|
if (updateTxs) {
|
||||||
synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations
|
synchronized (walletLock) { // avoid long fetch from blocking other operations
|
||||||
synchronized (HavenoUtils.getDaemonLock()) {
|
synchronized (HavenoUtils.getDaemonLock()) {
|
||||||
|
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||||
try {
|
try {
|
||||||
cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true));
|
cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true));
|
||||||
|
lastPollTxsTimestamp = System.currentTimeMillis();
|
||||||
} catch (Exception e) { // fetch from pool can fail
|
} catch (Exception e) { // fetch from pool can fail
|
||||||
if (!isShutDownStarted) {
|
if (!isShutDownStarted) {
|
||||||
if (lastLogPollErrorTimestamp == null || System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) { // limit error logging
|
|
||||||
|
// throttle error handling
|
||||||
|
if (System.currentTimeMillis() - lastLogPollErrorTimestamp > HavenoUtils.LOG_POLL_ERROR_PERIOD_MS) {
|
||||||
log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage());
|
log.warn("Error polling main wallet's transactions from the pool: {}", e.getMessage());
|
||||||
lastLogPollErrorTimestamp = System.currentTimeMillis();
|
lastLogPollErrorTimestamp = System.currentTimeMillis();
|
||||||
|
if (System.currentTimeMillis() - lastPollTxsTimestamp > POLL_TXS_TOLERANCE_MS) requestSwitchToNextBestConnection(sourceConnection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1868,33 +1808,32 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (wallet == null || isShutDownStarted) return;
|
if (wallet == null || isShutDownStarted) return;
|
||||||
boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused");
|
if (HavenoUtils.isUnresponsive(e)) forceRestartMainWallet();
|
||||||
if (isConnectionRefused) forceRestartMainWallet();
|
|
||||||
else if (isWalletConnectedToDaemon()) {
|
else if (isWalletConnectedToDaemon()) {
|
||||||
log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getConnectionService().getConnection());
|
log.warn("Error polling main wallet, errorMessage={}. Monerod={}", e.getMessage(), getXmrConnectionService().getConnection());
|
||||||
//e.printStackTrace();
|
//e.printStackTrace();
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
synchronized (pollLock) {
|
||||||
|
pollInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
// cache wallet info last
|
// cache wallet info last
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
if (wallet != null && !isShutDownStarted) {
|
if (wallet != null && !isShutDownStarted) {
|
||||||
try {
|
try {
|
||||||
cacheWalletInfo();
|
cacheWalletInfo();
|
||||||
|
requestSaveMainWallet();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized (pollLock) {
|
|
||||||
pollInProgress = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private MoneroSyncResult syncMainWallet() {
|
private MoneroSyncResult syncMainWallet() {
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
MoneroSyncResult result = syncWallet(wallet);
|
MoneroSyncResult result = syncWallet(wallet);
|
||||||
walletHeight.set(wallet.getHeight());
|
walletHeight.set(wallet.getHeight());
|
||||||
return result;
|
return result;
|
||||||
@ -1902,7 +1841,7 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isWalletConnectedToDaemon() {
|
public boolean isWalletConnectedToDaemon() {
|
||||||
synchronized (WALLET_LOCK) {
|
synchronized (walletLock) {
|
||||||
try {
|
try {
|
||||||
if (wallet == null) return false;
|
if (wallet == null) return false;
|
||||||
return wallet.isConnectedToDaemon();
|
return wallet.isConnectedToDaemon();
|
||||||
@ -1912,8 +1851,12 @@ public class XmrWalletService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean requestSwitchToNextBestConnection() {
|
private boolean requestSwitchToNextBestConnection() {
|
||||||
return xmrConnectionService.requestSwitchToNextBestConnection();
|
return requestSwitchToNextBestConnection(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
|
||||||
|
return xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onNewBlock(long height) {
|
private void onNewBlock(long height) {
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
Binary file not shown.
@ -5,10 +5,10 @@
|
|||||||
<!-- See: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -->
|
<!-- See: https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -->
|
||||||
|
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.0.9</string>
|
<string>1.0.10</string>
|
||||||
|
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.0.9</string>
|
<string>1.0.10</string>
|
||||||
|
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>Haveno</string>
|
<string>Haveno</string>
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 169 KiB |
@ -197,7 +197,7 @@ public class TxIdTextField extends AnchorPane {
|
|||||||
try {
|
try {
|
||||||
if (trade == null) {
|
if (trade == null) {
|
||||||
tx = useCache ? xmrWalletService.getDaemonTxWithCache(txId) : xmrWalletService.getDaemonTx(txId);
|
tx = useCache ? xmrWalletService.getDaemonTxWithCache(txId) : xmrWalletService.getDaemonTx(txId);
|
||||||
tx.setNumConfirmations(tx.isConfirmed() ? (height == null ? xmrWalletService.getConnectionService().getLastInfo().getHeight() : height) - tx.getHeight(): 0l); // TODO: don't set if tx.getNumConfirmations() works reliably on non-local testnet
|
tx.setNumConfirmations(tx.isConfirmed() ? (height == null ? xmrWalletService.getXmrConnectionService().getLastInfo().getHeight() : height) - tx.getHeight(): 0l); // TODO: don't set if tx.getNumConfirmations() works reliably on non-local testnet
|
||||||
} else {
|
} else {
|
||||||
if (txId.equals(trade.getMaker().getDepositTxHash())) tx = trade.getMakerDepositTx();
|
if (txId.equals(trade.getMaker().getDepositTxHash())) tx = trade.getMakerDepositTx();
|
||||||
else if (txId.equals(trade.getTaker().getDepositTxHash())) tx = trade.getTakerDepositTx();
|
else if (txId.equals(trade.getTaker().getDepositTxHash())) tx = trade.getTakerDepositTx();
|
||||||
|
@ -1201,16 +1201,16 @@ textfield */
|
|||||||
}
|
}
|
||||||
|
|
||||||
.text-area-no-border {
|
.text-area-no-border {
|
||||||
-fx-border-color: -fx-control-inner-background;
|
-fx-border-color: -bs-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-area-no-border .content {
|
.text-area-no-border .content {
|
||||||
-fx-background-color: -fx-control-inner-background;
|
-fx-background-color: -bs-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-area-no-border:focused {
|
.text-area-no-border:focused {
|
||||||
-fx-focus-color: -fx-control-inner-background;
|
-fx-focus-color: -bs-background-color;
|
||||||
-fx-faint-focus-color: -fx-control-inner-background;
|
-fx-faint-focus-color: -bs-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
|
@ -72,6 +72,7 @@ import haveno.core.trade.HavenoUtils;
|
|||||||
import haveno.core.util.FormattingUtils;
|
import haveno.core.util.FormattingUtils;
|
||||||
import haveno.core.util.coin.CoinFormatter;
|
import haveno.core.util.coin.CoinFormatter;
|
||||||
import haveno.desktop.common.view.FxmlView;
|
import haveno.desktop.common.view.FxmlView;
|
||||||
|
import haveno.desktop.components.AutocompleteComboBox;
|
||||||
import haveno.desktop.components.TitledGroupBg;
|
import haveno.desktop.components.TitledGroupBg;
|
||||||
import haveno.desktop.components.paymentmethods.AchTransferForm;
|
import haveno.desktop.components.paymentmethods.AchTransferForm;
|
||||||
import haveno.desktop.components.paymentmethods.AdvancedCashForm;
|
import haveno.desktop.components.paymentmethods.AdvancedCashForm;
|
||||||
@ -144,7 +145,6 @@ import java.util.stream.Collectors;
|
|||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ComboBox;
|
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.ListView;
|
import javafx.scene.control.ListView;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
@ -181,7 +181,7 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
|
|||||||
private final AdvancedCashValidator advancedCashValidator;
|
private final AdvancedCashValidator advancedCashValidator;
|
||||||
private final TransferwiseValidator transferwiseValidator;
|
private final TransferwiseValidator transferwiseValidator;
|
||||||
private final CoinFormatter formatter;
|
private final CoinFormatter formatter;
|
||||||
private ComboBox<PaymentMethod> paymentMethodComboBox;
|
private AutocompleteComboBox<PaymentMethod> paymentMethodComboBox;
|
||||||
private PaymentMethodForm paymentMethodForm;
|
private PaymentMethodForm paymentMethodForm;
|
||||||
private TitledGroupBg accountTitledGroupBg;
|
private TitledGroupBg accountTitledGroupBg;
|
||||||
private Button saveNewAccountButton;
|
private Button saveNewAccountButton;
|
||||||
@ -463,14 +463,16 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
|
|||||||
removeAccountRows();
|
removeAccountRows();
|
||||||
addAccountButton.setDisable(true);
|
addAccountButton.setDisable(true);
|
||||||
accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 2, Res.get("shared.createNewAccount"), Layout.GROUP_DISTANCE);
|
accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 2, Res.get("shared.createNewAccount"), Layout.GROUP_DISTANCE);
|
||||||
paymentMethodComboBox = FormBuilder.addComboBox(root, gridRow, Res.get("shared.selectPaymentMethod"), Layout.FIRST_ROW_AND_GROUP_DISTANCE);
|
paymentMethodComboBox = FormBuilder.addAutocompleteComboBox(
|
||||||
paymentMethodComboBox.setVisibleRowCount(11);
|
root, gridRow, Res.get("shared.selectPaymentMethod"), Layout.FIRST_ROW_AND_GROUP_DISTANCE
|
||||||
|
);
|
||||||
|
paymentMethodComboBox.setVisibleRowCount(Math.min(paymentMethodComboBox.getItems().size(), 10));
|
||||||
paymentMethodComboBox.setPrefWidth(250);
|
paymentMethodComboBox.setPrefWidth(250);
|
||||||
List<PaymentMethod> list = PaymentMethod.paymentMethods.stream()
|
List<PaymentMethod> list = PaymentMethod.paymentMethods.stream()
|
||||||
.filter(PaymentMethod::isTraditional)
|
.filter(PaymentMethod::isTraditional)
|
||||||
.sorted()
|
.sorted()
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
paymentMethodComboBox.setItems(FXCollections.observableArrayList(list));
|
paymentMethodComboBox.setAutocompleteItems(FXCollections.observableArrayList(list));
|
||||||
paymentMethodComboBox.setConverter(new StringConverter<>() {
|
paymentMethodComboBox.setConverter(new StringConverter<>() {
|
||||||
@Override
|
@Override
|
||||||
public String toString(PaymentMethod paymentMethod) {
|
public String toString(PaymentMethod paymentMethod) {
|
||||||
@ -479,10 +481,15 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PaymentMethod fromString(String s) {
|
public PaymentMethod fromString(String s) {
|
||||||
return null;
|
if (s.isEmpty())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return paymentMethodComboBox.getItems().stream()
|
||||||
|
.filter(item -> Res.get(item.getId()).equals(s))
|
||||||
|
.findAny().orElse(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
paymentMethodComboBox.setOnAction(e -> {
|
paymentMethodComboBox.setOnChangeConfirmed(e -> {
|
||||||
if (paymentMethodForm != null) {
|
if (paymentMethodForm != null) {
|
||||||
FormBuilder.removeRowsFromGridPane(root, 3, paymentMethodForm.getGridRow() + 1);
|
FormBuilder.removeRowsFromGridPane(root, 3, paymentMethodForm.getGridRow() + 1);
|
||||||
GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1);
|
GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1);
|
||||||
@ -550,6 +557,7 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod) {
|
private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod) {
|
||||||
|
if (paymentMethod == null) return null;
|
||||||
final PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod);
|
final PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod);
|
||||||
paymentAccount.init();
|
paymentAccount.init();
|
||||||
return getPaymentMethodForm(paymentMethod, paymentAccount);
|
return getPaymentMethodForm(paymentMethod, paymentAccount);
|
||||||
|
@ -65,6 +65,7 @@ import javafx.scene.control.Button;
|
|||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
import monero.common.MoneroRpcConnection;
|
||||||
import monero.common.MoneroUtils;
|
import monero.common.MoneroUtils;
|
||||||
import monero.wallet.model.MoneroTxConfig;
|
import monero.wallet.model.MoneroTxConfig;
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
import monero.wallet.model.MoneroTxWallet;
|
||||||
@ -256,6 +257,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
|
|||||||
// create tx
|
// create tx
|
||||||
MoneroTxWallet tx = null;
|
MoneroTxWallet tx = null;
|
||||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||||
|
MoneroRpcConnection sourceConnection = xmrWalletService.getXmrConnectionService().getConnection();
|
||||||
try {
|
try {
|
||||||
log.info("Creating withdraw tx");
|
log.info("Creating withdraw tx");
|
||||||
long startTime = System.currentTimeMillis();
|
long startTime = System.currentTimeMillis();
|
||||||
@ -270,7 +272,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
|
|||||||
if (isNotEnoughMoney(e.getMessage())) throw e;
|
if (isNotEnoughMoney(e.getMessage())) throw e;
|
||||||
log.warn("Error creating creating withdraw tx, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
log.warn("Error creating creating withdraw tx, attempt={}/{}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
||||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||||
if (xmrWalletService.getConnectionService().isConnected()) xmrWalletService.requestSwitchToNextBestConnection();
|
if (xmrWalletService.getXmrConnectionService().isConnected()) xmrWalletService.requestSwitchToNextBestConnection(sourceConnection);
|
||||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -594,7 +594,11 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||||||
// show confirmation
|
// show confirmation
|
||||||
showPayoutTxConfirmation(contract,
|
showPayoutTxConfirmation(contract,
|
||||||
payoutTx,
|
payoutTx,
|
||||||
() -> doClose(closeTicketButton, cancelButton));
|
() -> doClose(closeTicketButton, cancelButton),
|
||||||
|
() -> {
|
||||||
|
closeTicketButton.setDisable(false);
|
||||||
|
cancelButton.setDisable(false);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
doClose(closeTicketButton, cancelButton);
|
doClose(closeTicketButton, cancelButton);
|
||||||
}
|
}
|
||||||
@ -607,7 +611,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showPayoutTxConfirmation(Contract contract, MoneroTxWallet payoutTx, ResultHandler resultHandler) {
|
private void showPayoutTxConfirmation(Contract contract, MoneroTxWallet payoutTx, ResultHandler resultHandler, ResultHandler cancelHandler) {
|
||||||
|
|
||||||
// get buyer and seller destinations (order not preserved)
|
// get buyer and seller destinations (order not preserved)
|
||||||
String buyerPayoutAddressString = contract.getBuyerPayoutAddressString();
|
String buyerPayoutAddressString = contract.getBuyerPayoutAddressString();
|
||||||
@ -641,6 +645,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||||||
.actionButtonText(Res.get("shared.yes"))
|
.actionButtonText(Res.get("shared.yes"))
|
||||||
.onAction(() -> resultHandler.handleResult())
|
.onAction(() -> resultHandler.handleResult())
|
||||||
.closeButtonText(Res.get("shared.cancel"))
|
.closeButtonText(Res.get("shared.cancel"))
|
||||||
|
.onClose(() -> cancelHandler.handleResult())
|
||||||
.show();
|
.show();
|
||||||
} else {
|
} else {
|
||||||
// No payout will be made
|
// No payout will be made
|
||||||
@ -649,6 +654,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||||||
.actionButtonText(Res.get("shared.yes"))
|
.actionButtonText(Res.get("shared.yes"))
|
||||||
.onAction(resultHandler::handleResult)
|
.onAction(resultHandler::handleResult)
|
||||||
.closeButtonText(Res.get("shared.cancel"))
|
.closeButtonText(Res.get("shared.cancel"))
|
||||||
|
.onClose(() -> cancelHandler.handleResult())
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,6 @@ import haveno.core.offer.OfferUtil;
|
|||||||
import haveno.core.payment.payload.PaymentAccountPayload;
|
import haveno.core.payment.payload.PaymentAccountPayload;
|
||||||
import haveno.core.support.SupportType;
|
import haveno.core.support.SupportType;
|
||||||
import haveno.core.support.dispute.Dispute;
|
import haveno.core.support.dispute.Dispute;
|
||||||
import haveno.core.support.dispute.DisputeAlreadyOpenException;
|
|
||||||
import haveno.core.support.dispute.DisputeList;
|
import haveno.core.support.dispute.DisputeList;
|
||||||
import haveno.core.support.dispute.DisputeManager;
|
import haveno.core.support.dispute.DisputeManager;
|
||||||
import haveno.core.support.dispute.arbitration.ArbitrationManager;
|
import haveno.core.support.dispute.arbitration.ArbitrationManager;
|
||||||
@ -67,6 +66,7 @@ import haveno.network.p2p.P2PService;
|
|||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
@ -545,32 +545,38 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
|||||||
dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData());
|
dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData());
|
||||||
|
|
||||||
trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED);
|
trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED);
|
||||||
sendDisputeOpenedMessage(dispute, false, disputeManager, trade.getSelf().getUpdatedMultisigHex());
|
sendDisputeOpenedMessage(dispute, disputeManager);
|
||||||
tradeManager.requestPersistence();
|
tradeManager.requestPersistence();
|
||||||
} else if (useArbitration) {
|
} else if (useArbitration) {
|
||||||
// Only if we have completed mediation we allow arbitration
|
// Only if we have completed mediation we allow arbitration
|
||||||
disputeManager = arbitrationManager;
|
disputeManager = arbitrationManager;
|
||||||
Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket);
|
Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket);
|
||||||
sendDisputeOpenedMessage(dispute, false, disputeManager, trade.getSelf().getUpdatedMultisigHex());
|
trade.exportMultisigHex();
|
||||||
|
sendDisputeOpenedMessage(dispute, disputeManager);
|
||||||
tradeManager.requestPersistence();
|
tradeManager.requestPersistence();
|
||||||
} else {
|
} else {
|
||||||
log.warn("Invalid dispute state {}", disputeState.name());
|
log.warn("Invalid dispute state {}", disputeState.name());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendDisputeOpenedMessage(Dispute dispute, boolean reOpen, DisputeManager<? extends DisputeList<Dispute>> disputeManager, String senderMultisigHex) {
|
private void sendDisputeOpenedMessage(Dispute dispute, DisputeManager<? extends DisputeList<Dispute>> disputeManager) {
|
||||||
disputeManager.sendDisputeOpenedMessage(dispute, reOpen, senderMultisigHex,
|
Optional<Dispute> optionalDispute = disputeManager.findDispute(dispute);
|
||||||
() -> navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class), (errorMessage, throwable) -> {
|
boolean disputeClosed = optionalDispute.isPresent() && optionalDispute.get().isClosed();
|
||||||
if ((throwable instanceof DisputeAlreadyOpenException)) {
|
if (disputeClosed) {
|
||||||
errorMessage += "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg");
|
String msg = "We got a dispute already open for that trade and trading peer.\n" + "TradeId = " + dispute.getTradeId();
|
||||||
new Popup().warning(errorMessage)
|
new Popup().warning(msg + "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg"))
|
||||||
.actionButtonText(Res.get("portfolio.pending.openAgainDispute.button"))
|
.actionButtonText(Res.get("portfolio.pending.openAgainDispute.button"))
|
||||||
.onAction(() -> sendDisputeOpenedMessage(dispute, true, disputeManager, senderMultisigHex))
|
.onAction(() -> doSendDisputeOpenedMessage(dispute, disputeManager))
|
||||||
.closeButtonText(Res.get("shared.cancel")).show();
|
.closeButtonText(Res.get("shared.cancel")).show();
|
||||||
} else {
|
} else {
|
||||||
new Popup().warning(errorMessage).show();
|
doSendDisputeOpenedMessage(dispute, disputeManager);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
private void doSendDisputeOpenedMessage(Dispute dispute, DisputeManager<? extends DisputeList<Dispute>> disputeManager) {
|
||||||
|
disputeManager.sendDisputeOpenedMessage(dispute,
|
||||||
|
() -> navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class),
|
||||||
|
(errorMessage, throwable) -> new Popup().warning(errorMessage).show());
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isReadyForTxBroadcast() {
|
public boolean isReadyForTxBroadcast() {
|
||||||
|
@ -190,15 +190,13 @@ public abstract class TradeStepView extends AnchorPane {
|
|||||||
}
|
}
|
||||||
trade.errorMessageProperty().addListener(errorMessageListener);
|
trade.errorMessageProperty().addListener(errorMessageListener);
|
||||||
|
|
||||||
if (!isMediationClosedState()) {
|
tradeStepInfo.setOnAction(e -> {
|
||||||
tradeStepInfo.setOnAction(e -> {
|
if (!isArbitrationOpenedState() && this.isTradePeriodOver()) {
|
||||||
if (this.isTradePeriodOver()) {
|
openSupportTicket();
|
||||||
openSupportTicket();
|
} else {
|
||||||
} else {
|
openChat();
|
||||||
openChat();
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// We get mailbox messages processed after we have bootstrapped. This will influence the states we
|
// We get mailbox messages processed after we have bootstrapped. This will influence the states we
|
||||||
// handle in our disputeStateSubscription and mediationResultStateSubscriptions. To avoid that we show
|
// handle in our disputeStateSubscription and mediationResultStateSubscriptions. To avoid that we show
|
||||||
@ -572,7 +570,7 @@ public abstract class TradeStepView extends AnchorPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateMediationResultState(boolean blockOpeningOfResultAcceptedPopup) {
|
private void updateMediationResultState(boolean blockOpeningOfResultAcceptedPopup) {
|
||||||
if (isInArbitration()) {
|
if (isInMediation()) {
|
||||||
if (isRefundRequestStartedByPeer()) {
|
if (isRefundRequestStartedByPeer()) {
|
||||||
tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED);
|
tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED);
|
||||||
} else if (isRefundRequestSelfStarted()) {
|
} else if (isRefundRequestSelfStarted()) {
|
||||||
@ -597,7 +595,7 @@ public abstract class TradeStepView extends AnchorPane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isInArbitration() {
|
private boolean isInMediation() {
|
||||||
return isRefundRequestStartedByPeer() || isRefundRequestSelfStarted();
|
return isRefundRequestStartedByPeer() || isRefundRequestSelfStarted();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -613,6 +611,10 @@ public abstract class TradeStepView extends AnchorPane {
|
|||||||
return trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED;
|
return trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isArbitrationOpenedState() {
|
||||||
|
return trade.getDisputeState().isOpen();
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isTradePeriodOver() {
|
private boolean isTradePeriodOver() {
|
||||||
return Trade.TradePeriodState.TRADE_PERIOD_OVER == trade.tradePeriodStateProperty().get();
|
return Trade.TradePeriodState.TRADE_PERIOD_OVER == trade.tradePeriodStateProperty().get();
|
||||||
}
|
}
|
||||||
@ -741,7 +743,7 @@ public abstract class TradeStepView extends AnchorPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateTradePeriodState(Trade.TradePeriodState tradePeriodState) {
|
private void updateTradePeriodState(Trade.TradePeriodState tradePeriodState) {
|
||||||
if (trade.getDisputeState() == Trade.DisputeState.NO_DISPUTE) {
|
if (!trade.getDisputeState().isOpen()) {
|
||||||
switch (tradePeriodState) {
|
switch (tradePeriodState) {
|
||||||
case FIRST_HALF:
|
case FIRST_HALF:
|
||||||
// just for dev testing. not possible to go back in time ;-)
|
// just for dev testing. not possible to go back in time ;-)
|
||||||
|
@ -1485,7 +1485,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCloseDisputeFromChatWindow(Dispute dispute) {
|
public void onCloseDisputeFromChatWindow(Dispute dispute) {
|
||||||
if (dispute.getDisputeState() == Dispute.State.NEW || dispute.getDisputeState() == Dispute.State.OPEN) {
|
if (dispute.getDisputeState() == Dispute.State.NEW || dispute.getDisputeState().isOpen()) {
|
||||||
handleOnProcessDispute(dispute);
|
handleOnProcessDispute(dispute);
|
||||||
} else {
|
} else {
|
||||||
closeDisputeFromButton();
|
closeDisputeFromButton();
|
||||||
|
@ -131,7 +131,7 @@
|
|||||||
-bs-red-soft: derive(-bs-rd-error-red, 60%);
|
-bs-red-soft: derive(-bs-rd-error-red, 60%);
|
||||||
-bs-progress-bar-track: #272728;
|
-bs-progress-bar-track: #272728;
|
||||||
-bs-chart-tick: rgba(255, 255, 255, 0.7);
|
-bs-chart-tick: rgba(255, 255, 255, 0.7);
|
||||||
-bs-chart-lines: rgba(0, 0, 0, 0.3);
|
-bs-chart-lines: -bs-color-gray-2;
|
||||||
-bs-white: white;
|
-bs-white: white;
|
||||||
-bs-prompt-text: -bs-color-gray-6;
|
-bs-prompt-text: -bs-color-gray-6;
|
||||||
-bs-decimals: #db6300;
|
-bs-decimals: #db6300;
|
||||||
|
@ -1405,6 +1405,24 @@ public class FormBuilder {
|
|||||||
return comboBox;
|
return comboBox;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> AutocompleteComboBox<T> addAutocompleteComboBox(GridPane gridPane, int rowIndex, String title, double top) {
|
||||||
|
var comboBox = new AutocompleteComboBox<T>();
|
||||||
|
comboBox.setLabelFloat(true);
|
||||||
|
comboBox.setPromptText(title);
|
||||||
|
comboBox.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
|
// Default ComboBox does not show promptText after clear selection.
|
||||||
|
// https://stackoverflow.com/questions/50569330/how-to-reset-combobox-and-display-prompttext?noredirect=1&lq=1
|
||||||
|
comboBox.setButtonCell(getComboBoxButtonCell(title, comboBox));
|
||||||
|
|
||||||
|
GridPane.setRowIndex(comboBox, rowIndex);
|
||||||
|
GridPane.setColumnIndex(comboBox, 0);
|
||||||
|
GridPane.setMargin(comboBox, new Insets(top + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0));
|
||||||
|
gridPane.getChildren().add(comboBox);
|
||||||
|
|
||||||
|
return comboBox;
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Label + AutocompleteComboBox
|
// Label + AutocompleteComboBox
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -701,7 +701,7 @@ public class GUIUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isReadyForTxBroadcastOrShowPopup(XmrWalletService xmrWalletService) {
|
public static boolean isReadyForTxBroadcastOrShowPopup(XmrWalletService xmrWalletService) {
|
||||||
XmrConnectionService xmrConnectionService = xmrWalletService.getConnectionService();
|
XmrConnectionService xmrConnectionService = xmrWalletService.getXmrConnectionService();
|
||||||
if (!xmrConnectionService.hasSufficientPeersForBroadcast()) {
|
if (!xmrConnectionService.hasSufficientPeersForBroadcast()) {
|
||||||
new Popup().information(Res.get("popup.warning.notSufficientConnectionsToXmrNetwork", xmrConnectionService.getMinBroadcastConnections())).show();
|
new Popup().information(Res.get("popup.warning.notSufficientConnectionsToXmrNetwork", xmrConnectionService.getMinBroadcastConnections())).show();
|
||||||
return false;
|
return false;
|
||||||
|
@ -79,17 +79,60 @@ Customize and deploy haveno-pricenode.env and haveno-pricenode.service to run as
|
|||||||
|
|
||||||
## Add seed nodes
|
## Add seed nodes
|
||||||
|
|
||||||
|
### Seed nodes without Proof of Work (PoW)
|
||||||
|
|
||||||
|
> [!note]
|
||||||
|
> Using PoW is suggested. See next section for PoW setup.
|
||||||
|
|
||||||
For each seed node:
|
For each seed node:
|
||||||
|
|
||||||
1. [Build the Haveno repository](#fork-and-build-haveno).
|
1. [Build the Haveno repository](#fork-and-build-haveno).
|
||||||
2. [Start a local Monero node](#start-a-local-monero-node).
|
2. [Start a local Monero node](#start-a-local-monero-node).
|
||||||
3. Modify `./scripts/deployment/haveno-seednode.service` and `./scripts/deployment/haveno-seednode2.service` as needed.
|
3. Modify `./scripts/deployment/haveno-seednode.service` and `./scripts/deployment/haveno-seednode2.service` as needed.
|
||||||
4. Copy `./scripts/deployment/haveno-seednode.service` to `/etc/systemd/system` (if you are the very first seed in a new network also copy `./scripts/deployment/haveno-seednode2.service` to `/etc/systemd/system`).
|
4. Copy `./scripts/deployment/haveno-seednode.service` to `/etc/systemd/system` (if you are the very first seed in a new network also copy `./scripts/deployment/haveno-seednode2.service` to `/etc/systemd/system`).
|
||||||
5. Run `sudo systemctl start haveno-seednode.service` to start the seednode and also run `sudo systemctl start haveno-seednode2.service` if you are the very first seed in a new network and coppied haveno-seednode2.service to your systemd folder.
|
5. Run `sudo systemctl start haveno-seednode.service` to start the seednode and also run `sudo systemctl start haveno-seednode2.service` if you are the very first seed in a new network and copied haveno-seednode2.service to your systemd folder.
|
||||||
6. Run `journalctl -u haveno-seednode.service -b -f` which will print the log and show the `.onion` address of the seed node. Press `Ctrl+C` to stop printing the log and record the `.onion` address given.
|
6. Run `journalctl -u haveno-seednode.service -b -f` which will print the log and show the `.onion` address of the seed node. Press `Ctrl+C` to stop printing the log and record the `.onion` address given.
|
||||||
7. Add the `.onion` address to `core/src/main/resources/xmr_<network>.seednodes` along with the port specified in the haveno-seednode.service file(s) `(ex: example.onion:1002)`. Be careful to record full addresses correctly.
|
7. Add the `.onion` address to `core/src/main/resources/xmr_<network>.seednodes` along with the port specified in the haveno-seednode.service file(s) `(ex: example.onion:1002)`. Be careful to record full addresses correctly.
|
||||||
8. Update all seed nodes, arbitrators, and user applications for the change to take effect.
|
8. Update all seed nodes, arbitrators, and user applications for the change to take effect.
|
||||||
|
|
||||||
|
### Seed nodes with Proof of Work (PoW)
|
||||||
|
|
||||||
|
> [!note]
|
||||||
|
> These instructions were written for Ubuntu with an Intel/AMD 64-bit CPU so changes may be needed for your distribution.
|
||||||
|
|
||||||
|
### Install Tor
|
||||||
|
|
||||||
|
Source: [Tor Project Support](https://support.torproject.org/apt/)
|
||||||
|
|
||||||
|
1. Verify architecture `sudo dpkg --print-architecture`.
|
||||||
|
2. Create sources.list file `sudo nano /etc/apt/sources.list.d/tor.list`.
|
||||||
|
3. Paste `deb [signed-by=/usr/share/keyrings/deb.torproject.org-keyring.gpg] https://deb.torproject.org/torproject.org <DISTRIBUTION> main`.
|
||||||
|
4. Paste `deb-src [signed-by=/usr/share/keyrings/deb.torproject.org-keyring.gpg] https://deb.torproject.org/torproject.org <DISTRIBUTION> main`.
|
||||||
|
> [!note]
|
||||||
|
> Replace `<DISTRIBUTION>` with your system codename such as "jammy" for Ubuntu 22.04.
|
||||||
|
5. Press Ctrl+X, then "y", then the enter key.
|
||||||
|
6. Add the gpg key used to sign the packages `sudo wget -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/deb.torproject.org-keyring.gpg >/dev/null`.
|
||||||
|
7. Update repositories `sudo apt update`.
|
||||||
|
8. Install tor and tor debian keyring `sudo apt install tor deb.torproject.org-keyring`.
|
||||||
|
9. Replace torrc `sudo mv /etc/tor/torrc /etc/tor/torrc.default` then `sudo cp seednode/torrc /etc/tor/torrc`.
|
||||||
|
10. Stop tor `sudo systemctl stop tor`.
|
||||||
|
|
||||||
|
For each seed node:
|
||||||
|
|
||||||
|
1. [Build the Haveno repository](#fork-and-build-haveno).
|
||||||
|
2. [Start a local Monero node](#start-a-local-monero-node).
|
||||||
|
3. Run `sudo cat /var/lib/tor/haveno_seednode/hostname` and note down the .onion for the next step & step 10.
|
||||||
|
4. Modify `./scripts/deployment/haveno-seednode.service` and `./scripts/deployment/haveno-seednode2.service` as needed.
|
||||||
|
5. Copy `./scripts/deployment/haveno-seednode.service` to `/etc/systemd/system` (if you are the very first seed in a new network also copy `./scripts/deployment/haveno-seednode2.service` to `/etc/systemd/system`).
|
||||||
|
6. Add user to tor group `sudo usermod -aG debian-tor <user>`.
|
||||||
|
> [!note]
|
||||||
|
> Replace `<user>` above with the user that will be running the seed node (step 6 above & step 4)
|
||||||
|
7. Disconnect and reconnect SSH session or logout and back in.
|
||||||
|
8. Run `sudo systemctl start tor`.
|
||||||
|
9. Run `sudo systemctl start haveno-seednode` to start the seednode and also run `sudo systemctl start haveno-seednode2` if you are the very first seed in a new network and copied haveno-seednode2.service to your systemd folder.
|
||||||
|
10. Add the `.onion` address from step 3 to `core/src/main/resources/xmr_<network>.seednodes` along with the port specified in the haveno-seednode.service file(s) `(ex: example.onion:2002)`. Be careful to record full addresses correctly.
|
||||||
|
11. Update all seed nodes, arbitrators, and user applications for the change to take effect.
|
||||||
|
|
||||||
Customize and deploy haveno-seednode.service to run a seed node as a system service.
|
Customize and deploy haveno-seednode.service to run a seed node as a system service.
|
||||||
|
|
||||||
Each seed node requires a locally running Monero node. You can use the default port or configure it manually with `--xmrNode`, `--xmrNodeUsername`, and `--xmrNodePassword`.
|
Each seed node requires a locally running Monero node. You can use the default port or configure it manually with `--xmrNode`, `--xmrNodeUsername`, and `--xmrNodePassword`.
|
||||||
|
@ -28,8 +28,10 @@ import haveno.network.p2p.network.LocalhostNetworkNode;
|
|||||||
import haveno.network.p2p.network.NetworkNode;
|
import haveno.network.p2p.network.NetworkNode;
|
||||||
import haveno.network.p2p.network.NewTor;
|
import haveno.network.p2p.network.NewTor;
|
||||||
import haveno.network.p2p.network.RunningTor;
|
import haveno.network.p2p.network.RunningTor;
|
||||||
|
import haveno.network.p2p.network.DirectBindTor;
|
||||||
import haveno.network.p2p.network.TorMode;
|
import haveno.network.p2p.network.TorMode;
|
||||||
import haveno.network.p2p.network.TorNetworkNode;
|
import haveno.network.p2p.network.TorNetworkNodeDirectBind;
|
||||||
|
import haveno.network.p2p.network.TorNetworkNodeNetlayer;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
@ -44,6 +46,7 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
|
|||||||
@Named(Config.MAX_CONNECTIONS) int maxConnections,
|
@Named(Config.MAX_CONNECTIONS) int maxConnections,
|
||||||
@Named(Config.USE_LOCALHOST_FOR_P2P) boolean useLocalhostForP2P,
|
@Named(Config.USE_LOCALHOST_FOR_P2P) boolean useLocalhostForP2P,
|
||||||
@Named(Config.NODE_PORT) int port,
|
@Named(Config.NODE_PORT) int port,
|
||||||
|
@Named(Config.HIDDEN_SERVICE_ADDRESS) String hiddenServiceAddress,
|
||||||
@Named(Config.TOR_DIR) File torDir,
|
@Named(Config.TOR_DIR) File torDir,
|
||||||
@Nullable @Named(Config.TORRC_FILE) File torrcFile,
|
@Nullable @Named(Config.TORRC_FILE) File torrcFile,
|
||||||
@Named(Config.TORRC_OPTIONS) String torrcOptions,
|
@Named(Config.TORRC_OPTIONS) String torrcOptions,
|
||||||
@ -62,10 +65,15 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
|
|||||||
torrcOptions,
|
torrcOptions,
|
||||||
controlHost,
|
controlHost,
|
||||||
controlPort,
|
controlPort,
|
||||||
|
hiddenServiceAddress,
|
||||||
password,
|
password,
|
||||||
cookieFile,
|
cookieFile,
|
||||||
useSafeCookieAuthentication);
|
useSafeCookieAuthentication);
|
||||||
networkNode = new TorNetworkNode(port, networkProtoResolver, streamIsolation, torMode, banFilter, maxConnections, controlHost);
|
if (torMode instanceof NewTor || torMode instanceof RunningTor) {
|
||||||
|
networkNode = new TorNetworkNodeNetlayer(port, networkProtoResolver, torMode, banFilter, maxConnections, streamIsolation, controlHost);
|
||||||
|
} else {
|
||||||
|
networkNode = new TorNetworkNodeDirectBind(port, networkProtoResolver, banFilter, maxConnections, hiddenServiceAddress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,12 +83,17 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
|
|||||||
String torrcOptions,
|
String torrcOptions,
|
||||||
String controlHost,
|
String controlHost,
|
||||||
int controlPort,
|
int controlPort,
|
||||||
|
String hiddenServiceAddress,
|
||||||
String password,
|
String password,
|
||||||
@Nullable File cookieFile,
|
@Nullable File cookieFile,
|
||||||
boolean useSafeCookieAuthentication) {
|
boolean useSafeCookieAuthentication) {
|
||||||
return controlPort != Config.UNSPECIFIED_PORT ?
|
if (!hiddenServiceAddress.equals("")) {
|
||||||
new RunningTor(torDir, controlHost, controlPort, password, cookieFile, useSafeCookieAuthentication) :
|
return new DirectBindTor();
|
||||||
new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider);
|
} else if (controlPort != Config.UNSPECIFIED_PORT) {
|
||||||
|
return new RunningTor(torDir, controlHost, controlPort, password, cookieFile, useSafeCookieAuthentication);
|
||||||
|
} else {
|
||||||
|
return new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -26,6 +26,7 @@ import haveno.common.config.Config;
|
|||||||
import static haveno.common.config.Config.BAN_LIST;
|
import static haveno.common.config.Config.BAN_LIST;
|
||||||
import static haveno.common.config.Config.MAX_CONNECTIONS;
|
import static haveno.common.config.Config.MAX_CONNECTIONS;
|
||||||
import static haveno.common.config.Config.NODE_PORT;
|
import static haveno.common.config.Config.NODE_PORT;
|
||||||
|
import static haveno.common.config.Config.HIDDEN_SERVICE_ADDRESS;
|
||||||
import static haveno.common.config.Config.REPUBLISH_MAILBOX_ENTRIES;
|
import static haveno.common.config.Config.REPUBLISH_MAILBOX_ENTRIES;
|
||||||
import static haveno.common.config.Config.SOCKS_5_PROXY_HTTP_ADDRESS;
|
import static haveno.common.config.Config.SOCKS_5_PROXY_HTTP_ADDRESS;
|
||||||
import static haveno.common.config.Config.SOCKS_5_PROXY_XMR_ADDRESS;
|
import static haveno.common.config.Config.SOCKS_5_PROXY_XMR_ADDRESS;
|
||||||
@ -87,6 +88,7 @@ public class P2PModule extends AppModule {
|
|||||||
bind(File.class).annotatedWith(named(TOR_DIR)).toInstance(config.torDir);
|
bind(File.class).annotatedWith(named(TOR_DIR)).toInstance(config.torDir);
|
||||||
|
|
||||||
bind(int.class).annotatedWith(named(NODE_PORT)).toInstance(config.nodePort);
|
bind(int.class).annotatedWith(named(NODE_PORT)).toInstance(config.nodePort);
|
||||||
|
bind(String.class).annotatedWith(named(HIDDEN_SERVICE_ADDRESS)).toInstance(config.hiddenServiceAddress);
|
||||||
|
|
||||||
bindConstant().annotatedWith(named(MAX_CONNECTIONS)).to(config.maxConnections);
|
bindConstant().annotatedWith(named(MAX_CONNECTIONS)).to(config.maxConnections);
|
||||||
|
|
||||||
|
@ -171,9 +171,11 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
private final Capabilities capabilities = new Capabilities();
|
private final Capabilities capabilities = new Capabilities();
|
||||||
|
|
||||||
// throttle logs of reported invalid requests
|
// throttle logs of reported invalid requests
|
||||||
private static long lastLoggedInvalidRequestReport = 0;
|
private static final long LOG_THROTTLE_INTERVAL_MS = 30000; // throttle logging rule violations and warnings to once every 30 seconds
|
||||||
private static int unloggedInvalidRequestReports = 0;
|
private static long lastLoggedInvalidRequestReportTs = 0;
|
||||||
private static final long LOG_INVALID_REQUEST_REPORTS_INTERVAL_MS = 60000; // log invalid request reports once every 60s
|
private static int numUnloggedInvalidRequestReports = 0;
|
||||||
|
private static long lastLoggedWarningTs = 0;
|
||||||
|
private static int numUnloggedWarnings = 0;
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Constructor
|
// Constructor
|
||||||
@ -218,8 +220,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
if (peersNodeAddress != null) {
|
if (peersNodeAddress != null) {
|
||||||
setPeersNodeAddress(peersNodeAddress);
|
setPeersNodeAddress(peersNodeAddress);
|
||||||
if (banFilter != null && banFilter.isPeerBanned(peersNodeAddress)) {
|
if (banFilter != null && banFilter.isPeerBanned(peersNodeAddress)) {
|
||||||
log.warn("We created an outbound connection with a banned peer");
|
reportInvalidRequest(RuleViolation.PEER_BANNED, "We created an outbound connection with a banned peer");
|
||||||
reportInvalidRequest(RuleViolation.PEER_BANNED);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ThreadUtils.execute(() -> connectionListener.onConnection(this), THREAD_ID);
|
ThreadUtils.execute(() -> connectionListener.onConnection(this), THREAD_ID);
|
||||||
@ -249,8 +250,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
if (banFilter != null &&
|
if (banFilter != null &&
|
||||||
peersNodeAddressOptional.isPresent() &&
|
peersNodeAddressOptional.isPresent() &&
|
||||||
banFilter.isPeerBanned(peersNodeAddressOptional.get())) {
|
banFilter.isPeerBanned(peersNodeAddressOptional.get())) {
|
||||||
log.warn("We tried to send a message to a banned peer. message={}", networkEnvelope.getClass().getSimpleName());
|
String errorMessage = "We tried to send a message to a banned peer. message=" + networkEnvelope.getClass().getSimpleName();
|
||||||
reportInvalidRequest(RuleViolation.PEER_BANNED);
|
reportInvalidRequest(RuleViolation.PEER_BANNED, errorMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -419,7 +420,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
if (networkEnvelope instanceof SendersNodeAddressMessage) {
|
if (networkEnvelope instanceof SendersNodeAddressMessage) {
|
||||||
boolean isValid = processSendersNodeAddressMessage((SendersNodeAddressMessage) networkEnvelope);
|
boolean isValid = processSendersNodeAddressMessage((SendersNodeAddressMessage) networkEnvelope);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
log.warn("Received an invalid {} at processing BundleOfEnvelopes", networkEnvelope.getClass().getSimpleName());
|
throttleWarn("Received an invalid " + networkEnvelope.getClass().getSimpleName() + " at processing BundleOfEnvelopes");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -610,20 +611,20 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
* Runs in same thread as Connection
|
* Runs in same thread as Connection
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public boolean reportInvalidRequest(RuleViolation ruleViolation) {
|
public boolean reportInvalidRequest(RuleViolation ruleViolation, String errorMessage) {
|
||||||
return Connection.reportInvalidRequest(this, ruleViolation);
|
return Connection.reportInvalidRequest(this, ruleViolation, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static synchronized boolean reportInvalidRequest(Connection connection, RuleViolation ruleViolation) {
|
private static synchronized boolean reportInvalidRequest(Connection connection, RuleViolation ruleViolation, String errorMessage) {
|
||||||
|
|
||||||
// determine if report should be logged to avoid spamming the logs
|
// determine if report should be logged to avoid spamming the logs
|
||||||
boolean logReport = System.currentTimeMillis() - lastLoggedInvalidRequestReport > LOG_INVALID_REQUEST_REPORTS_INTERVAL_MS;
|
boolean logReport = System.currentTimeMillis() - lastLoggedInvalidRequestReportTs > LOG_THROTTLE_INTERVAL_MS;
|
||||||
|
|
||||||
// count the number of unlogged reports since last log entry
|
// count the number of unlogged reports since last log entry
|
||||||
if (!logReport) unloggedInvalidRequestReports++;
|
if (!logReport) numUnloggedInvalidRequestReports++;
|
||||||
|
|
||||||
// handle report
|
// handle report
|
||||||
if (logReport) log.info("We got reported the ruleViolation {} at connection with address {} and uid {}", ruleViolation, connection.getPeersNodeAddressProperty(), connection.getUid());
|
if (logReport) log.warn("We got reported the ruleViolation {} at connection with address={}, uid={}, errorMessage={}", ruleViolation, connection.getPeersNodeAddressProperty(), connection.getUid(), errorMessage);
|
||||||
int numRuleViolations;
|
int numRuleViolations;
|
||||||
numRuleViolations = connection.ruleViolations.getOrDefault(ruleViolation, 0);
|
numRuleViolations = connection.ruleViolations.getOrDefault(ruleViolation, 0);
|
||||||
numRuleViolations++;
|
numRuleViolations++;
|
||||||
@ -654,9 +655,9 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
|
|
||||||
private static synchronized void resetReportedInvalidRequestsThrottle(boolean logReport) {
|
private static synchronized void resetReportedInvalidRequestsThrottle(boolean logReport) {
|
||||||
if (logReport) {
|
if (logReport) {
|
||||||
if (unloggedInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", unloggedInvalidRequestReports);
|
if (numUnloggedInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", numUnloggedInvalidRequestReports);
|
||||||
unloggedInvalidRequestReports = 0;
|
numUnloggedInvalidRequestReports = 0;
|
||||||
lastLoggedInvalidRequestReport = System.currentTimeMillis();
|
lastLoggedInvalidRequestReportTs = System.currentTimeMillis();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -673,25 +674,22 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
else
|
else
|
||||||
closeConnectionReason = CloseConnectionReason.RESET;
|
closeConnectionReason = CloseConnectionReason.RESET;
|
||||||
|
|
||||||
log.info("SocketException (expected if connection lost). closeConnectionReason={}; connection={}", closeConnectionReason, this);
|
throttleWarn("SocketException (expected if connection lost). closeConnectionReason=" + closeConnectionReason + "; connection=" + this);
|
||||||
} else if (e instanceof SocketTimeoutException || e instanceof TimeoutException) {
|
} else if (e instanceof SocketTimeoutException || e instanceof TimeoutException) {
|
||||||
closeConnectionReason = CloseConnectionReason.SOCKET_TIMEOUT;
|
closeConnectionReason = CloseConnectionReason.SOCKET_TIMEOUT;
|
||||||
log.info("Shut down caused by exception {} on connection={}", e, this);
|
throttleWarn("Shut down caused by exception " + e.getMessage() + " on connection=" + this);
|
||||||
} else if (e instanceof EOFException) {
|
} else if (e instanceof EOFException) {
|
||||||
closeConnectionReason = CloseConnectionReason.TERMINATED;
|
closeConnectionReason = CloseConnectionReason.TERMINATED;
|
||||||
log.warn("Shut down caused by exception {} on connection={}", e, this);
|
throttleWarn("Shut down caused by exception " + e.getMessage() + " on connection=" + this);
|
||||||
} else if (e instanceof OptionalDataException || e instanceof StreamCorruptedException) {
|
} else if (e instanceof OptionalDataException || e instanceof StreamCorruptedException) {
|
||||||
closeConnectionReason = CloseConnectionReason.CORRUPTED_DATA;
|
closeConnectionReason = CloseConnectionReason.CORRUPTED_DATA;
|
||||||
log.warn("Shut down caused by exception {} on connection={}", e, this);
|
throttleWarn("Shut down caused by exception " + e.getMessage() + " on connection=" + this);
|
||||||
} else {
|
} else {
|
||||||
// TODO sometimes we get StreamCorruptedException, OptionalDataException, IllegalStateException
|
// TODO sometimes we get StreamCorruptedException, OptionalDataException, IllegalStateException
|
||||||
closeConnectionReason = CloseConnectionReason.UNKNOWN_EXCEPTION;
|
closeConnectionReason = CloseConnectionReason.UNKNOWN_EXCEPTION;
|
||||||
log.warn("Unknown reason for exception at socket: {}\n\t" +
|
throttleWarn("Unknown reason for exception at socket: " + socket.toString() + "\n\t" +
|
||||||
"peer={}\n\t" +
|
"peer=" + this.peersNodeAddressOptional + "\n\t" +
|
||||||
"Exception={}",
|
"Exception=" + e.toString());
|
||||||
socket.toString(),
|
|
||||||
this.peersNodeAddressOptional,
|
|
||||||
e.toString());
|
|
||||||
}
|
}
|
||||||
shutDown(closeConnectionReason);
|
shutDown(closeConnectionReason);
|
||||||
}
|
}
|
||||||
@ -712,8 +710,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (banFilter != null && banFilter.isPeerBanned(senderNodeAddress)) {
|
if (banFilter != null && banFilter.isPeerBanned(senderNodeAddress)) {
|
||||||
log.warn("We got a message from a banned peer. message={}", sendersNodeAddressMessage.getClass().getSimpleName());
|
String errorMessage = "We got a message from a banned peer. message=" + sendersNodeAddressMessage.getClass().getSimpleName();
|
||||||
reportInvalidRequest(RuleViolation.PEER_BANNED);
|
reportInvalidRequest(RuleViolation.PEER_BANNED, errorMessage);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -745,7 +743,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
try {
|
try {
|
||||||
if (socket != null &&
|
if (socket != null &&
|
||||||
socket.isClosed()) {
|
socket.isClosed()) {
|
||||||
log.warn("Socket is null or closed socket={}", socket);
|
throttleWarn("Socket is null or closed socket=" + socket);
|
||||||
shutDown(CloseConnectionReason.SOCKET_CLOSED);
|
shutDown(CloseConnectionReason.SOCKET_CLOSED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -757,7 +755,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
|
|
||||||
if (socket != null &&
|
if (socket != null &&
|
||||||
socket.isClosed()) {
|
socket.isClosed()) {
|
||||||
log.warn("Socket is null or closed socket={}", socket);
|
throttleWarn("Socket is null or closed socket=" + socket);
|
||||||
shutDown(CloseConnectionReason.SOCKET_CLOSED);
|
shutDown(CloseConnectionReason.SOCKET_CLOSED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -767,9 +765,9 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (protoInputStream.read() == -1) {
|
if (protoInputStream.read() == -1) {
|
||||||
log.warn("proto is null because protoInputStream.read()=-1 (EOF). That is expected if client got stopped without proper shutdown.");
|
throttleWarn("proto is null because protoInputStream.read()=-1 (EOF). That is expected if client got stopped without proper shutdown.");
|
||||||
} else {
|
} else {
|
||||||
log.warn("proto is null. protoInputStream.read()=" + protoInputStream.read());
|
throttleWarn("proto is null. protoInputStream.read()=" + protoInputStream.read());
|
||||||
}
|
}
|
||||||
shutDown(CloseConnectionReason.NO_PROTO_BUFFER_ENV);
|
shutDown(CloseConnectionReason.NO_PROTO_BUFFER_ENV);
|
||||||
return;
|
return;
|
||||||
@ -778,8 +776,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
if (banFilter != null &&
|
if (banFilter != null &&
|
||||||
peersNodeAddressOptional.isPresent() &&
|
peersNodeAddressOptional.isPresent() &&
|
||||||
banFilter.isPeerBanned(peersNodeAddressOptional.get())) {
|
banFilter.isPeerBanned(peersNodeAddressOptional.get())) {
|
||||||
log.warn("We got a message from a banned peer. proto={}", Utilities.toTruncatedString(proto));
|
String errorMessage = "We got a message from a banned peer. proto=" + Utilities.toTruncatedString(proto);
|
||||||
reportInvalidRequest(RuleViolation.PEER_BANNED);
|
reportInvalidRequest(RuleViolation.PEER_BANNED, errorMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -814,30 +812,28 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
|
|
||||||
if (networkEnvelope instanceof AddPersistableNetworkPayloadMessage &&
|
if (networkEnvelope instanceof AddPersistableNetworkPayloadMessage &&
|
||||||
!((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().verifyHashSize()) {
|
!((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().verifyHashSize()) {
|
||||||
log.warn("PersistableNetworkPayload.verifyHashSize failed. hashSize={}; object={}",
|
String errorMessage = "PersistableNetworkPayload.verifyHashSize failed. hashSize=" +
|
||||||
((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().getHash().length,
|
((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().getHash().length + "; object=" +
|
||||||
Utilities.toTruncatedString(proto));
|
Utilities.toTruncatedString(proto);
|
||||||
if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED))
|
if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED, errorMessage))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exceeds) {
|
if (exceeds) {
|
||||||
log.warn("size > MAX_MSG_SIZE. size={}; object={}", size, Utilities.toTruncatedString(proto));
|
String errorMessage = "size > MAX_MSG_SIZE. size=" + size + "; object=" + Utilities.toTruncatedString(proto);
|
||||||
|
if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED, errorMessage))
|
||||||
if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED))
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (violatesThrottleLimit() && reportInvalidRequest(RuleViolation.THROTTLE_LIMIT_EXCEEDED))
|
if (violatesThrottleLimit() && reportInvalidRequest(RuleViolation.THROTTLE_LIMIT_EXCEEDED, "Violates throttle limit"))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Check P2P network ID
|
// Check P2P network ID
|
||||||
|
String errorMessage = "RuleViolation.WRONG_NETWORK_ID. version of message=" + proto.getMessageVersion() +
|
||||||
|
", app version=" + Version.getP2PMessageVersion() +
|
||||||
|
", proto.toTruncatedString=" + Utilities.toTruncatedString(proto.toString());
|
||||||
if (!proto.getMessageVersion().equals(Version.getP2PMessageVersion())
|
if (!proto.getMessageVersion().equals(Version.getP2PMessageVersion())
|
||||||
&& reportInvalidRequest(RuleViolation.WRONG_NETWORK_ID)) {
|
&& reportInvalidRequest(RuleViolation.WRONG_NETWORK_ID, errorMessage)) {
|
||||||
log.warn("RuleViolation.WRONG_NETWORK_ID. version of message={}, app version={}, " +
|
|
||||||
"proto.toTruncatedString={}", proto.getMessageVersion(),
|
|
||||||
Version.getP2PMessageVersion(),
|
|
||||||
Utilities.toTruncatedString(proto.toString()));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -879,12 +875,9 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
ThreadUtils.execute(() -> connectionStatistics.addReceivedMsgMetrics(System.currentTimeMillis() - ts, size), THREAD_ID);
|
ThreadUtils.execute(() -> connectionStatistics.addReceivedMsgMetrics(System.currentTimeMillis() - ts, size), THREAD_ID);
|
||||||
}
|
}
|
||||||
} catch (InvalidClassException e) {
|
} catch (InvalidClassException e) {
|
||||||
log.error(e.getMessage());
|
reportInvalidRequest(RuleViolation.INVALID_CLASS, e.getMessage());
|
||||||
e.printStackTrace();
|
|
||||||
reportInvalidRequest(RuleViolation.INVALID_CLASS);
|
|
||||||
} catch (ProtobufferException | NoClassDefFoundError | InvalidProtocolBufferException e) {
|
} catch (ProtobufferException | NoClassDefFoundError | InvalidProtocolBufferException e) {
|
||||||
log.error(e.getMessage());
|
reportInvalidRequest(RuleViolation.INVALID_DATA_TYPE, e.getMessage());
|
||||||
reportInvalidRequest(RuleViolation.INVALID_DATA_TYPE);
|
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
handleException(t);
|
handleException(t);
|
||||||
}
|
}
|
||||||
@ -943,4 +936,16 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
|||||||
NodeAddress nodeAddress = getSenderNodeAddress(networkEnvelope);
|
NodeAddress nodeAddress = getSenderNodeAddress(networkEnvelope);
|
||||||
return nodeAddress == null ? "null" : nodeAddress.getFullAddress();
|
return nodeAddress == null ? "null" : nodeAddress.getFullAddress();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private synchronized void throttleWarn(String msg) {
|
||||||
|
boolean logWarning = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS;
|
||||||
|
if (logWarning) {
|
||||||
|
log.warn(msg);
|
||||||
|
if (numUnloggedWarnings > 0) log.warn("We received {} other log warnings since the last log entry", numUnloggedWarnings);
|
||||||
|
numUnloggedWarnings = 0;
|
||||||
|
lastLoggedWarningTs = System.currentTimeMillis();
|
||||||
|
} else {
|
||||||
|
numUnloggedWarnings++;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
package haveno.network.p2p.network;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.berndpruenster.netlayer.tor.Tor;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class DirectBindTor extends TorMode {
|
||||||
|
|
||||||
|
public DirectBindTor() {
|
||||||
|
super(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Tor getTor() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHiddenServiceDirectory() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -18,50 +18,26 @@
|
|||||||
package haveno.network.p2p.network;
|
package haveno.network.p2p.network;
|
||||||
|
|
||||||
import haveno.network.p2p.NodeAddress;
|
import haveno.network.p2p.NodeAddress;
|
||||||
import haveno.network.utils.Utils;
|
|
||||||
|
|
||||||
import haveno.common.Timer;
|
|
||||||
import haveno.common.UserThread;
|
|
||||||
import haveno.common.proto.network.NetworkProtoResolver;
|
import haveno.common.proto.network.NetworkProtoResolver;
|
||||||
import haveno.common.util.SingleThreadExecutorUtils;
|
import haveno.common.util.SingleThreadExecutorUtils;
|
||||||
|
|
||||||
import org.berndpruenster.netlayer.tor.HiddenServiceSocket;
|
|
||||||
import org.berndpruenster.netlayer.tor.Tor;
|
|
||||||
import org.berndpruenster.netlayer.tor.TorCtlException;
|
|
||||||
import org.berndpruenster.netlayer.tor.TorSocket;
|
|
||||||
|
|
||||||
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
|
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkArgument;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class TorNetworkNode extends NetworkNode {
|
public abstract class TorNetworkNode extends NetworkNode {
|
||||||
private static final long SHUT_DOWN_TIMEOUT = 2;
|
|
||||||
|
|
||||||
private final String torControlHost;
|
protected final ExecutorService executor;
|
||||||
|
|
||||||
private HiddenServiceSocket hiddenServiceSocket;
|
|
||||||
private Timer shutDownTimeoutTimer;
|
|
||||||
private Tor tor;
|
|
||||||
private TorMode torMode;
|
|
||||||
private boolean streamIsolation;
|
|
||||||
private Socks5Proxy socksProxy;
|
|
||||||
private boolean shutDownInProgress;
|
|
||||||
private boolean shutDownComplete;
|
|
||||||
private final ExecutorService executor;
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Constructor
|
// Constructor
|
||||||
@ -69,15 +45,9 @@ public class TorNetworkNode extends NetworkNode {
|
|||||||
|
|
||||||
public TorNetworkNode(int servicePort,
|
public TorNetworkNode(int servicePort,
|
||||||
NetworkProtoResolver networkProtoResolver,
|
NetworkProtoResolver networkProtoResolver,
|
||||||
boolean useStreamIsolation,
|
|
||||||
TorMode torMode,
|
|
||||||
@Nullable BanFilter banFilter,
|
@Nullable BanFilter banFilter,
|
||||||
int maxConnections, String torControlHost) {
|
int maxConnections) {
|
||||||
super(servicePort, networkProtoResolver, banFilter, maxConnections);
|
super(servicePort, networkProtoResolver, banFilter, maxConnections);
|
||||||
this.torMode = torMode;
|
|
||||||
this.streamIsolation = useStreamIsolation;
|
|
||||||
this.torControlHost = torControlHost;
|
|
||||||
|
|
||||||
executor = SingleThreadExecutorUtils.getSingleThreadExecutor("StartTor");
|
executor = SingleThreadExecutorUtils.getSingleThreadExecutor("StartTor");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,121 +57,19 @@ public class TorNetworkNode extends NetworkNode {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start(@Nullable SetupListener setupListener) {
|
public void start(@Nullable SetupListener setupListener) {
|
||||||
torMode.doRollingBackup();
|
|
||||||
|
|
||||||
if (setupListener != null)
|
if (setupListener != null)
|
||||||
addSetupListener(setupListener);
|
addSetupListener(setupListener);
|
||||||
|
|
||||||
createTorAndHiddenService(Utils.findFreeSystemPort(), servicePort);
|
createTorAndHiddenService();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Socket createSocket(NodeAddress peerNodeAddress) throws IOException {
|
|
||||||
checkArgument(peerNodeAddress.getHostName().endsWith(".onion"), "PeerAddress is not an onion address");
|
|
||||||
// If streamId is null stream isolation gets deactivated.
|
|
||||||
// Hidden services use stream isolation by default, so we pass null.
|
|
||||||
return new TorSocket(peerNodeAddress.getHostName(), peerNodeAddress.getPort(), torControlHost, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Socks5Proxy getSocksProxy() {
|
|
||||||
try {
|
|
||||||
String stream = null;
|
|
||||||
if (streamIsolation) {
|
|
||||||
byte[] bytes = new byte[512]; // tor.getProxy creates a Sha256 hash
|
|
||||||
new SecureRandom().nextBytes(bytes);
|
|
||||||
stream = Base64.getEncoder().encodeToString(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (socksProxy == null || streamIsolation) {
|
|
||||||
tor = Tor.getDefault();
|
|
||||||
socksProxy = tor != null ? tor.getProxy(torControlHost, stream) : null;
|
|
||||||
}
|
|
||||||
return socksProxy;
|
|
||||||
} catch (Throwable t) {
|
|
||||||
log.error("Error at getSocksProxy", t);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutDown(@Nullable Runnable shutDownCompleteHandler) {
|
public void shutDown(@Nullable Runnable shutDownCompleteHandler) {
|
||||||
log.info("TorNetworkNode shutdown started");
|
super.shutDown(shutDownCompleteHandler);
|
||||||
if (shutDownComplete) {
|
|
||||||
log.info("TorNetworkNode shutdown already completed");
|
|
||||||
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (shutDownInProgress) {
|
|
||||||
log.warn("Ignoring request to shut down because shut down is in progress");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
shutDownInProgress = true;
|
|
||||||
|
|
||||||
shutDownTimeoutTimer = UserThread.runAfter(() -> {
|
|
||||||
log.error("A timeout occurred at shutDown");
|
|
||||||
shutDownComplete = true;
|
|
||||||
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
|
|
||||||
executor.shutdownNow();
|
|
||||||
}, SHUT_DOWN_TIMEOUT);
|
|
||||||
|
|
||||||
super.shutDown(() -> {
|
|
||||||
try {
|
|
||||||
tor = Tor.getDefault();
|
|
||||||
if (tor != null) {
|
|
||||||
tor.shutdown();
|
|
||||||
tor = null;
|
|
||||||
log.info("Tor shutdown completed");
|
|
||||||
}
|
|
||||||
executor.shutdownNow();
|
|
||||||
} catch (Throwable e) {
|
|
||||||
log.error("Shutdown torNetworkNode failed with exception", e);
|
|
||||||
} finally {
|
|
||||||
shutDownTimeoutTimer.stop();
|
|
||||||
shutDownComplete = true;
|
|
||||||
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
public abstract Socks5Proxy getSocksProxy();
|
||||||
// Create tor and hidden service
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
private void createTorAndHiddenService(int localPort, int servicePort) {
|
protected abstract Socket createSocket(NodeAddress peerNodeAddress) throws IOException;
|
||||||
executor.submit(() -> {
|
|
||||||
try {
|
protected abstract void createTorAndHiddenService();
|
||||||
Tor.setDefault(torMode.getTor());
|
|
||||||
long ts = System.currentTimeMillis();
|
|
||||||
hiddenServiceSocket = new HiddenServiceSocket(localPort, torMode.getHiddenServiceDirectory(), servicePort);
|
|
||||||
nodeAddressProperty.set(new NodeAddress(hiddenServiceSocket.getServiceName() + ":" + hiddenServiceSocket.getHiddenServicePort()));
|
|
||||||
UserThread.execute(() -> setupListeners.forEach(SetupListener::onTorNodeReady));
|
|
||||||
hiddenServiceSocket.addReadyListener(socket -> {
|
|
||||||
log.info("\n################################################################\n" +
|
|
||||||
"Tor hidden service published after {} ms. Socket={}\n" +
|
|
||||||
"################################################################",
|
|
||||||
System.currentTimeMillis() - ts, socket);
|
|
||||||
UserThread.execute(() -> {
|
|
||||||
nodeAddressProperty.set(new NodeAddress(hiddenServiceSocket.getServiceName() + ":"
|
|
||||||
+ hiddenServiceSocket.getHiddenServicePort()));
|
|
||||||
startServer(socket);
|
|
||||||
setupListeners.forEach(SetupListener::onHiddenServicePublished);
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
} catch (TorCtlException e) {
|
|
||||||
log.error("Starting tor node failed", e);
|
|
||||||
if (e.getCause() instanceof IOException) {
|
|
||||||
UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage()))));
|
|
||||||
} else {
|
|
||||||
UserThread.execute(() -> setupListeners.forEach(SetupListener::onRequestCustomBridges));
|
|
||||||
log.warn("We shutdown as starting tor with the default bridges failed. We request user to add custom bridges.");
|
|
||||||
shutDown(null);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("Could not connect to running Tor", e);
|
|
||||||
UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage()))));
|
|
||||||
} catch (Throwable ignore) {
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,114 @@
|
|||||||
|
package haveno.network.p2p.network;
|
||||||
|
|
||||||
|
import haveno.common.util.Hex;
|
||||||
|
import haveno.network.p2p.NodeAddress;
|
||||||
|
|
||||||
|
import haveno.common.UserThread;
|
||||||
|
import haveno.common.proto.network.NetworkProtoResolver;
|
||||||
|
|
||||||
|
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
|
||||||
|
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class TorNetworkNodeDirectBind extends TorNetworkNode {
|
||||||
|
|
||||||
|
private static final int TOR_DATA_PORT = 9050; // TODO: config option?
|
||||||
|
private final String serviceAddress;
|
||||||
|
|
||||||
|
public TorNetworkNodeDirectBind(int servicePort,
|
||||||
|
NetworkProtoResolver networkProtoResolver,
|
||||||
|
@Nullable BanFilter banFilter,
|
||||||
|
int maxConnections,
|
||||||
|
String hiddenServiceAddress) {
|
||||||
|
super(servicePort, networkProtoResolver, banFilter, maxConnections);
|
||||||
|
this.serviceAddress = hiddenServiceAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutDown(@Nullable Runnable shutDownCompleteHandler) {
|
||||||
|
super.shutDown(() -> {
|
||||||
|
log.info("TorNetworkNodeDirectBind shutdown completed");
|
||||||
|
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socks5Proxy getSocksProxy() {
|
||||||
|
Socks5Proxy proxy = new Socks5Proxy(InetAddress.getLoopbackAddress(), TOR_DATA_PORT);
|
||||||
|
proxy.resolveAddrLocally(false);
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Socket createSocket(NodeAddress peerNodeAddress) throws IOException {
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc1928 SOCKS5 Protocol
|
||||||
|
try {
|
||||||
|
checkArgument(peerNodeAddress.getHostName().endsWith(".onion"), "PeerAddress is not an onion address");
|
||||||
|
Socket sock = new Socket(InetAddress.getLoopbackAddress(), TOR_DATA_PORT);
|
||||||
|
sock.getOutputStream().write(Hex.decode("050100"));
|
||||||
|
String response = Hex.encode(sock.getInputStream().readNBytes(2));
|
||||||
|
if (!response.equalsIgnoreCase("0500")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String connect_details = "050100033E" + Hex.encode(peerNodeAddress.getHostName().getBytes(StandardCharsets.UTF_8));
|
||||||
|
StringBuilder connect_port = new StringBuilder(Integer.toHexString(peerNodeAddress.getPort()));
|
||||||
|
while (connect_port.length() < 4) connect_port.insert(0, "0");
|
||||||
|
connect_details = connect_details + connect_port;
|
||||||
|
sock.getOutputStream().write(Hex.decode(connect_details));
|
||||||
|
response = Hex.encode(sock.getInputStream().readNBytes(10));
|
||||||
|
if (response.substring(0, 2).equalsIgnoreCase("05") && response.substring(2, 4).equalsIgnoreCase("00")) {
|
||||||
|
return sock; // success
|
||||||
|
}
|
||||||
|
if (response.substring(2, 4).equalsIgnoreCase("04")) {
|
||||||
|
log.warn("Host unreachable: {}", peerNodeAddress);
|
||||||
|
} else {
|
||||||
|
log.warn("SOCKS error code received {} expected 00", response.substring(2, 4));
|
||||||
|
}
|
||||||
|
if (!response.substring(0, 2).equalsIgnoreCase("05")) {
|
||||||
|
log.warn("unexpected response, this isn't a SOCKS5 proxy?: {} {}", response, response.substring(0, 2));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn(e.toString());
|
||||||
|
}
|
||||||
|
throw new IOException("createSocket failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void createTorAndHiddenService() {
|
||||||
|
executor.submit(() -> {
|
||||||
|
try {
|
||||||
|
// listener for incoming messages at the hidden service
|
||||||
|
ServerSocket socket = new ServerSocket(servicePort);
|
||||||
|
nodeAddressProperty.set(new NodeAddress(serviceAddress + ":" + servicePort));
|
||||||
|
log.info("\n################################################################\n" +
|
||||||
|
"Bound to Tor hidden service: {} Port: {}\n" +
|
||||||
|
"################################################################",
|
||||||
|
serviceAddress, servicePort);
|
||||||
|
UserThread.execute(() -> setupListeners.forEach(SetupListener::onTorNodeReady));
|
||||||
|
UserThread.runAfter(() -> {
|
||||||
|
nodeAddressProperty.set(new NodeAddress(serviceAddress + ":" + servicePort));
|
||||||
|
startServer(socket);
|
||||||
|
setupListeners.forEach(SetupListener::onHiddenServicePublished);
|
||||||
|
}, 3);
|
||||||
|
return null;
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Could not connect to external Tor", e);
|
||||||
|
UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage()))));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,174 @@
|
|||||||
|
package haveno.network.p2p.network;
|
||||||
|
|
||||||
|
import haveno.common.Timer;
|
||||||
|
import haveno.network.p2p.NodeAddress;
|
||||||
|
|
||||||
|
import haveno.common.UserThread;
|
||||||
|
import haveno.common.proto.network.NetworkProtoResolver;
|
||||||
|
|
||||||
|
import haveno.network.utils.Utils;
|
||||||
|
import org.berndpruenster.netlayer.tor.HiddenServiceSocket;
|
||||||
|
import org.berndpruenster.netlayer.tor.Tor;
|
||||||
|
import org.berndpruenster.netlayer.tor.TorCtlException;
|
||||||
|
import org.berndpruenster.netlayer.tor.TorSocket;
|
||||||
|
|
||||||
|
import com.runjva.sourceforge.jsocks.protocol.Socks5Proxy;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import static com.google.common.base.Preconditions.checkArgument;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class TorNetworkNodeNetlayer extends TorNetworkNode {
|
||||||
|
|
||||||
|
private static final long SHUT_DOWN_TIMEOUT = 2;
|
||||||
|
|
||||||
|
private HiddenServiceSocket hiddenServiceSocket;
|
||||||
|
private boolean streamIsolation;
|
||||||
|
private Socks5Proxy socksProxy;
|
||||||
|
protected TorMode torMode;
|
||||||
|
private Tor tor;
|
||||||
|
private final String torControlHost;
|
||||||
|
private Timer shutDownTimeoutTimer;
|
||||||
|
private boolean shutDownInProgress;
|
||||||
|
private boolean shutDownComplete;
|
||||||
|
|
||||||
|
public TorNetworkNodeNetlayer(int servicePort,
|
||||||
|
NetworkProtoResolver networkProtoResolver,
|
||||||
|
TorMode torMode,
|
||||||
|
@Nullable BanFilter banFilter,
|
||||||
|
int maxConnections,
|
||||||
|
boolean useStreamIsolation,
|
||||||
|
String torControlHost) {
|
||||||
|
super(servicePort, networkProtoResolver, banFilter, maxConnections);
|
||||||
|
this.torControlHost = torControlHost;
|
||||||
|
this.streamIsolation = useStreamIsolation;
|
||||||
|
this.torMode = torMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(@Nullable SetupListener setupListener) {
|
||||||
|
torMode.doRollingBackup();
|
||||||
|
super.start(setupListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutDown(@Nullable Runnable shutDownCompleteHandler) {
|
||||||
|
log.info("TorNetworkNodeNetlayer shutdown started");
|
||||||
|
if (shutDownComplete) {
|
||||||
|
log.info("TorNetworkNodeNetlayer shutdown already completed");
|
||||||
|
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (shutDownInProgress) {
|
||||||
|
log.warn("Ignoring request to shut down because shut down is in progress");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
shutDownInProgress = true;
|
||||||
|
|
||||||
|
shutDownTimeoutTimer = UserThread.runAfter(() -> {
|
||||||
|
log.error("A timeout occurred at shutDown");
|
||||||
|
shutDownComplete = true;
|
||||||
|
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
|
||||||
|
executor.shutdownNow();
|
||||||
|
}, SHUT_DOWN_TIMEOUT);
|
||||||
|
|
||||||
|
super.shutDown(() -> {
|
||||||
|
try {
|
||||||
|
tor = Tor.getDefault();
|
||||||
|
if (tor != null) {
|
||||||
|
tor.shutdown();
|
||||||
|
tor = null;
|
||||||
|
log.info("Tor shutdown completed");
|
||||||
|
}
|
||||||
|
executor.shutdownNow();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
log.error("Shutdown TorNetworkNodeNetlayer failed with exception", e);
|
||||||
|
} finally {
|
||||||
|
shutDownTimeoutTimer.stop();
|
||||||
|
shutDownComplete = true;
|
||||||
|
if (shutDownCompleteHandler != null) shutDownCompleteHandler.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Socket createSocket(NodeAddress peerNodeAddress) throws IOException {
|
||||||
|
checkArgument(peerNodeAddress.getHostName().endsWith(".onion"), "PeerAddress is not an onion address");
|
||||||
|
// If streamId is null stream isolation gets deactivated.
|
||||||
|
// Hidden services use stream isolation by default, so we pass null.
|
||||||
|
return new TorSocket(peerNodeAddress.getHostName(), peerNodeAddress.getPort(), torControlHost, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Socks5Proxy getSocksProxy() {
|
||||||
|
try {
|
||||||
|
String stream = null;
|
||||||
|
if (streamIsolation) {
|
||||||
|
byte[] bytes = new byte[512]; // tor.getProxy creates a Sha256 hash
|
||||||
|
new SecureRandom().nextBytes(bytes);
|
||||||
|
stream = Base64.getEncoder().encodeToString(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socksProxy == null || streamIsolation) {
|
||||||
|
tor = Tor.getDefault();
|
||||||
|
socksProxy = tor != null ? tor.getProxy(torControlHost, stream) : null;
|
||||||
|
}
|
||||||
|
return socksProxy;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
log.error("Error at getSocksProxy", t);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void createTorAndHiddenService() {
|
||||||
|
int localPort = Utils.findFreeSystemPort();
|
||||||
|
executor.submit(() -> {
|
||||||
|
try {
|
||||||
|
Tor.setDefault(torMode.getTor());
|
||||||
|
long ts = System.currentTimeMillis();
|
||||||
|
hiddenServiceSocket = new HiddenServiceSocket(localPort, torMode.getHiddenServiceDirectory(), servicePort);
|
||||||
|
nodeAddressProperty.set(new NodeAddress(hiddenServiceSocket.getServiceName() + ":" + hiddenServiceSocket.getHiddenServicePort()));
|
||||||
|
UserThread.execute(() -> setupListeners.forEach(SetupListener::onTorNodeReady));
|
||||||
|
hiddenServiceSocket.addReadyListener(socket -> {
|
||||||
|
log.info("\n################################################################\n" +
|
||||||
|
"Tor hidden service published after {} ms. Socket={}\n" +
|
||||||
|
"################################################################",
|
||||||
|
System.currentTimeMillis() - ts, socket);
|
||||||
|
UserThread.execute(() -> {
|
||||||
|
nodeAddressProperty.set(new NodeAddress(hiddenServiceSocket.getServiceName() + ":"
|
||||||
|
+ hiddenServiceSocket.getHiddenServicePort()));
|
||||||
|
startServer(socket);
|
||||||
|
setupListeners.forEach(SetupListener::onHiddenServicePublished);
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
} catch (TorCtlException e) {
|
||||||
|
log.error("Starting tor node failed", e);
|
||||||
|
if (e.getCause() instanceof IOException) {
|
||||||
|
UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage()))));
|
||||||
|
} else {
|
||||||
|
UserThread.execute(() -> setupListeners.forEach(SetupListener::onRequestCustomBridges));
|
||||||
|
log.warn("We shutdown as starting tor with the default bridges failed. We request user to add custom bridges.");
|
||||||
|
shutDown(null);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Could not connect to running Tor", e);
|
||||||
|
UserThread.execute(() -> setupListeners.forEach(s -> s.onSetupFailed(new RuntimeException(e.getMessage()))));
|
||||||
|
} catch (Throwable ignore) {
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -381,7 +381,7 @@ public final class PeerManager implements ConnectionListener, PersistedDataHost
|
|||||||
// If a node is trying to send too many list we treat it as rule violation.
|
// If a node is trying to send too many list we treat it as rule violation.
|
||||||
// Reported list include the connected list. We use the max value and give some extra headroom.
|
// Reported list include the connected list. We use the max value and give some extra headroom.
|
||||||
// Will trigger a shutdown after 2nd time sending too much
|
// Will trigger a shutdown after 2nd time sending too much
|
||||||
connection.reportInvalidRequest(RuleViolation.TOO_MANY_REPORTED_PEERS_SENT);
|
connection.reportInvalidRequest(RuleViolation.TOO_MANY_REPORTED_PEERS_SENT, "Too many reported peers sent");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,8 +50,13 @@ public class TorNetworkNodeTest {
|
|||||||
public void testTorNodeBeforeSecondReady() throws InterruptedException, IOException {
|
public void testTorNodeBeforeSecondReady() throws InterruptedException, IOException {
|
||||||
latch = new CountDownLatch(1);
|
latch = new CountDownLatch(1);
|
||||||
int port = 9001;
|
int port = 9001;
|
||||||
TorNetworkNode node1 = new TorNetworkNode(port, TestUtils.getNetworkProtoResolver(), false,
|
TorNetworkNode node1 = new TorNetworkNodeNetlayer(port,
|
||||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
TestUtils.getNetworkProtoResolver(),
|
||||||
|
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||||
|
null,
|
||||||
|
12,
|
||||||
|
false,
|
||||||
|
"127.0.0.1");
|
||||||
node1.start(new SetupListener() {
|
node1.start(new SetupListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTorNodeReady() {
|
public void onTorNodeReady() {
|
||||||
@ -77,8 +82,13 @@ public class TorNetworkNodeTest {
|
|||||||
|
|
||||||
latch = new CountDownLatch(1);
|
latch = new CountDownLatch(1);
|
||||||
int port2 = 9002;
|
int port2 = 9002;
|
||||||
TorNetworkNode node2 = new TorNetworkNode(port2, TestUtils.getNetworkProtoResolver(), false,
|
TorNetworkNode node2 = new TorNetworkNodeNetlayer(port2,
|
||||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
TestUtils.getNetworkProtoResolver(),
|
||||||
|
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||||
|
null,
|
||||||
|
12,
|
||||||
|
false,
|
||||||
|
"127.0.0.1");
|
||||||
node2.start(new SetupListener() {
|
node2.start(new SetupListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTorNodeReady() {
|
public void onTorNodeReady() {
|
||||||
@ -135,8 +145,13 @@ public class TorNetworkNodeTest {
|
|||||||
public void testTorNodeAfterBothReady() throws InterruptedException, IOException {
|
public void testTorNodeAfterBothReady() throws InterruptedException, IOException {
|
||||||
latch = new CountDownLatch(2);
|
latch = new CountDownLatch(2);
|
||||||
int port = 9001;
|
int port = 9001;
|
||||||
TorNetworkNode node1 = new TorNetworkNode(port, TestUtils.getNetworkProtoResolver(), false,
|
TorNetworkNode node1 = new TorNetworkNodeNetlayer(port,
|
||||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
TestUtils.getNetworkProtoResolver(),
|
||||||
|
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||||
|
null,
|
||||||
|
12,
|
||||||
|
false,
|
||||||
|
"127.0.0.1");
|
||||||
node1.start(new SetupListener() {
|
node1.start(new SetupListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTorNodeReady() {
|
public void onTorNodeReady() {
|
||||||
@ -161,8 +176,12 @@ public class TorNetworkNodeTest {
|
|||||||
});
|
});
|
||||||
|
|
||||||
int port2 = 9002;
|
int port2 = 9002;
|
||||||
TorNetworkNode node2 = new TorNetworkNode(port2, TestUtils.getNetworkProtoResolver(), false,
|
TorNetworkNode node2 = new TorNetworkNodeNetlayer(port2, TestUtils.getNetworkProtoResolver(),
|
||||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||||
|
null,
|
||||||
|
12,
|
||||||
|
false,
|
||||||
|
"127.0.0.1");
|
||||||
node2.start(new SetupListener() {
|
node2.start(new SetupListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onTorNodeReady() {
|
public void onTorNodeReady() {
|
||||||
|
@ -10,6 +10,8 @@ SyslogIdentifier=Haveno-Seednode
|
|||||||
ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\
|
ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\
|
||||||
--useLocalhostForP2P=false\
|
--useLocalhostForP2P=false\
|
||||||
--useDevPrivilegeKeys=false\
|
--useDevPrivilegeKeys=false\
|
||||||
|
# Uncomment the following line to use external tor
|
||||||
|
# --hiddenServiceAddress=example.onion\
|
||||||
--nodePort=2002\
|
--nodePort=2002\
|
||||||
--appName=haveno-XMR_STAGENET_Seed_2002\
|
--appName=haveno-XMR_STAGENET_Seed_2002\
|
||||||
# --logLevel=trace\
|
# --logLevel=trace\
|
||||||
|
@ -10,8 +10,10 @@ SyslogIdentifier=Haveno-Seednode2
|
|||||||
ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\
|
ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\
|
||||||
--useLocalhostForP2P=false\
|
--useLocalhostForP2P=false\
|
||||||
--useDevPrivilegeKeys=false\
|
--useDevPrivilegeKeys=false\
|
||||||
--nodePort=3003\
|
# Uncomment the following line to use external tor
|
||||||
--appName=haveno-XMR_STAGENET_Seed_3003\
|
# --hiddenServiceAddress=example.onion\
|
||||||
|
--nodePort=2003\
|
||||||
|
--appName=haveno-XMR_STAGENET_Seed_2003\
|
||||||
# --logLevel=trace\
|
# --logLevel=trace\
|
||||||
--xmrNode=http://[::1]:38088\
|
--xmrNode=http://[::1]:38088\
|
||||||
--xmrNodeUsername=admin\
|
--xmrNodeUsername=admin\
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
## 1. Enable persistent storage and admin password before starting tails
|
## 1. Enable persistent storage and admin password before starting tails
|
||||||
|
|
||||||
## 2. Get your haveno deb file in persistent storage, currently most people use haveno-reto (amd64 version for tails)
|
## 2. Get your haveno deb file in persistent storage (amd64 version for tails)
|
||||||
|
|
||||||
## 3. Edit the path to the haveno deb file if necessary then run ```sudo ./haveno-install.sh```
|
## 3. Edit the path to the haveno deb file if necessary then run ```sudo ./haveno-install.sh```
|
||||||
## 4. As amnesia run ```source ~/.bashrc```
|
## 4. As amnesia run ```source ~/.bashrc```
|
||||||
## 5. Start haveno using ```haveno-tails```
|
## 5. Start haveno using ```haveno-tails```
|
||||||
|
|
||||||
## You will need to run this script after each reset, but your data will be saved persistently in /home/amnesia/Persistence/Haveno-reto
|
## You will need to run this script after each reset, but your data will be saved persistently in /home/amnesia/Persistence/Haveno
|
||||||
|
@ -11,9 +11,11 @@ SyslogIdentifier=Haveno-Seednode
|
|||||||
ExecStart=/bin/sh $PATH/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\
|
ExecStart=/bin/sh $PATH/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\
|
||||||
--useLocalhostForP2P=false\
|
--useLocalhostForP2P=false\
|
||||||
--useDevPrivilegeKeys=false\
|
--useDevPrivilegeKeys=false\
|
||||||
|
# Uncomment the following line to use external tor
|
||||||
|
# --hiddenServiceAddress=example.onion\
|
||||||
--nodePort=2002\
|
--nodePort=2002\
|
||||||
--appName=haveno-XMR_STAGENET_Seed_2002
|
--appName=haveno-XMR_STAGENET_Seed_2002\
|
||||||
--xmrNode=[::1]:38088
|
--xmrNode=http://[::1]:38088
|
||||||
|
|
||||||
ExecStop=/bin/kill ${MAINPID} ; sleep 5
|
ExecStop=/bin/kill ${MAINPID} ; sleep 5
|
||||||
Restart=always
|
Restart=always
|
||||||
|
@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class SeedNodeMain extends ExecutableForAppWithP2p {
|
public class SeedNodeMain extends ExecutableForAppWithP2p {
|
||||||
private static final long CHECK_CONNECTION_LOSS_SEC = 30;
|
private static final long CHECK_CONNECTION_LOSS_SEC = 30;
|
||||||
private static final String VERSION = "1.0.9";
|
private static final String VERSION = "1.0.10";
|
||||||
private SeedNode seedNode;
|
private SeedNode seedNode;
|
||||||
private Timer checkConnectionLossTime;
|
private Timer checkConnectionLossTime;
|
||||||
|
|
||||||
|
165
seednode/torrc
165
seednode/torrc
@ -1,18 +1,155 @@
|
|||||||
RunAsDaemon 1
|
## Configuration file for Haveno Seednode
|
||||||
SOCKSPort 9050
|
##
|
||||||
ControlPort 9051
|
## To start/reload/etc this instance, run "systemctl start tor" (or reload, or..).
|
||||||
Log notice syslog
|
## This instance will run as user debian-tor; its data directory is /var/lib/tor.
|
||||||
|
##
|
||||||
|
## This file is configured via:
|
||||||
|
## /usr/share/tor/tor-service-defaults-torrc
|
||||||
|
##
|
||||||
|
## See 'man tor', for more options you can use in this file.
|
||||||
|
|
||||||
CookieAuthentication 0
|
## Tor opens a socks proxy on port 9050 by default -- even if you don't
|
||||||
CookieAuthFileGroupReadable 1
|
## configure one below. Set "SocksPort 0" if you plan to run Tor only
|
||||||
DataDirectoryGroupReadable 1
|
## as a relay, and not make any local application connections yourself.
|
||||||
|
#SocksPort 9050 # Default: Bind to localhost:9050 for local connections.
|
||||||
|
# ### SocksPort flag: OnionTrafficOnly ###
|
||||||
|
## Tell the tor client to only connect to .onion addresses in response to SOCKS5 requests on this connection.
|
||||||
|
## This is equivalent to NoDNSRequest, NoIPv4Traffic, NoIPv6Traffic.
|
||||||
|
# ### SocksPort flag: ExtendedErrors ###
|
||||||
|
## Return extended error code in the SOCKS reply. So far, the possible errors are:
|
||||||
|
# X'F0' Onion Service Descriptor Can Not be Found
|
||||||
|
# X'F1' Onion Service Descriptor Is Invalid
|
||||||
|
# X'F2' Onion Service Introduction Failed
|
||||||
|
# X'F3' Onion Service Rendezvous Failed
|
||||||
|
# X'F4' Onion Service Missing Client Authorization
|
||||||
|
# X'F5' Onion Service Wrong Client Authorization
|
||||||
|
# X'F6' Onion Service Invalid Address
|
||||||
|
# X'F7' Onion Service Introduction Timed Out
|
||||||
|
SocksPort 9050 OnionTrafficOnly ExtendedErrors
|
||||||
|
|
||||||
SafeSocks 0
|
## Entry policies to allow/deny SOCKS requests based on IP address.
|
||||||
HiddenServiceStatistics 0
|
## First entry that matches wins. If no SocksPolicy is set, we accept
|
||||||
|
## all (and only) requests that reach a SocksPort. Untrusted users who
|
||||||
|
## can access your SocksPort may be able to learn about the connections
|
||||||
|
## you make.
|
||||||
|
SocksPolicy accept 127.0.0.1
|
||||||
|
SocksPolicy accept6 [::1]
|
||||||
|
SocksPolicy reject *
|
||||||
|
|
||||||
|
## Tor will reject application connections that use unsafe variants of the socks protocol
|
||||||
|
## — ones that only provide an IP address, meaning the application is doing a DNS resolve first.
|
||||||
|
## Specifically, these are socks4 and socks5 when not doing remote DNS. (Default: 0)
|
||||||
|
#SafeSocks 1
|
||||||
|
|
||||||
|
## Tor will make a notice-level log entry for each connection to the Socks port indicating
|
||||||
|
## whether the request used a safe socks protocol or an unsafe one (see above entry on SafeSocks).
|
||||||
|
## This helps to determine whether an application using Tor is possibly leaking DNS requests. (Default: 0)
|
||||||
|
TestSocks 1
|
||||||
|
|
||||||
|
## Logs go to stdout at level "notice" unless redirected by something
|
||||||
|
## else, like one of the below lines. You can have as many Log lines as
|
||||||
|
## you want.
|
||||||
|
##
|
||||||
|
## We advise using "notice" in most cases, since anything more verbose
|
||||||
|
## may provide sensitive information to an attacker who obtains the logs.
|
||||||
|
##
|
||||||
|
## Send all messages of level 'notice' or higher to /var/log/tor/notices.log
|
||||||
|
#Log notice file /var/log/tor/notices.log
|
||||||
|
## Send every possible message to /var/log/tor/debug.log
|
||||||
|
#Log debug file /var/log/tor/debug.log
|
||||||
|
## Use the system log instead of Tor's logfiles (This is default)
|
||||||
|
#Log notice syslog
|
||||||
|
## To send all messages to stderr:
|
||||||
|
#Log debug stderr
|
||||||
|
|
||||||
|
# Try to write to disk less frequently than we would otherwise. This is useful when running on flash memory.
|
||||||
AvoidDiskWrites 1
|
AvoidDiskWrites 1
|
||||||
|
|
||||||
#MaxClientCircuitsPending 64
|
## TODO: This option has no effect. Bisq/Haveno is tor client &/or hidden service. 'man torrc':
|
||||||
#KeepalivePeriod 2
|
## Relays and bridges only. When this option is enabled, a Tor relay writes obfuscated statistics on its
|
||||||
#CircuitBuildTimeout 5
|
## role as hidden-service directory, introduction point, or rendezvous point to disk every 24 hours.
|
||||||
#NewCircuitPeriod 15
|
## If ExtraInfoStatistics is enabled, it will be published as part of the extra-info document. (Default: 1)
|
||||||
#NumEntryGuards 8
|
HiddenServiceStatistics 0
|
||||||
|
|
||||||
|
## NOTE: In order to use the ControlPort, the <user> must belong to the tor group.
|
||||||
|
## sudo usermod -aG debian-tor <user>
|
||||||
|
##
|
||||||
|
## The port on which Tor will listen for local connections from Tor
|
||||||
|
## controller applications, as documented in control-spec.txt.
|
||||||
|
#ControlPort 9051
|
||||||
|
## If you enable the controlport, be sure to enable one of these
|
||||||
|
## authentication methods, to prevent attackers from accessing it.
|
||||||
|
##
|
||||||
|
## Compute the hash of a password with "tor --hash-password password".
|
||||||
|
#HashedControlPassword 16:872860B76453A77D60CA2BB8C1A7042072093276A3D701AD684053EC4C
|
||||||
|
CookieAuthentication 0 # (Default: 1)
|
||||||
|
|
||||||
|
## MetricsPort provides an interface to the underlying Tor relay metrics.
|
||||||
|
## Exposing publicly is dangerous, set a very strict access policy.
|
||||||
|
## Retrieve the metrics with: curl http://127.0.0.1:9035/metrics
|
||||||
|
MetricsPort 127.0.0.1:9035
|
||||||
|
MetricsPortPolicy accept 127.0.0.1
|
||||||
|
MetricsPortPolicy accept [::1]
|
||||||
|
|
||||||
|
############### This section is just for location-hidden services ###
|
||||||
|
|
||||||
|
## Once you have configured a hidden service, you can look at the
|
||||||
|
## contents of the file ".../hidden_service/hostname" for the address
|
||||||
|
## to tell people. e.g.: 'sudo cat /var/lib/tor/haveno_seednode/hostname'
|
||||||
|
##
|
||||||
|
## HiddenServicePort x y:z says to redirect requests on port x to the
|
||||||
|
## address y:z.
|
||||||
|
##
|
||||||
|
## If you plan to keep your service available for a long time, you might want to make a backup copy
|
||||||
|
## of the private_key file or complete folder /var/lib/tor/hidden_service somewhere.
|
||||||
|
|
||||||
|
#### Haveno seednode incoming anonymity connections ###
|
||||||
|
HiddenServiceDir /var/lib/tor/haveno_seednode
|
||||||
|
HiddenServicePort 2002 127.0.0.1:2002
|
||||||
|
HiddenServicePort 2002 [::1]:2002
|
||||||
|
|
||||||
|
## NOTE: HiddenService options are per onion service
|
||||||
|
## https://community.torproject.org/onion-services/advanced/dos/
|
||||||
|
##
|
||||||
|
## Rate limiting at the Introduction Points
|
||||||
|
## Intropoint protections prevents onion service DoS from becoming a DoS for the entire machine and its guard.
|
||||||
|
HiddenServiceEnableIntroDoSDefense 1
|
||||||
|
#HiddenServiceEnableIntroDoSRatePerSec 25 # (Default: 25)
|
||||||
|
#HiddenServiceEnableIntroDoSBurstPerSec 200 # (Default: 200)
|
||||||
|
|
||||||
|
# Number of introduction points the hidden service will have. You can’t have more than 20.
|
||||||
|
#HiddenServiceNumIntroductionPoints 3 # (Default: 3)
|
||||||
|
|
||||||
|
## https://tpo.pages.torproject.net/onion-services/ecosystem/technology/pow/#configuring-an-onion-service-with-the-pow-protection
|
||||||
|
## Proof of Work (PoW) before establishing Rendezvous Circuits
|
||||||
|
## The lower the queue and burst rates, the higher the puzzle effort tends to be for users.
|
||||||
|
HiddenServicePoWDefensesEnabled 1
|
||||||
|
HiddenServicePoWQueueRate 200 # (Default: 250)
|
||||||
|
HiddenServicePoWQueueBurst 1000 # (Default: 2500)
|
||||||
|
|
||||||
|
## Stream limits in the established Rendezvous Circuits
|
||||||
|
## The maximum number of simultaneous streams (connections) per rendezvous circuit. The max value allowed is 65535. (0 = unlimited)
|
||||||
|
HiddenServiceMaxStreams 25
|
||||||
|
#HiddenServiceMaxStreamsCloseCircuit 1
|
||||||
|
|
||||||
|
#### Haveno seednode2 incoming anonymity connections ###
|
||||||
|
HiddenServiceDir /var/lib/tor/haveno_seednode2
|
||||||
|
HiddenServicePort 2003 127.0.0.1:2003
|
||||||
|
HiddenServicePort 2003 [::1]:2003
|
||||||
|
|
||||||
|
HiddenServiceEnableIntroDoSDefense 1
|
||||||
|
#HiddenServiceEnableIntroDoSRatePerSec 25 # (Default: 25)
|
||||||
|
#HiddenServiceEnableIntroDoSBurstPerSec 200 # (Default: 200)
|
||||||
|
#HiddenServiceNumIntroductionPoints 3 # (Default: 3)
|
||||||
|
|
||||||
|
HiddenServicePoWDefensesEnabled 1
|
||||||
|
HiddenServicePoWQueueRate 200 # (Default: 250)
|
||||||
|
HiddenServicePoWQueueBurst 1000 # (Default: 2500)
|
||||||
|
|
||||||
|
HiddenServiceMaxStreams 25
|
||||||
|
#HiddenServiceMaxStreamsCloseCircuit 1
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
LongLivedPorts 2002,2003
|
||||||
|
## Default: 21, 22, 706, 1863, 5050, 5190, 5222, 5223, 6523, 6667, 6697, 8300
|
||||||
|
Loading…
Reference in New Issue
Block a user