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
|
||||
|
||||
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.
|
||||
|
||||
|
@ -317,9 +317,6 @@ configure(project(':common')) {
|
||||
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-graphics:$javafxVersion:$os"
|
||||
}
|
||||
@ -610,7 +607,7 @@ configure(project(':desktop')) {
|
||||
apply plugin: 'com.github.johnrengelman.shadow'
|
||||
apply from: 'package/package.gradle'
|
||||
|
||||
version = '1.0.9-SNAPSHOT'
|
||||
version = '1.0.10-SNAPSHOT'
|
||||
|
||||
jar.manifest.attributes(
|
||||
"Implementation-Title": project.name,
|
||||
|
@ -28,7 +28,7 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
public class Version {
|
||||
// The application versions
|
||||
// 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.
|
||||
|
@ -77,6 +77,7 @@ public class Config {
|
||||
public static final String SEED_NODES = "seedNodes";
|
||||
public static final String BAN_LIST = "banList";
|
||||
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 MAX_CONNECTIONS = "maxConnections";
|
||||
public static final String SOCKS_5_PROXY_XMR_ADDRESS = "socks5ProxyXmrAddress";
|
||||
@ -151,6 +152,7 @@ public class Config {
|
||||
public final File appDataDir;
|
||||
public final int walletRpcBindPort;
|
||||
public final int nodePort;
|
||||
public final String hiddenServiceAddress;
|
||||
public final int maxMemory;
|
||||
public final String logLevel;
|
||||
public final List<String> bannedXmrNodes;
|
||||
@ -286,6 +288,12 @@ public class Config {
|
||||
.ofType(Integer.class)
|
||||
.defaultsTo(9999);
|
||||
|
||||
ArgumentAcceptingOptionSpec<String> hiddenServiceAddressOpt =
|
||||
parser.accepts(HIDDEN_SERVICE_ADDRESS, "Hidden Service Address to listen on")
|
||||
.withRequiredArg()
|
||||
.ofType(String.class)
|
||||
.defaultsTo("");
|
||||
|
||||
ArgumentAcceptingOptionSpec<Integer> walletRpcBindPortOpt =
|
||||
parser.accepts(WALLET_RPC_BIND_PORT, "Port to bind the wallet RPC on")
|
||||
.withRequiredArg()
|
||||
@ -670,6 +678,7 @@ public class Config {
|
||||
this.helpRequested = options.has(helpOpt);
|
||||
this.configFile = configFile;
|
||||
this.nodePort = options.valueOf(nodePortOpt);
|
||||
this.hiddenServiceAddress = options.valueOf(hiddenServiceAddressOpt);
|
||||
this.walletRpcBindPort = options.valueOf(walletRpcBindPortOpt);
|
||||
this.maxMemory = options.valueOf(maxMemoryOpt);
|
||||
this.logLevel = options.valueOf(logLevelOpt);
|
||||
|
@ -112,13 +112,13 @@ public class CoreDisputesService {
|
||||
|
||||
// Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes
|
||||
// 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();
|
||||
}, trade.getId());
|
||||
}
|
||||
|
||||
public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
byte[] payoutTxSerialized = null;
|
||||
String payoutTxHashAsString = null;
|
||||
|
||||
@ -163,7 +163,7 @@ public class CoreDisputesService {
|
||||
if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get();
|
||||
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
|
||||
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
try {
|
||||
|
||||
// create dispute result
|
||||
@ -275,11 +275,13 @@ public class CoreDisputesService {
|
||||
disputeResult.summaryNotesProperty().get()
|
||||
);
|
||||
|
||||
synchronized (dispute.getChatMessages()) {
|
||||
if (reason == DisputeResult.Reason.OPTION_TRADE &&
|
||||
dispute.getChatMessages().size() > 1 &&
|
||||
dispute.getChatMessages().get(1).isSystemMessage()) {
|
||||
textToSign += "\n" + dispute.getChatMessages().get(1).getMessage() + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign);
|
||||
|
||||
|
@ -178,7 +178,7 @@ class CoreWalletsService {
|
||||
verifyWalletsAreAvailable();
|
||||
verifyEncryptedWalletIsUnlocked();
|
||||
try {
|
||||
return xmrWalletService.getWallet().relayTx(metadata);
|
||||
return xmrWalletService.relayTx(metadata);
|
||||
} catch (Exception ex) {
|
||||
log.error("", ex);
|
||||
throw new IllegalStateException(ex);
|
||||
|
@ -101,6 +101,7 @@ public final class XmrConnectionService {
|
||||
private Long lastLogPollErrorTimestamp;
|
||||
private Long syncStartHeight = null;
|
||||
private TaskLooper daemonPollLooper;
|
||||
private long lastRefreshPeriodMs = 0;
|
||||
@Getter
|
||||
private boolean isShutDownStarted;
|
||||
private List<MoneroConnectionManagerListener> listeners = new ArrayList<>();
|
||||
@ -273,7 +274,11 @@ public final class XmrConnectionService {
|
||||
}
|
||||
|
||||
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
|
||||
if (isShutDownStarted) {
|
||||
@ -281,9 +286,15 @@ public final class XmrConnectionService {
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
@ -343,7 +354,11 @@ public final class XmrConnectionService {
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -413,12 +428,16 @@ public final class XmrConnectionService {
|
||||
return connection != null && HavenoUtils.isLocalHost(connection.getUri());
|
||||
}
|
||||
|
||||
private long getDefaultRefreshPeriodMs() {
|
||||
private long getDefaultRefreshPeriodMs(boolean internal) {
|
||||
MoneroRpcConnection connection = getConnection();
|
||||
if (connection == null) return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS;
|
||||
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
|
||||
else return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS; // TODO: announce faster refresh after done syncing
|
||||
if (internal) return XmrLocalNode.REFRESH_PERIOD_LOCAL_MS;
|
||||
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)) {
|
||||
return REFRESH_PERIOD_ONION_MS;
|
||||
} else {
|
||||
@ -638,7 +657,7 @@ public final class XmrConnectionService {
|
||||
// update polling
|
||||
doPollDaemon();
|
||||
if (currentConnection != getConnection()) return; // polling can change connection
|
||||
UserThread.runAfter(() -> updatePolling(), getRefreshPeriodMs() / 1000);
|
||||
UserThread.runAfter(() -> updatePolling(), getInternalRefreshPeriodMs() / 1000);
|
||||
|
||||
// notify listeners in parallel
|
||||
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) {
|
||||
if (daemonPollLooper != null) daemonPollLooper.stop();
|
||||
daemonPollLooper = new TaskLooper(() -> pollDaemon());
|
||||
daemonPollLooper.start(getRefreshPeriodMs());
|
||||
daemonPollLooper.start(getInternalRefreshPeriodMs());
|
||||
}
|
||||
}
|
||||
|
||||
@ -715,6 +734,13 @@ public final class XmrConnectionService {
|
||||
// connected to daemon
|
||||
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
|
||||
UserThread.execute(() -> {
|
||||
|
||||
|
@ -133,6 +133,22 @@ public class WalletAppSetup {
|
||||
String result;
|
||||
if (exception == null && errorMsg == null) {
|
||||
|
||||
// update daemon sync progress
|
||||
double chainDownloadPercentageD = xmrConnectionService.downloadPercentageProperty().doubleValue();
|
||||
Long bestChainHeight = xmrConnectionService.chainHeightProperty().get();
|
||||
String chainHeightAsString = bestChainHeight != null && bestChainHeight > 0 ? String.valueOf(bestChainHeight) : "";
|
||||
if (chainDownloadPercentageD < 1) {
|
||||
xmrDaemonSyncProgress.set(chainDownloadPercentageD);
|
||||
if (chainDownloadPercentageD > 0.0) {
|
||||
String synchronizingWith = Res.get("mainView.footer.xmrInfo.synchronizingWith", getXmrDaemonNetworkAsString(), chainHeightAsString, FormattingUtils.formatToRoundedPercentWithSymbol(chainDownloadPercentageD));
|
||||
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
|
||||
} else {
|
||||
result = Res.get("mainView.footer.xmrInfo",
|
||||
Res.get("mainView.footer.xmrInfo.connectingTo"),
|
||||
getXmrDaemonNetworkAsString());
|
||||
}
|
||||
} else {
|
||||
|
||||
// update wallet sync progress
|
||||
double walletDownloadPercentageD = (double) walletDownloadPercentage;
|
||||
xmrWalletSyncProgress.set(walletDownloadPercentageD);
|
||||
@ -144,29 +160,15 @@ public class WalletAppSetup {
|
||||
result = Res.get("mainView.footer.xmrInfo", synchronizedWith, feeInfo);
|
||||
getXmrSplashSyncIconId().set("image-connection-synced");
|
||||
downloadCompleteHandler.run();
|
||||
} else if (walletDownloadPercentageD > 0) {
|
||||
} 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);
|
||||
Long bestChainHeight = xmrConnectionService.chainHeightProperty().get();
|
||||
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));
|
||||
result = Res.get("mainView.footer.xmrInfo", synchronizingWith, "");
|
||||
} else {
|
||||
result = Res.get("mainView.footer.xmrInfo",
|
||||
Res.get("mainView.footer.xmrInfo.connectingTo"),
|
||||
getXmrDaemonNetworkAsString());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -114,6 +114,7 @@ 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
|
||||
// case we revert the isClosed flag so that the UI can reopen the dispute and indicate that a new dispute
|
||||
// message arrived.
|
||||
synchronized (dispute.getChatMessages()) {
|
||||
ObservableList<ChatMessage> chatMessages = dispute.getChatMessages();
|
||||
// If last message is not a result message we re-open as we might have received a new message from the
|
||||
// trader/mediator/arbitrator who has reopened the case
|
||||
@ -127,3 +128,4 @@ public class DisputeMsgEvents {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,13 +28,10 @@ import haveno.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
||||
import haveno.core.trade.HavenoUtils;
|
||||
import haveno.core.user.Preferences;
|
||||
import haveno.core.user.User;
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
import haveno.network.p2p.P2PService;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.collections.SetChangeListener;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -218,7 +215,7 @@ public class OfferFilterService {
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean hasValidSignature(Offer offer) {
|
||||
private boolean hasValidSignature(Offer offer) {
|
||||
|
||||
// get accepted arbitrator by address
|
||||
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?
|
||||
} else {
|
||||
|
||||
// otherwise log warning that arbitrator is unregistered
|
||||
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);
|
||||
// // otherwise log warning that arbitrator is unregistered
|
||||
// List<NodeAddress> arbitratorAddresses = user.getAcceptedArbitrators().stream().map(Arbitrator::getNodeAddress).collect(Collectors.toList());
|
||||
// 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());
|
||||
List<String> acceptedCountryCodes = proto.getAcceptedCountryCodesList().isEmpty() ?
|
||||
null : new ArrayList<>(proto.getAcceptedCountryCodesList());
|
||||
List<String> reserveTxKeyImages = proto.getReserveTxKeyImagesList().isEmpty() ?
|
||||
null : new ArrayList<>(proto.getReserveTxKeyImagesList());
|
||||
String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge());
|
||||
Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
|
||||
null : proto.getExtraDataMap();
|
||||
@ -431,7 +433,7 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
proto.getProtocolVersion(),
|
||||
proto.hasArbitratorSigner() ? NodeAddress.fromProto(proto.getArbitratorSigner()) : null,
|
||||
ProtoUtil.byteArrayOrNullFromProto(proto.getArbitratorSignature()),
|
||||
proto.getReserveTxKeyImagesList() == null ? null : new ArrayList<String>(proto.getReserveTxKeyImagesList()));
|
||||
reserveTxKeyImages);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -110,6 +110,7 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javax.annotation.Nullable;
|
||||
import lombok.Getter;
|
||||
import monero.common.MoneroRpcConnection;
|
||||
import monero.daemon.model.MoneroKeyImageSpentStatus;
|
||||
import monero.daemon.model.MoneroTx;
|
||||
import monero.wallet.model.MoneroIncomingTransfer;
|
||||
@ -688,7 +689,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
addOpenOffer(editedOpenOffer);
|
||||
|
||||
if (editedOpenOffer.isAvailable())
|
||||
republishOffer(editedOpenOffer);
|
||||
maybeRepublishOffer(editedOpenOffer);
|
||||
|
||||
offersToBeEdited.remove(openOffer.getId());
|
||||
requestPersistence();
|
||||
@ -863,8 +864,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
TransactionResultHandler resultHandler, // TODO (woodser): transaction not needed with result handler
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (processOffersLock) {
|
||||
List<String> errorMessages = new ArrayList<String>();
|
||||
synchronized (processOffersLock) {
|
||||
List<OpenOffer> openOffers = getOpenOffers();
|
||||
for (OpenOffer pendingOffer : openOffers) {
|
||||
if (pendingOffer.getState() != OpenOffer.State.PENDING) continue;
|
||||
@ -887,13 +888,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
});
|
||||
HavenoUtils.awaitLatch(latch);
|
||||
}
|
||||
}
|
||||
requestPersistence();
|
||||
if (errorMessages.isEmpty()) {
|
||||
if (resultHandler != null) resultHandler.handleResult(null);
|
||||
} else {
|
||||
if (errorMessageHandler != null) errorMessageHandler.handleErrorMessage(errorMessages.toString());
|
||||
}
|
||||
}
|
||||
}, THREAD_ID);
|
||||
}
|
||||
|
||||
@ -962,9 +963,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
}
|
||||
} else {
|
||||
|
||||
// handle sufficient balance
|
||||
// sign and post offer if enough funds
|
||||
boolean hasFundsReserved = openOffer.getReserveTxHash() != null;
|
||||
boolean hasSufficientBalance = xmrWalletService.getAvailableBalance().compareTo(amountNeeded) >= 0;
|
||||
if (hasSufficientBalance) {
|
||||
if (hasFundsReserved || hasSufficientBalance) {
|
||||
signAndPostOffer(openOffer, true, resultHandler, errorMessageHandler);
|
||||
return;
|
||||
} else if (openOffer.getScheduledTxHashes() == null) {
|
||||
@ -1083,11 +1085,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
BigInteger reserveAmount = openOffer.getOffer().getAmountNeeded();
|
||||
xmrWalletService.swapAddressEntryToAvailable(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING); // change funding subaddress in case funded with unsuitable output(s)
|
||||
MoneroTxWallet splitOutputTx = null;
|
||||
synchronized (XmrWalletService.WALLET_LOCK) {
|
||||
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
|
||||
XmrAddressEntry entry = xmrWalletService.getOrCreateAddressEntry(openOffer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||
try {
|
||||
log.info("Creating split output tx to fund offer {} at subaddress {}", openOffer.getShortId(), entry.getSubaddressIndex());
|
||||
splitOutputTx = xmrWalletService.createTx(new MoneroTxConfig()
|
||||
@ -1100,8 +1103,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
} catch (Exception e) {
|
||||
if (e.getMessage().contains("not enough")) throw e; // do not retry if not enough funds
|
||||
log.warn("Error creating split output tx to fund offer, offerId={}, subaddress={}, attempt={}/{}, error={}", openOffer.getShortId(), entry.getSubaddressIndex(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
||||
xmrWalletService.handleWalletError(e, sourceConnection);
|
||||
if (stopped || i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||
if (xmrConnectionService.isConnected()) xmrWalletService.requestSwitchToNextBestConnection();
|
||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||
}
|
||||
}
|
||||
@ -1714,11 +1717,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
synchronized (openOffers) {
|
||||
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
|
||||
// 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
|
||||
republishOffer(openOffer, () -> processListForRepublishOffers(list));
|
||||
maybeRepublishOffer(openOffer, () -> processListForRepublishOffers(list));
|
||||
|
||||
/* republishOffer(openOffer,
|
||||
() -> UserThread.runAfter(() -> processListForRepublishOffers(list),
|
||||
@ -1730,13 +1733,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
}
|
||||
}
|
||||
|
||||
private void republishOffer(OpenOffer openOffer) {
|
||||
republishOffer(openOffer, null);
|
||||
private void maybeRepublishOffer(OpenOffer openOffer) {
|
||||
maybeRepublishOffer(openOffer, null);
|
||||
}
|
||||
|
||||
private void republishOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) {
|
||||
private void maybeRepublishOffer(OpenOffer openOffer, @Nullable Runnable completeHandler) {
|
||||
ThreadUtils.execute(() -> {
|
||||
|
||||
// skip if prevented from publishing
|
||||
if (preventedFromPublishing(openOffer)) {
|
||||
if (completeHandler != null) completeHandler.run();
|
||||
return;
|
||||
}
|
||||
|
||||
// determine if offer is valid
|
||||
boolean isValid = true;
|
||||
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());
|
||||
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());
|
||||
isValid = false;
|
||||
}
|
||||
@ -1811,6 +1820,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
}, THREAD_ID);
|
||||
}
|
||||
|
||||
private boolean preventedFromPublishing(OpenOffer openOffer) {
|
||||
return openOffer.isDeactivated() || openOffer.isCanceled();
|
||||
}
|
||||
|
||||
private void startPeriodicRepublishOffersTimer() {
|
||||
stopped = false;
|
||||
if (periodicRepublishOffersTimer == null) {
|
||||
@ -1843,8 +1856,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
final OpenOffer openOffer = openOffersList.get(i);
|
||||
UserThread.runAfterRandomDelay(() -> {
|
||||
// we need to check if in the meantime the offer has been removed
|
||||
if (openOffers.contains(openOffer) && openOffer.isAvailable())
|
||||
refreshOffer(openOffer, 0, 1);
|
||||
boolean contained = false;
|
||||
synchronized (openOffers) {
|
||||
contained = openOffers.contains(openOffer);
|
||||
}
|
||||
if (contained) maybeRefreshOffer(openOffer, 0, 1);
|
||||
}, minDelay, maxDelay, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
} else {
|
||||
@ -1857,13 +1873,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
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(),
|
||||
() -> log.debug("Successful refreshed TTL for offer"),
|
||||
(errorMessage) -> {
|
||||
log.warn(errorMessage);
|
||||
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.protocol.TradeProtocol;
|
||||
import haveno.core.xmr.model.XmrAddressEntry;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.common.MoneroRpcConnection;
|
||||
import monero.daemon.model.MoneroOutput;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
|
||||
@ -59,11 +59,11 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||
}
|
||||
|
||||
// verify monero connection
|
||||
model.getXmrWalletService().getConnectionService().verifyConnection();
|
||||
model.getXmrWalletService().getXmrConnectionService().verifyConnection();
|
||||
|
||||
// create reserve tx
|
||||
MoneroTxWallet reserveTx = null;
|
||||
synchronized (XmrWalletService.WALLET_LOCK) {
|
||||
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
|
||||
|
||||
// reset protocol timeout
|
||||
verifyPending();
|
||||
@ -82,14 +82,16 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||
try {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
MoneroRpcConnection sourceConnection = model.getXmrWalletService().getXmrConnectionService().getConnection();
|
||||
try {
|
||||
//if (true) throw new RuntimeException("Pretend error");
|
||||
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, makerFee, sendAmount, securityDeposit, returnAddress, openOffer.isReserveExactAmount(), preferredSubaddressIndex);
|
||||
} catch (Exception e) {
|
||||
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;
|
||||
model.getProtocol().startTimeoutTimer(); // reset protocol timeout
|
||||
if (model.getXmrWalletService().getConnectionService().isConnected()) model.getXmrWalletService().requestSwitchToNextBestConnection();
|
||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||
}
|
||||
|
||||
@ -102,11 +104,8 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||
|
||||
// reset state with wallet lock
|
||||
model.getXmrWalletService().resetAddressEntriesForOpenOffer(offer.getId());
|
||||
if (reserveTx != null) {
|
||||
model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
|
||||
if (reserveTx != null) model.getXmrWalletService().thawOutputs(HavenoUtils.getInputKeyImages(reserveTx));
|
||||
offer.getOfferPayload().setReserveTxKeyImages(null);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
@ -132,7 +131,11 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
|
||||
}
|
||||
}
|
||||
|
||||
public void verifyPending() {
|
||||
if (!model.getOpenOffer().isPending()) throw new RuntimeException("Offer " + model.getOpenOffer().getOffer().getId() + " is canceled");
|
||||
private boolean isPending() {
|
||||
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) {
|
||||
if (ackMessage.getSourceType() == getAckMessageSourceType()) {
|
||||
if (ackMessage.isSuccess()) {
|
||||
log.info("Received AckMessage for {} with tradeId {} and uid {}",
|
||||
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
|
||||
log.info("Received AckMessage for {} with tradeId {} and uid {}", ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
|
||||
|
||||
// ack message on chat message received when dispute is opened and closed
|
||||
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 (dispute.isClosed()) trade.pollWalletNormallyForMs(30000); // sync to check for payout
|
||||
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 {} and errorMessage={}",
|
||||
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAllChatMessages(ackMessage.getSourceId()).stream()
|
||||
|
@ -77,6 +77,10 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
||||
REOPENED,
|
||||
CLOSED;
|
||||
|
||||
public boolean isOpen() {
|
||||
return this == NEW || this == OPEN || this == REOPENED;
|
||||
}
|
||||
|
||||
public static Dispute.State fromProto(protobuf.Dispute.State state) {
|
||||
return ProtoUtil.enumFromProto(Dispute.State.class, state.name());
|
||||
}
|
||||
@ -349,18 +353,21 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void addAndPersistChatMessage(ChatMessage chatMessage) {
|
||||
synchronized (chatMessages) {
|
||||
if (!chatMessages.contains(chatMessage)) {
|
||||
chatMessages.add(chatMessage);
|
||||
} else {
|
||||
log.error("disputeDirectMessage already exists");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isMediationDispute() {
|
||||
return !chatMessages.isEmpty() && chatMessages.get(0).getSupportType() == SupportType.MEDIATION;
|
||||
}
|
||||
|
||||
public boolean removeAllChatMessages() {
|
||||
synchronized (chatMessages) {
|
||||
if (chatMessages.size() > 1) {
|
||||
// removes all chat except the initial guidelines message.
|
||||
String firstMessageUid = chatMessages.get(0).getUid();
|
||||
@ -369,6 +376,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void maybeClearSensitiveData() {
|
||||
String change = "";
|
||||
|
@ -90,6 +90,7 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void cleanupDisputes(@Nullable Consumer<String> closedDisputeHandler) {
|
||||
synchronized (disputeList.getObservableList()) {
|
||||
disputeList.stream().forEach(dispute -> {
|
||||
String tradeId = dispute.getTradeId();
|
||||
if (dispute.isClosed() && closedDisputeHandler != null) {
|
||||
@ -97,6 +98,7 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -130,8 +132,10 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
|
||||
}
|
||||
|
||||
ObservableList<Dispute> getObservableList() {
|
||||
synchronized (disputeList.getObservableList()) {
|
||||
return disputeList.getObservableList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -151,10 +155,12 @@ public abstract class DisputeListService<T extends DisputeList<Dispute>> impleme
|
||||
isAlerting -> {
|
||||
// We get the event before the list gets updated, so we execute on next frame
|
||||
UserThread.execute(() -> {
|
||||
synchronized (disputeList.getObservableList()) {
|
||||
int numAlerts = (int) disputeList.getList().stream()
|
||||
.mapToLong(x -> x.getBadgeCountProperty().getValue())
|
||||
.sum();
|
||||
numOpenDisputes.set(numAlerts);
|
||||
}
|
||||
});
|
||||
});
|
||||
disputedTradeIds.add(dispute.getTradeId());
|
||||
|
@ -157,6 +157,11 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
disputeListService.requestPersistence();
|
||||
}
|
||||
|
||||
protected void requestPersistence(Trade trade) {
|
||||
trade.requestPersistence();
|
||||
disputeListService.requestPersistence();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeAddress getPeerNodeAddress(ChatMessage message) {
|
||||
Optional<Dispute> disputeOptional = findDispute(message);
|
||||
@ -182,11 +187,13 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
|
||||
@Override
|
||||
public List<ChatMessage> getAllChatMessages(String tradeId) {
|
||||
synchronized (getDisputeList().getObservableList()) {
|
||||
return getDisputeList().stream()
|
||||
.filter(dispute -> dispute.getTradeId().equals(tradeId))
|
||||
.flatMap(dispute -> dispute.getChatMessages().stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean channelOpen(ChatMessage message) {
|
||||
@ -234,7 +241,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
}
|
||||
|
||||
public ObservableList<Dispute> getDisputesAsObservableList() {
|
||||
synchronized(disputeListService.getDisputeList()) {
|
||||
synchronized(disputeListService.getDisputeList().getObservableList()) {
|
||||
return disputeListService.getObservableList();
|
||||
}
|
||||
}
|
||||
@ -244,7 +251,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
}
|
||||
|
||||
protected T getDisputeList() {
|
||||
synchronized(disputeListService.getDisputeList()) {
|
||||
synchronized(disputeListService.getDisputeList().getObservableList()) {
|
||||
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
|
||||
public void sendDisputeOpenedMessage(Dispute dispute,
|
||||
boolean reOpen,
|
||||
String updatedMultisigHex,
|
||||
ResultHandler resultHandler,
|
||||
FaultHandler faultHandler) {
|
||||
|
||||
// get trade
|
||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||
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;
|
||||
}
|
||||
|
||||
@ -343,7 +356,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (disputeList) {
|
||||
synchronized (disputeList.getObservableList()) {
|
||||
if (disputeList.contains(dispute)) {
|
||||
String msg = "We got a dispute msg that we have already stored. TradeId = " + dispute.getTradeId() + ", DisputeId = " + dispute.getId();
|
||||
log.warn(msg);
|
||||
@ -352,7 +365,16 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
}
|
||||
|
||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed();
|
||||
if (!storedDisputeOptional.isPresent() || reOpen) {
|
||||
|
||||
// add or re-open dispute
|
||||
if (reOpen) {
|
||||
dispute = storedDisputeOptional.get();
|
||||
} else {
|
||||
disputeList.add(dispute);
|
||||
}
|
||||
|
||||
String disputeInfo = getDisputeInfo(dispute);
|
||||
String sysMsg = dispute.isSupportTicket() ?
|
||||
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) :
|
||||
@ -367,17 +389,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
p2PService.getAddress());
|
||||
chatMessage.setSystemMessage(true);
|
||||
dispute.addAndPersistChatMessage(chatMessage);
|
||||
if (!reOpen) {
|
||||
disputeList.add(dispute);
|
||||
}
|
||||
|
||||
// create dispute opened message
|
||||
trade.exportMultisigHex();
|
||||
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
|
||||
DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
|
||||
p2PService.getAddress(),
|
||||
UUID.randomUUID().toString(),
|
||||
getSupportType(),
|
||||
updatedMultisigHex,
|
||||
trade.getSelf().getUpdatedMultisigHex(),
|
||||
trade.getArbitrator().getPaymentSentMessage());
|
||||
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
@ -387,6 +407,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName());
|
||||
|
||||
// send dispute opened message
|
||||
trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
|
||||
mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress,
|
||||
dispute.getAgentPubKeyRing(),
|
||||
disputeOpenedMessage,
|
||||
@ -420,7 +441,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setStoredInMailbox(true);
|
||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_REQUESTED);
|
||||
requestPersistence();
|
||||
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
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setSendMessageError(errorMessage);
|
||||
trade.setDisputeState(Trade.DisputeState.NO_DISPUTE);
|
||||
requestPersistence();
|
||||
faultHandler.handleFault("Sending dispute message failed: " +
|
||||
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
|
||||
protected void handleDisputeOpenedMessage(DisputeOpenedMessage message) {
|
||||
Dispute dispute = message.getDispute();
|
||||
log.info("Processing {} with trade {}, dispute {}", message.getClass().getSimpleName(), dispute.getTradeId(), dispute.getId());
|
||||
Dispute msgDispute = message.getDispute();
|
||||
log.info("Processing {} with trade {}, dispute {}", message.getClass().getSimpleName(), msgDispute.getTradeId(), msgDispute.getId());
|
||||
|
||||
// get trade
|
||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||
Trade trade = tradeManager.getTrade(msgDispute.getTradeId());
|
||||
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;
|
||||
}
|
||||
|
||||
// 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(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
String errorMessage = null;
|
||||
PubKeyRing senderPubKeyRing = null;
|
||||
try {
|
||||
@ -503,14 +538,20 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
}
|
||||
|
||||
// get sender
|
||||
TradePeer sender;
|
||||
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();
|
||||
TradePeer sender = trade.getTradePeer(senderPubKeyRing);
|
||||
sender = trade.getTradePeer(senderPubKeyRing);
|
||||
}
|
||||
if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
|
||||
|
||||
// update sender node address
|
||||
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()) {
|
||||
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
|
||||
if (trade instanceof ArbitratorTrade) addPriceInfoMessage(dispute, 0);
|
||||
|
||||
// add dispute
|
||||
// add or re-open dispute
|
||||
synchronized (disputeList) {
|
||||
if (!disputeList.contains(dispute)) {
|
||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||
if (!storedDisputeOptional.isPresent()) {
|
||||
if (!disputeList.contains(msgDispute)) {
|
||||
if (!storedDisputeOptional.isPresent() || reOpen) {
|
||||
|
||||
// update trade state
|
||||
if (reOpen) {
|
||||
trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
||||
} else {
|
||||
disputeList.add(dispute);
|
||||
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
|
||||
}
|
||||
|
||||
// send dispute opened message to peer if arbitrator
|
||||
if (trade.isArbitrator()) sendDisputeOpenedMessageToPeer(dispute, contract, dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(), trade.getSelf().getUpdatedMultisigHex());
|
||||
// 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();
|
||||
errorMessage = null;
|
||||
} else {
|
||||
@ -548,7 +602,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
// add chat message with mediation info if applicable
|
||||
addMediationResultMessage(dispute);
|
||||
} 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) {
|
||||
@ -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
|
||||
ObservableList<ChatMessage> messages = message.getDispute().getChatMessages();
|
||||
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);
|
||||
}
|
||||
|
||||
@ -575,7 +629,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
Contract contractFromOpener,
|
||||
PubKeyRing pubKeyRing,
|
||||
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
|
||||
// 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
|
||||
@ -597,6 +651,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
return;
|
||||
}
|
||||
|
||||
// create mirrored dispute
|
||||
Dispute dispute = new Dispute(new Date().getTime(),
|
||||
disputeFromOpener.getTradeId(),
|
||||
pubKeyRing.hashCode(),
|
||||
@ -622,10 +677,9 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
|
||||
dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx());
|
||||
|
||||
// skip if dispute already open
|
||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||
|
||||
// Valid case if both have opened a dispute and agent was not online.
|
||||
if (storedDisputeOptional.isPresent()) {
|
||||
if (storedDisputeOptional.isPresent() && !storedDisputeOptional.get().isClosed()) {
|
||||
log.info("We got a dispute already open for that trade and trading peer. TradeId = {}", dispute.getTradeId());
|
||||
return;
|
||||
}
|
||||
@ -647,9 +701,16 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
|
||||
addPriceInfoMessage(dispute, 0);
|
||||
|
||||
// add or re-open dispute
|
||||
boolean reOpen = storedDisputeOptional.isPresent() && storedDisputeOptional.get().isClosed();
|
||||
if (reOpen) {
|
||||
dispute = storedDisputeOptional.get();
|
||||
dispute.reOpen();
|
||||
} else {
|
||||
synchronized (disputeList) {
|
||||
disputeList.add(dispute);
|
||||
}
|
||||
}
|
||||
|
||||
// get trade
|
||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||
@ -658,10 +719,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
return;
|
||||
}
|
||||
|
||||
// We mirrored dispute already!
|
||||
Contract contract = dispute.getContract();
|
||||
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
|
||||
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress();
|
||||
// create dispute opened message with peer dispute
|
||||
TradePeer peer = trade.getTradePeer(pubKeyRing);
|
||||
PubKeyRing peersPubKeyRing = peer.getPubKeyRing();
|
||||
NodeAddress peersNodeAddress = peer.getNodeAddress();
|
||||
DisputeOpenedMessage peerOpenedDisputeMessage = new DisputeOpenedMessage(dispute,
|
||||
p2PService.getAddress(),
|
||||
UUID.randomUUID().toString(),
|
||||
@ -749,7 +810,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
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());
|
||||
if (!trade.isPayoutPublished() && receiver.getUpdatedMultisigHex() != null && receiver.getUnsignedPayoutTxHex() == null) {
|
||||
createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true);
|
||||
@ -792,7 +853,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
disputeResult.getChatMessage().setArrived(true);
|
||||
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG);
|
||||
trade.pollWalletNormallyForMs(30000);
|
||||
requestPersistence();
|
||||
requestPersistence(trade);
|
||||
resultHandler.handleResult();
|
||||
}
|
||||
|
||||
@ -811,7 +872,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
disputeResult.getChatMessage().setStoredInMailbox(true);
|
||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG);
|
||||
requestPersistence();
|
||||
requestPersistence(trade);
|
||||
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
|
||||
disputeResult.getChatMessage().setSendMessageError(errorMessage);
|
||||
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG);
|
||||
requestPersistence();
|
||||
requestPersistence(trade);
|
||||
faultHandler.handleFault(errorMessage, new RuntimeException(errorMessage));
|
||||
}
|
||||
}
|
||||
);
|
||||
trade.advanceDisputeState(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG);
|
||||
requestPersistence();
|
||||
requestPersistence(trade);
|
||||
} catch (Exception e) {
|
||||
faultHandler.handleFault(e.getMessage(), e);
|
||||
}
|
||||
@ -900,11 +961,11 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
// update trade state
|
||||
if (updateState) {
|
||||
trade.getProcessModel().setUnsignedPayoutTx(payoutTx);
|
||||
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());
|
||||
trade.updatePayout(payoutTx);
|
||||
if (trade.getBuyer().getUpdatedMultisigHex() != null && trade.getBuyer().getUnsignedPayoutTxHex() == null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||
if (trade.getSeller().getUpdatedMultisigHex() != null && trade.getSeller().getUnsignedPayoutTxHex() == null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||
if (trade.getBuyer().getUpdatedMultisigHex() != null) trade.getBuyer().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||
if (trade.getSeller().getUpdatedMultisigHex() != null) trade.getSeller().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||
}
|
||||
trade.requestPersistence();
|
||||
return payoutTx;
|
||||
} catch (Exception e) {
|
||||
trade.syncAndPollWallet();
|
||||
@ -937,21 +998,21 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
return keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing());
|
||||
}
|
||||
|
||||
private Optional<Dispute> findDispute(Dispute dispute) {
|
||||
public Optional<Dispute> findDispute(Dispute dispute) {
|
||||
return findDispute(dispute.getTradeId(), dispute.getTraderId());
|
||||
}
|
||||
|
||||
protected Optional<Dispute> findDispute(DisputeResult disputeResult) {
|
||||
public Optional<Dispute> findDispute(DisputeResult disputeResult) {
|
||||
ChatMessage chatMessage = disputeResult.getChatMessage();
|
||||
checkNotNull(chatMessage, "chatMessage must not be null");
|
||||
return findDispute(disputeResult.getTradeId(), disputeResult.getTraderId());
|
||||
}
|
||||
|
||||
private Optional<Dispute> findDispute(ChatMessage message) {
|
||||
public Optional<Dispute> findDispute(ChatMessage message) {
|
||||
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();
|
||||
if (disputeList == 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.MessageListener;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.common.MoneroRpcConnection;
|
||||
import monero.wallet.MoneroWallet;
|
||||
import monero.wallet.model.MoneroDestination;
|
||||
import monero.wallet.model.MoneroMultisigSignResult;
|
||||
@ -87,9 +88,11 @@ import monero.wallet.model.MoneroTxWallet;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
@ -168,7 +171,28 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
|
||||
@Override
|
||||
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
|
||||
@ -216,7 +240,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
ThreadUtils.execute(() -> {
|
||||
ChatMessage chatMessage = null;
|
||||
Dispute dispute = null;
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
try {
|
||||
DisputeResult disputeResult = disputeClosedMessage.getDisputeResult();
|
||||
chatMessage = disputeResult.getChatMessage();
|
||||
@ -252,7 +276,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
|
||||
// save dispute closed message for reprocessing
|
||||
trade.getArbitrator().setDisputeClosedMessage(disputeClosedMessage);
|
||||
requestPersistence();
|
||||
requestPersistence(trade);
|
||||
|
||||
// verify arbitrator does not receive DisputeClosedMessage
|
||||
if (keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing())) {
|
||||
@ -263,11 +287,13 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
|
||||
// set dispute state
|
||||
cleanupRetryMap(uid);
|
||||
synchronized (dispute.getChatMessages()) {
|
||||
if (!dispute.getChatMessages().contains(chatMessage)) {
|
||||
dispute.addAndPersistChatMessage(chatMessage);
|
||||
} else {
|
||||
log.warn("We got a dispute mail msg that we have already stored. TradeId = " + chatMessage.getTradeId());
|
||||
}
|
||||
}
|
||||
dispute.setIsClosed();
|
||||
if (dispute.disputeResultProperty().get() != null) {
|
||||
log.info("We already got a dispute result, indicating the message was resent after updating multisig info. TradeId = " + tradeId);
|
||||
@ -308,7 +334,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
if (trade.isPayoutPublished()) {
|
||||
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
@ -326,17 +352,21 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
// 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.
|
||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
|
||||
requestPersistence();
|
||||
requestPersistence(trade);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error processing dispute closed message: " + e.getMessage());
|
||||
log.warn("Error processing dispute closed message: {}", e.getMessage());
|
||||
e.printStackTrace();
|
||||
requestPersistence();
|
||||
requestPersistence(trade);
|
||||
|
||||
// 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.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());
|
||||
requestPersistence();
|
||||
HavenoUtils.havenoSetup.getTopErrorMsg().set(warningMsg);
|
||||
requestPersistence(trade);
|
||||
throw e;
|
||||
}
|
||||
|
||||
@ -356,7 +386,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
public void maybeReprocessDisputeClosedMessage(Trade trade, boolean reprocessOnError) {
|
||||
if (trade.isShutDownStarted()) return;
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
|
||||
// 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()) {
|
||||
@ -442,12 +472,16 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
|
||||
// sign arbitrator-signed payout tx
|
||||
if (trade.getPayoutTxHex() == null) {
|
||||
try {
|
||||
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex);
|
||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
||||
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
||||
disputeTxSet.setMultisigTxHex(signedMultisigTxHex);
|
||||
trade.setPayoutTxHex(signedMultisigTxHex);
|
||||
requestPersistence();
|
||||
requestPersistence(trade);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e.getMessage());
|
||||
}
|
||||
|
||||
// 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?
|
||||
@ -470,6 +504,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
|
||||
// submit fully signed payout tx to the network
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||
try {
|
||||
List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex());
|
||||
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());
|
||||
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 (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection();
|
||||
if (trade.getXmrConnectionService().isConnected()) trade.requestSwitchToNextBestConnection(sourceConnection);
|
||||
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.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
|
||||
dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash());
|
||||
requestPersistence(trade);
|
||||
return disputeTxSet;
|
||||
}
|
||||
|
||||
|
@ -28,6 +28,7 @@ import haveno.common.crypto.KeyRing;
|
||||
import haveno.common.crypto.PubKeyRing;
|
||||
import haveno.common.crypto.Sig;
|
||||
import haveno.common.util.Utilities;
|
||||
import haveno.core.api.XmrConnectionService;
|
||||
import haveno.core.app.HavenoSetup;
|
||||
import haveno.core.offer.OfferPayload;
|
||||
import haveno.core.offer.OpenOfferManager;
|
||||
@ -106,6 +107,7 @@ public class HavenoUtils {
|
||||
public static HavenoSetup havenoSetup;
|
||||
public static ArbitrationManager arbitrationManager;
|
||||
public static XmrWalletService xmrWalletService;
|
||||
public static XmrConnectionService xmrConnectionService;
|
||||
public static OpenOfferManager openOfferManager;
|
||||
|
||||
public static boolean isSeedNode() {
|
||||
@ -502,4 +504,20 @@ public class HavenoUtils {
|
||||
else if (Config.baseCurrencyNetwork().isStagenet()) return 38081;
|
||||
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.taskrunner.Model;
|
||||
import haveno.common.util.Utilities;
|
||||
import haveno.core.api.XmrConnectionService;
|
||||
import haveno.core.monetary.Price;
|
||||
import haveno.core.monetary.Volume;
|
||||
import haveno.core.network.MessageState;
|
||||
@ -69,6 +68,7 @@ import haveno.core.trade.protocol.TradeProtocol;
|
||||
import haveno.core.trade.statistics.TradeStatistics3;
|
||||
import haveno.core.util.VolumeUtil;
|
||||
import haveno.core.xmr.model.XmrAddressEntry;
|
||||
import haveno.core.xmr.wallet.XmrWalletBase;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
import haveno.network.p2p.AckMessage;
|
||||
import haveno.network.p2p.NodeAddress;
|
||||
@ -76,14 +76,12 @@ import haveno.network.p2p.P2PService;
|
||||
import haveno.network.p2p.network.TorNetworkNode;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
@ -135,19 +133,18 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
* stored in the task model.
|
||||
*/
|
||||
@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 long SHUTDOWN_TIMEOUT_MS = 60000;
|
||||
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 EXTENDED_RPC_TIMEOUT = 600000; // 10 minutes
|
||||
private static final long DELETE_AFTER_MS = TradeProtocol.TRADE_STEP_TIMEOUT_SECONDS;
|
||||
private final Object walletLock = new Object();
|
||||
private final Object pollLock = new Object();
|
||||
private final LongProperty walletHeight = new SimpleLongProperty(0);
|
||||
private MoneroWallet wallet;
|
||||
private boolean wasWalletSynced;
|
||||
protected final Object pollLock = new Object();
|
||||
protected static final Object importMultisigLock = new Object();
|
||||
private boolean pollInProgress;
|
||||
private boolean restartInProgress;
|
||||
private Subscription protocolErrorStateSubscription;
|
||||
@ -321,7 +318,11 @@ public abstract class Trade implements Tradable, Model {
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -409,9 +410,6 @@ public abstract class Trade implements Tradable, Model {
|
||||
// Immutable
|
||||
@Getter
|
||||
transient final private XmrWalletService xmrWalletService;
|
||||
@Getter
|
||||
transient final private XmrConnectionService xmrConnectionService;
|
||||
|
||||
transient final private DoubleProperty initProgressProperty = new SimpleDoubleProperty(0.0);
|
||||
transient final private ObjectProperty<State> stateProperty = new SimpleObjectProperty<>(state);
|
||||
transient final private ObjectProperty<Phase> phaseProperty = new SimpleObjectProperty<>(state.phase);
|
||||
@ -437,10 +435,6 @@ public abstract class Trade implements Tradable, Model {
|
||||
@Getter
|
||||
transient private boolean isInitialized;
|
||||
transient private boolean isFullyInitialized;
|
||||
@Getter
|
||||
transient private boolean isShutDownStarted;
|
||||
@Getter
|
||||
transient private boolean isShutDown;
|
||||
|
||||
// Added in v1.2.0
|
||||
transient private ObjectProperty<BigInteger> tradeAmountProperty;
|
||||
@ -507,11 +501,12 @@ public abstract class Trade implements Tradable, Model {
|
||||
@Nullable NodeAddress makerNodeAddress,
|
||||
@Nullable NodeAddress takerNodeAddress,
|
||||
@Nullable NodeAddress arbitratorNodeAddress) {
|
||||
super();
|
||||
this.offer = offer;
|
||||
this.amount = tradeAmount.longValueExact();
|
||||
this.price = tradePrice;
|
||||
this.xmrWalletService = xmrWalletService;
|
||||
this.xmrConnectionService = xmrWalletService.getConnectionService();
|
||||
this.xmrConnectionService = xmrWalletService.getXmrConnectionService();
|
||||
this.processModel = processModel;
|
||||
this.uid = uid;
|
||||
this.takeOfferDate = new Date().getTime();
|
||||
@ -846,8 +841,8 @@ public abstract class Trade implements Tradable, Model {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean requestSwitchToNextBestConnection() {
|
||||
if (xmrConnectionService.requestSwitchToNextBestConnection()) {
|
||||
public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
|
||||
if (xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection)) {
|
||||
onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread
|
||||
return true;
|
||||
}
|
||||
@ -891,10 +886,6 @@ public abstract class Trade implements Tradable, Model {
|
||||
}).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?
|
||||
private boolean isInvalidImportError(String errMsg) {
|
||||
return errMsg.contains("Failed to parse hex") || errMsg.contains("Multisig info is for a different account");
|
||||
@ -1058,15 +1049,27 @@ public abstract class Trade implements Tradable, Model {
|
||||
public MoneroTxWallet createTx(MoneroTxConfig txConfig) {
|
||||
synchronized (walletLock) {
|
||||
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() {
|
||||
synchronized (walletLock) {
|
||||
synchronized (HavenoUtils.getDaemonLock()) { // lock on daemon because import calls full refresh
|
||||
synchronized (importMultisigLock) {
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||
try {
|
||||
doImportMultisigHex();
|
||||
break;
|
||||
@ -1074,15 +1077,15 @@ public abstract class Trade implements Tradable, Model {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to import multisig hex, tradeId={}, attempt={}/{}, error={}", getShortId(), i + 1, TradeProtocol.MAX_ATTEMPTS, e.getMessage());
|
||||
handleWalletError(e, sourceConnection);
|
||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
|
||||
if (isReadTimeoutError(e.getMessage())) forceRestartTradeWallet(); // wallet can be stuck a while
|
||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void doImportMultisigHex() {
|
||||
|
||||
@ -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());
|
||||
}
|
||||
|
||||
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) {
|
||||
if (multisigHex.equals(getArbitrator().getUpdatedMultisigHex())) return "arbitrator";
|
||||
if (multisigHex.equals(getBuyer().getUpdatedMultisigHex())) return "buyer";
|
||||
@ -1175,14 +1184,15 @@ public abstract class Trade implements Tradable, Model {
|
||||
synchronized (walletLock) {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||
try {
|
||||
return doCreatePayoutTx();
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
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 (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
|
||||
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));
|
||||
|
||||
// update state
|
||||
saveWallet();
|
||||
BigInteger payoutTxFeeSplit = payoutTx.getFee().divide(BigInteger.valueOf(2));
|
||||
getBuyer().setPayoutTxFee(payoutTxFeeSplit);
|
||||
getBuyer().setPayoutAmount(HavenoUtils.getDestination(buyerPayoutAddress, payoutTx).getAmount());
|
||||
getSeller().setPayoutTxFee(payoutTxFeeSplit);
|
||||
getSeller().setPayoutAmount(HavenoUtils.getDestination(sellerPayoutAddress, payoutTx).getAmount());
|
||||
getSelf().setUpdatedMultisigHex(wallet.exportMultisigHex());
|
||||
return payoutTx;
|
||||
}
|
||||
|
||||
@ -1234,6 +1242,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
synchronized (walletLock) {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||
try {
|
||||
if (wallet.isMultisigImportNeeded()) throw new IllegalStateException("Cannot create dispute payout tx because multisig import is needed for " + getClass().getSimpleName() + " " + getShortId());
|
||||
return createTx(txConfig);
|
||||
@ -1242,8 +1251,8 @@ public abstract class Trade implements Tradable, Model {
|
||||
} catch (Exception e) {
|
||||
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());
|
||||
handleWalletError(e, sourceConnection);
|
||||
if (i == TradeProtocol.MAX_ATTEMPTS - 1) throw e;
|
||||
if (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
|
||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||
}
|
||||
}
|
||||
@ -1263,6 +1272,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
synchronized (walletLock) {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||
try {
|
||||
doProcessPayoutTx(payoutTxHex, sign, publish);
|
||||
break;
|
||||
@ -1270,9 +1280,12 @@ public abstract class Trade implements Tradable, Model {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
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 (xmrConnectionService.isConnected()) requestSwitchToNextBestConnection();
|
||||
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
|
||||
for (int i = 0; i < 20; i++) {
|
||||
synchronized (this) {
|
||||
synchronized (getLock()) {
|
||||
HavenoUtils.waitFor(10);
|
||||
}
|
||||
}
|
||||
|
||||
// shut down trade threads
|
||||
synchronized (this) {
|
||||
synchronized (getLock()) {
|
||||
isInitialized = false;
|
||||
isShutDown = true;
|
||||
List<Runnable> shutDownThreads = new ArrayList<>();
|
||||
@ -2299,7 +2312,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
private void doPublishTradeStatistics() {
|
||||
String referralId = processModel.getReferralIdService().getOptionalReferralId().orElse(null);
|
||||
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()) {
|
||||
log.info("Publishing trade statistics for {} {}", getClass().getSimpleName(), getId());
|
||||
processModel.getP2PService().addPersistableNetworkPayload(tradeStatistics, true);
|
||||
@ -2337,7 +2350,10 @@ public abstract class Trade implements Tradable, Model {
|
||||
// check if ignored
|
||||
if (isShutDownStarted) 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)
|
||||
String oldProxyUri = wallet.getDaemonConnection() == null ? null : wallet.getDaemonConnection().getProxyUri();
|
||||
@ -2397,7 +2413,9 @@ public abstract class Trade implements Tradable, Model {
|
||||
}
|
||||
|
||||
private void syncWallet(boolean pollWallet) {
|
||||
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||
try {
|
||||
synchronized (walletLock) {
|
||||
if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId());
|
||||
if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId());
|
||||
if (isWalletBehind()) {
|
||||
@ -2406,6 +2424,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
syncWalletIfBehind();
|
||||
log.info("Done syncing wallet for {} {} in {} ms", getClass().getSimpleName(), getShortId(), System.currentTimeMillis() - startTime);
|
||||
}
|
||||
}
|
||||
|
||||
// apply tor after wallet synced depending on configuration
|
||||
if (!wasWalletSynced) {
|
||||
@ -2417,7 +2436,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
|
||||
if (pollWallet) pollWallet();
|
||||
} catch (Exception e) {
|
||||
ThreadUtils.execute(() -> requestSwitchToNextBestConnection(), getId());
|
||||
ThreadUtils.execute(() -> requestSwitchToNextBestConnection(sourceConnection), getId());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@ -2546,11 +2565,12 @@ public abstract class Trade implements Tradable, Model {
|
||||
|
||||
// rescan spent outputs to detect unconfirmed payout tx
|
||||
if (isPayoutExpected && wallet.getBalance().compareTo(BigInteger.ZERO) > 0) {
|
||||
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||
try {
|
||||
wallet.rescanSpent();
|
||||
} catch (Exception e) {
|
||||
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) {
|
||||
boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused");
|
||||
if (isConnectionRefused) forceRestartTradeWallet();
|
||||
if (HavenoUtils.isUnresponsive(e)) forceRestartTradeWallet();
|
||||
else {
|
||||
boolean isWalletConnected = isWalletConnectedToDaemon();
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -2613,9 +2632,15 @@ public abstract class Trade implements Tradable, Model {
|
||||
}
|
||||
|
||||
private void syncWalletIfBehind() {
|
||||
if (isWalletBehind()) {
|
||||
synchronized (walletLock) {
|
||||
if (isWalletBehind()) {
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
@ -2660,7 +2685,8 @@ public abstract class Trade implements Tradable, Model {
|
||||
log.warn("Rescanning blockchain for {} {}", getClass().getSimpleName(), getShortId());
|
||||
wallet.rescanBlockchain();
|
||||
} 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;
|
||||
} finally {
|
||||
|
||||
@ -2790,7 +2816,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
if (!isInitialized || isShutDownStarted) return;
|
||||
if (isWalletConnectedToDaemon()) {
|
||||
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());
|
||||
|
@ -45,7 +45,7 @@ public class ArbitratorProtocol extends DisputeProtocol {
|
||||
public void handleInitTradeRequest(InitTradeRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) {
|
||||
System.out.println("ArbitratorProtocol.handleInitTradeRequest()");
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
latchTrade();
|
||||
this.errorMessageHandler = errorMessageHandler;
|
||||
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) {
|
||||
System.out.println("ArbitratorProtocol.handleDepositRequest() " + trade.getId());
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
latchTrade();
|
||||
Validator.checkTradeId(processModel.getOfferId(), request);
|
||||
processModel.setTradeMessage(request);
|
||||
|
@ -62,7 +62,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
latchTrade();
|
||||
this.errorMessageHandler = errorMessageHandler;
|
||||
expect(phase(Trade.Phase.INIT)
|
||||
|
@ -70,7 +70,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
System.out.println(getClass().getSimpleName() + ".onTakeOffer()");
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
latchTrade();
|
||||
this.tradeResultHandler = tradeResultHandler;
|
||||
this.errorMessageHandler = errorMessageHandler;
|
||||
@ -101,7 +101,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
|
||||
NodeAddress peer) {
|
||||
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
latchTrade();
|
||||
expect(phase(Trade.Phase.INIT)
|
||||
.with(message)
|
||||
|
@ -75,7 +75,7 @@ public class BuyerProtocol extends DisputeProtocol {
|
||||
// re-send payment sent message if not acked
|
||||
ThreadUtils.execute(() -> {
|
||||
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
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()) {
|
||||
latchTrade();
|
||||
@ -121,7 +121,7 @@ public class BuyerProtocol extends DisputeProtocol {
|
||||
public void onPaymentSent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
System.out.println("BuyerProtocol.onPaymentSent()");
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
latchTrade();
|
||||
this.errorMessageHandler = errorMessageHandler;
|
||||
BuyerEvent event = BuyerEvent.PAYMENT_SENT;
|
||||
|
@ -67,7 +67,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
latchTrade();
|
||||
this.errorMessageHandler = errorMessageHandler;
|
||||
expect(phase(Trade.Phase.INIT)
|
||||
|
@ -70,7 +70,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
System.out.println(getClass().getSimpleName() + ".onTakeOffer()");
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
latchTrade();
|
||||
this.tradeResultHandler = tradeResultHandler;
|
||||
this.errorMessageHandler = errorMessageHandler;
|
||||
@ -101,7 +101,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
|
||||
NodeAddress peer) {
|
||||
System.out.println(getClass().getCanonicalName() + ".handleInitTradeRequest()");
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
latchTrade();
|
||||
expect(phase(Trade.Phase.INIT)
|
||||
.with(message)
|
||||
|
@ -70,7 +70,7 @@ public class SellerProtocol extends DisputeProtocol {
|
||||
// re-send payment received message if payout not published
|
||||
ThreadUtils.execute(() -> {
|
||||
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
if (trade.isShutDownStarted() || trade.isPayoutPublished()) return;
|
||||
if (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !trade.isPayoutPublished()) {
|
||||
latchTrade();
|
||||
@ -117,7 +117,7 @@ public class SellerProtocol extends DisputeProtocol {
|
||||
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
log.info("SellerProtocol.onPaymentReceived()");
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
latchTrade();
|
||||
this.errorMessageHandler = errorMessageHandler;
|
||||
SellerEvent event = SellerEvent.PAYMENT_RECEIVED;
|
||||
|
@ -243,7 +243,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
if (!trade.isCompleted()) processModel.getP2PService().addDecryptedDirectMessageListener(this);
|
||||
|
||||
// initialize trade
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
trade.initialize(processModel.getProvider());
|
||||
|
||||
// process mailbox messages
|
||||
@ -261,7 +261,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
ThreadUtils.execute(() -> {
|
||||
if (!trade.isDepositsConfirmed() || trade.isDepositsConfirmedAcked() || trade.isPayoutPublished() || depositsConfirmedTasksCalled) return;
|
||||
depositsConfirmedTasksCalled = true;
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
if (!trade.isInitialized() || trade.isShutDownStarted()) return; // skip if shutting down
|
||||
latchTrade();
|
||||
expect(new Condition(trade))
|
||||
@ -282,7 +282,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
public void maybeReprocessPaymentReceivedMessage(boolean reprocessOnError) {
|
||||
if (trade.isShutDownStarted()) return;
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
|
||||
// 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())) {
|
||||
@ -299,7 +299,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
||||
trade.addInitProgressStep();
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
|
||||
// check trade
|
||||
if (trade.hasFailed()) {
|
||||
@ -335,7 +335,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
||||
System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() for " + trade.getClass().getSimpleName() + " " + trade.getShortId());
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
|
||||
// check trade
|
||||
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());
|
||||
trade.addInitProgressStep();
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
|
||||
// check trade
|
||||
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());
|
||||
trade.addInitProgressStep();
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
Validator.checkTradeId(processModel.getOfferId(), response);
|
||||
latchTrade();
|
||||
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());
|
||||
if (!trade.isInitialized() || trade.isShutDown()) return;
|
||||
ThreadUtils.execute(() -> {
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
if (!trade.isInitialized() || trade.isShutDown()) return;
|
||||
latchTrade();
|
||||
this.errorMessageHandler = null;
|
||||
@ -493,7 +493,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
// a mailbox message with PaymentSentMessage.
|
||||
// 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.
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
if (!trade.isInitialized() || trade.isShutDown()) return;
|
||||
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());
|
||||
@ -542,7 +542,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator");
|
||||
return;
|
||||
}
|
||||
synchronized (trade) {
|
||||
synchronized (trade.getLock()) {
|
||||
if (!trade.isInitialized() || trade.isShutDown()) return;
|
||||
latchTrade();
|
||||
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) {
|
||||
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) {
|
||||
sendAckMessage(ackReceiver, message, false, errorMessage);
|
||||
|
@ -87,6 +87,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
|
||||
MoneroTxWallet payoutTx = trade.createPayoutTx();
|
||||
trade.updatePayout(payoutTx);
|
||||
trade.getSelf().setUnsignedPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||
trade.requestPersistence();
|
||||
}
|
||||
|
||||
complete();
|
||||
@ -163,7 +164,8 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
|
||||
// convert info to csv
|
||||
Integer length = null;
|
||||
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));
|
||||
@ -203,5 +205,3 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -28,9 +28,9 @@ import haveno.core.trade.Trade.State;
|
||||
import haveno.core.trade.messages.SignContractRequest;
|
||||
import haveno.core.trade.protocol.TradeProtocol;
|
||||
import haveno.core.xmr.model.XmrAddressEntry;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
import haveno.network.p2p.SendDirectMessageListener;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.common.MoneroRpcConnection;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
|
||||
import java.math.BigInteger;
|
||||
@ -77,7 +77,7 @@ public class MaybeSendSignContractRequest extends TradeTask {
|
||||
|
||||
// create deposit tx and freeze inputs
|
||||
MoneroTxWallet depositTx = null;
|
||||
synchronized (XmrWalletService.WALLET_LOCK) {
|
||||
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
|
||||
|
||||
// check for timeout
|
||||
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 {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
|
||||
try {
|
||||
depositTx = trade.getXmrWalletService().createDepositTx(trade, reserveExactAmount, subaddressIndex);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error creating deposit tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
|
||||
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 (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection();
|
||||
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 (trade.isSeller()) trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
|
||||
trade.requestPersistence();
|
||||
|
||||
// update state
|
||||
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();
|
||||
}
|
||||
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
trade.requestPersistence();
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
|
@ -76,8 +76,7 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas
|
||||
|
||||
// export multisig hex once
|
||||
if (trade.getSelf().getUpdatedMultisigHex() == null) {
|
||||
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
trade.exportMultisigHex();
|
||||
}
|
||||
|
||||
// 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.protocol.TradeProtocol;
|
||||
import haveno.core.xmr.model.XmrAddressEntry;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.common.MoneroRpcConnection;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
|
||||
import java.math.BigInteger;
|
||||
@ -49,7 +49,7 @@ public class TakerReserveTradeFunds extends TradeTask {
|
||||
|
||||
// create reserve tx
|
||||
MoneroTxWallet reserveTx = null;
|
||||
synchronized (XmrWalletService.WALLET_LOCK) {
|
||||
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
|
||||
|
||||
// check for timeout
|
||||
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 {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
MoneroRpcConnection sourceConnection = trade.getXmrConnectionService().getConnection();
|
||||
try {
|
||||
reserveTx = model.getXmrWalletService().createReserveTx(penaltyFee, takerFee, sendAmount, securityDeposit, returnAddress, false, null);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error creating reserve tx, attempt={}/{}, tradeId={}, error={}", i + 1, TradeProtocol.MAX_ATTEMPTS, trade.getShortId(), e.getMessage());
|
||||
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 (trade.getXmrConnectionService().isConnected()) trade.getXmrWalletService().requestSwitchToNextBestConnection();
|
||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||
}
|
||||
|
||||
|
@ -69,10 +69,13 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
|
||||
|
||||
@JsonExclude
|
||||
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,
|
||||
@Nullable String referralId,
|
||||
boolean isTorNetworkNode) {
|
||||
boolean isTorNetworkNode,
|
||||
boolean isFuzzed) {
|
||||
Map<String, String> extraDataMap = new HashMap<>();
|
||||
if (referralId != null) {
|
||||
extraDataMap.put(OfferPayload.REFERRAL_ID, referralId);
|
||||
@ -90,9 +93,9 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
|
||||
Offer offer = checkNotNull(trade.getOffer());
|
||||
return new TradeStatistics3(offer.getCurrencyCode(),
|
||||
trade.getPrice().getValue(),
|
||||
fuzzTradeAmountReproducibly(trade),
|
||||
isFuzzed ? fuzzTradeAmountReproducibly(trade) : trade.getAmount().longValueExact(),
|
||||
offer.getPaymentMethod().getId(),
|
||||
fuzzTradeDateReproducibly(trade),
|
||||
isFuzzed ? fuzzTradeDateReproducibly(trade) : trade.getTakeOfferDate().getTime(),
|
||||
truncatedArbitratorNodeAddress,
|
||||
extraDataMap);
|
||||
}
|
||||
@ -101,8 +104,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
|
||||
long originalTimestamp = trade.getTakeOfferDate().getTime();
|
||||
long exactAmount = trade.getAmount().longValueExact();
|
||||
Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp
|
||||
long adjustedAmount = (long) random.nextDouble(
|
||||
exactAmount * 0.95, exactAmount * 1.05);
|
||||
long adjustedAmount = (long) random.nextDouble(exactAmount * (1.0 - FUZZ_AMOUNT_PCT), exactAmount * (1 + FUZZ_AMOUNT_PCT));
|
||||
log.debug("trade {} fuzzed trade amount for tradeStatistics is {}", trade.getShortId(), adjustedAmount);
|
||||
return adjustedAmount;
|
||||
}
|
||||
@ -110,8 +112,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
|
||||
private static long fuzzTradeDateReproducibly(Trade trade) { // randomize completed trade info #1099
|
||||
long originalTimestamp = trade.getTakeOfferDate().getTime();
|
||||
Random random = new Random(originalTimestamp); // pseudo random generator seeded from take offer datestamp
|
||||
long adjustedTimestamp = random.nextLong(
|
||||
originalTimestamp-TimeUnit.HOURS.toMillis(24), originalTimestamp);
|
||||
long adjustedTimestamp = random.nextLong(originalTimestamp - TimeUnit.HOURS.toMillis(FUZZ_DATE_HOURS), originalTimestamp);
|
||||
log.debug("trade {} fuzzed trade datestamp for tradeStatistics is {}", trade.getShortId(), new Date(adjustedTimestamp));
|
||||
return adjustedTimestamp;
|
||||
}
|
||||
|
@ -110,35 +110,54 @@ public class TradeStatisticsManager {
|
||||
maybeDumpStatistics();
|
||||
}
|
||||
|
||||
private void deduplicateEarlyTradeStatistics(Set<TradeStatistics3> set) {
|
||||
private void deduplicateEarlyTradeStatistics(Set<TradeStatistics3> tradeStats) {
|
||||
|
||||
// collect trades before May 31, 2024
|
||||
Set<TradeStatistics3> tradesBeforeMay31_24 = set.stream()
|
||||
.filter(e -> e.getDate().toInstant().isBefore(Instant.parse("2024-05-31T00:00:00Z")))
|
||||
// collect trades before August 7, 2024
|
||||
Set<TradeStatistics3> earlyTrades = tradeStats.stream()
|
||||
.filter(e -> e.getDate().toInstant().isBefore(Instant.parse("2024-08-07T00:00:00Z")))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// collect duplicated trades
|
||||
Set<TradeStatistics3> duplicated = new HashSet<TradeStatistics3>();
|
||||
Set<TradeStatistics3> deduplicated = new HashSet<TradeStatistics3>();
|
||||
for (TradeStatistics3 tradeStatistics : tradesBeforeMay31_24) {
|
||||
if (hasLenientDuplicate(tradeStatistics, deduplicated)) duplicated.add(tradeStatistics);
|
||||
else deduplicated.add(tradeStatistics);
|
||||
Set<TradeStatistics3> duplicates = new HashSet<TradeStatistics3>();
|
||||
Set<TradeStatistics3> deduplicates = new HashSet<TradeStatistics3>();
|
||||
Set<TradeStatistics3> usedAsDuplicate = new HashSet<TradeStatistics3>();
|
||||
for (TradeStatistics3 tradeStatistic : earlyTrades) {
|
||||
TradeStatistics3 fuzzyDuplicate = findFuzzyDuplicate(tradeStatistic, deduplicates, usedAsDuplicate);
|
||||
if (fuzzyDuplicate == null) deduplicates.add(tradeStatistic);
|
||||
else {
|
||||
duplicates.add(tradeStatistic);
|
||||
usedAsDuplicate.add(fuzzyDuplicate);
|
||||
}
|
||||
}
|
||||
|
||||
// remove duplicated trades
|
||||
set.removeAll(duplicated);
|
||||
tradeStats.removeAll(duplicates);
|
||||
}
|
||||
|
||||
private boolean hasLenientDuplicate(TradeStatistics3 tradeStatistics, Set<TradeStatistics3> set) {
|
||||
return set.stream().anyMatch(e -> isLenientDuplicate(tradeStatistics, e));
|
||||
private TradeStatistics3 findFuzzyDuplicate(TradeStatistics3 tradeStatistics, Set<TradeStatistics3> set, Set<TradeStatistics3> excluded) {
|
||||
return set.stream().filter(e -> !excluded.contains(e)).filter(e -> isFuzzyDuplicate(tradeStatistics, e)).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
private boolean isLenientDuplicate(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) {
|
||||
boolean isWithin2Minutes = Math.abs(tradeStatistics1.getDate().getTime() - tradeStatistics2.getDate().getTime()) < 120000;
|
||||
return isWithin2Minutes &&
|
||||
tradeStatistics1.getCurrency().equals(tradeStatistics2.getCurrency()) &&
|
||||
tradeStatistics1.getAmount() == tradeStatistics2.getAmount() &&
|
||||
tradeStatistics1.getPrice() == tradeStatistics2.getPrice();
|
||||
private boolean isFuzzyDuplicate(TradeStatistics3 tradeStatistics1, TradeStatistics3 tradeStatistics2) {
|
||||
if (!tradeStatistics1.getPaymentMethodId().equals(tradeStatistics2.getPaymentMethodId())) return false;
|
||||
if (!tradeStatistics1.getCurrency().equals(tradeStatistics2.getCurrency())) return false;
|
||||
if (tradeStatistics1.getPrice() != tradeStatistics2.getPrice()) return false;
|
||||
return isFuzzyDuplicateV1(tradeStatistics1, tradeStatistics2) || isFuzzyDuplicateV2(tradeStatistics1, tradeStatistics2);
|
||||
}
|
||||
|
||||
// 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() {
|
||||
@ -206,13 +225,23 @@ public class TradeStatisticsManager {
|
||||
|
||||
TradeStatistics3 tradeStatistics3 = null;
|
||||
try {
|
||||
tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode);
|
||||
tradeStatistics3 = TradeStatistics3.from(trade, referralId, isTorNetworkNode, false);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error getting trade statistic for {} {}: {}", trade.getClass().getName(), trade.getId(), e.getMessage());
|
||||
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()));
|
||||
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.",
|
||||
trade.getShortId());
|
||||
return;
|
||||
|
@ -44,6 +44,7 @@ import haveno.core.offer.OpenOfferManager;
|
||||
import haveno.core.support.dispute.Dispute;
|
||||
import haveno.core.support.dispute.refund.RefundManager;
|
||||
import haveno.core.trade.ClosedTradableManager;
|
||||
import haveno.core.trade.HavenoUtils;
|
||||
import haveno.core.trade.MakerTrade;
|
||||
import haveno.core.trade.Trade;
|
||||
import haveno.core.trade.TradeManager;
|
||||
@ -124,7 +125,7 @@ public class Balances {
|
||||
|
||||
private void doUpdateBalances() {
|
||||
synchronized (this) {
|
||||
synchronized (XmrWalletService.WALLET_LOCK) {
|
||||
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
|
||||
|
||||
// get wallet balances
|
||||
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 haveno.common.ThreadUtils;
|
||||
import haveno.common.Timer;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.config.Config;
|
||||
import haveno.common.file.FileUtil;
|
||||
@ -43,7 +42,6 @@ import haveno.core.user.User;
|
||||
import haveno.core.xmr.listeners.XmrBalanceListener;
|
||||
import haveno.core.xmr.model.XmrAddressEntry;
|
||||
import haveno.core.xmr.model.XmrAddressEntryList;
|
||||
import haveno.core.xmr.setup.DownloadListener;
|
||||
import haveno.core.xmr.setup.MoneroWalletRpcManager;
|
||||
import haveno.core.xmr.setup.WalletsSetup;
|
||||
import java.io.File;
|
||||
@ -55,26 +53,22 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import lombok.Getter;
|
||||
import monero.common.MoneroError;
|
||||
import monero.common.MoneroRpcConnection;
|
||||
import monero.common.MoneroRpcError;
|
||||
@ -103,14 +97,13 @@ import monero.wallet.model.MoneroTxPriority;
|
||||
import monero.wallet.model.MoneroTxQuery;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
import monero.wallet.model.MoneroWalletConfig;
|
||||
import monero.wallet.model.MoneroWalletListener;
|
||||
import monero.wallet.model.MoneroWalletListenerI;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
||||
public class XmrWalletService {
|
||||
public class XmrWalletService extends XmrWalletBase {
|
||||
private static final Logger log = LoggerFactory.getLogger(XmrWalletService.class);
|
||||
|
||||
// monero configuration
|
||||
@ -134,15 +127,13 @@ public class XmrWalletService {
|
||||
private static final String THREAD_ID = XmrWalletService.class.getSimpleName();
|
||||
private static final long SHUTDOWN_TIMEOUT_MS = 60000;
|
||||
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 Preferences preferences;
|
||||
private final CoreAccountService accountService;
|
||||
private final XmrConnectionService xmrConnectionService;
|
||||
private final XmrAddressEntryList xmrAddressEntryList;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final DownloadListener downloadListener = new DownloadListener();
|
||||
private final LongProperty walletHeight = new SimpleLongProperty(0);
|
||||
|
||||
private final File walletDir;
|
||||
private final File xmrWalletFile;
|
||||
@ -153,24 +144,15 @@ public class XmrWalletService {
|
||||
|
||||
private ChangeListener<? super Number> walletInitListener;
|
||||
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 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 boolean pollInProgress;
|
||||
private Long pollPeriodMs;
|
||||
private Long lastLogPollErrorTimestamp;
|
||||
private long lastLogPollErrorTimestamp;
|
||||
private long lastPollTxsTimestamp;
|
||||
private final Object pollLock = new Object();
|
||||
private Long cachedHeight;
|
||||
private BigInteger cachedBalance;
|
||||
@ -178,7 +160,6 @@ public class XmrWalletService {
|
||||
private List<MoneroSubaddress> cachedSubaddresses;
|
||||
private List<MoneroOutputWallet> cachedOutputs;
|
||||
private List<MoneroTxWallet> cachedTxs;
|
||||
private boolean runReconnectTestOnStartup = false; // test reconnecting on startup while syncing so the wallet is blocked
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Inject
|
||||
@ -194,7 +175,6 @@ public class XmrWalletService {
|
||||
this.user = user;
|
||||
this.preferences = preferences;
|
||||
this.accountService = accountService;
|
||||
this.xmrConnectionService = xmrConnectionService;
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.xmrAddressEntryList = xmrAddressEntryList;
|
||||
this.walletDir = walletDir;
|
||||
@ -202,6 +182,8 @@ public class XmrWalletService {
|
||||
this.useNativeXmrWallet = useNativeXmrWallet;
|
||||
this.xmrWalletFile = new File(walletDir, MONERO_WALLET_NAME);
|
||||
HavenoUtils.xmrWalletService = this;
|
||||
HavenoUtils.xmrConnectionService = xmrConnectionService;
|
||||
this.xmrConnectionService = xmrConnectionService; // TODO: super's is null unless set here from injection
|
||||
|
||||
// set monero logging
|
||||
if (MONERO_LOG_LEVEL >= 0) MoneroUtils.setLogLevel(MONERO_LOG_LEVEL);
|
||||
@ -316,10 +298,6 @@ public class XmrWalletService {
|
||||
return xmrConnectionService.getDaemon();
|
||||
}
|
||||
|
||||
public XmrConnectionService getConnectionService() {
|
||||
return xmrConnectionService;
|
||||
}
|
||||
|
||||
public boolean isProxyApplied() {
|
||||
return isProxyApplied(wasWalletSynced);
|
||||
}
|
||||
@ -420,6 +398,10 @@ public class XmrWalletService {
|
||||
}
|
||||
|
||||
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) {
|
||||
MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) wallet, path, true);
|
||||
} else {
|
||||
@ -456,7 +438,7 @@ public class XmrWalletService {
|
||||
}
|
||||
|
||||
public MoneroTxWallet createTx(MoneroTxConfig txConfig) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
MoneroTxWallet tx = wallet.createTx(txConfig);
|
||||
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) {
|
||||
MoneroTxWallet tx = createTx(new MoneroTxConfig().setAccountIndex(0).setDestinations(destinations).setRelay(false).setCanSplit(false));
|
||||
//printTxs("XmrWalletService.createTx", tx);
|
||||
@ -479,7 +469,7 @@ public class XmrWalletService {
|
||||
* Freeze reserved outputs and thaw unreserved outputs.
|
||||
*/
|
||||
public void fixReservedOutputs() {
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
|
||||
// collect reserved outputs
|
||||
Set<String> reservedKeyImages = new HashSet<String>();
|
||||
@ -498,7 +488,7 @@ public class XmrWalletService {
|
||||
}
|
||||
|
||||
private void freezeReservedOutputs(Set<String> reservedKeyImages) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
|
||||
// ensure wallet is open
|
||||
if (wallet == null) {
|
||||
@ -522,7 +512,7 @@ public class XmrWalletService {
|
||||
}
|
||||
|
||||
private void thawUnreservedOutputs(Set<String> reservedKeyImages) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
|
||||
// ensure wallet is open
|
||||
if (wallet == null) {
|
||||
@ -552,7 +542,7 @@ public class XmrWalletService {
|
||||
*/
|
||||
public void freezeOutputs(Collection<String> keyImages) {
|
||||
if (keyImages == null || keyImages.isEmpty()) return;
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
|
||||
// collect outputs to freeze
|
||||
List<String> unfrozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(false).setIsSpent(false)).stream()
|
||||
@ -574,7 +564,7 @@ public class XmrWalletService {
|
||||
*/
|
||||
public void thawOutputs(Collection<String> keyImages) {
|
||||
if (keyImages == null || keyImages.isEmpty()) return;
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
|
||||
// collect outputs to thaw
|
||||
List<String> frozenKeyImages = getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false)).stream()
|
||||
@ -627,7 +617,7 @@ public class XmrWalletService {
|
||||
* @return the reserve tx
|
||||
*/
|
||||
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()) {
|
||||
log.info("Creating reserve tx with preferred subaddress index={}, return address={}", preferredSubaddressIndex, returnAddress);
|
||||
long time = System.currentTimeMillis();
|
||||
@ -648,7 +638,7 @@ public class XmrWalletService {
|
||||
* @return MoneroTxWallet the multisig deposit tx
|
||||
*/
|
||||
public MoneroTxWallet createDepositTx(Trade trade, boolean reserveExactAmount, Integer preferredSubaddressIndex) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
synchronized (HavenoUtils.getWalletFunctionLock()) {
|
||||
BigInteger feeAmount = trade instanceof MakerTrade ? trade.getMakerFee() : trade.getTakerFee();
|
||||
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) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
MoneroWallet wallet = getWallet();
|
||||
|
||||
// create a list of subaddresses to attempt spending from in preferred order
|
||||
@ -903,7 +893,7 @@ public class XmrWalletService {
|
||||
Runnable shutDownTask = () -> {
|
||||
|
||||
// remove listeners
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
if (wallet != null) {
|
||||
for (MoneroWalletListenerI listener : new HashSet<>(wallet.getListeners())) {
|
||||
wallet.removeListener(listener);
|
||||
@ -913,7 +903,7 @@ public class XmrWalletService {
|
||||
}
|
||||
|
||||
// shut down threads
|
||||
synchronized (this) {
|
||||
synchronized (getLock()) {
|
||||
List<Runnable> shutDownThreads = new ArrayList<>();
|
||||
shutDownThreads.add(() -> ThreadUtils.shutDown(THREAD_ID));
|
||||
ThreadUtils.awaitTasks(shutDownThreads);
|
||||
@ -1285,9 +1275,19 @@ public class XmrWalletService {
|
||||
else log.info(appliedMsg);
|
||||
|
||||
// listen for connection changes
|
||||
xmrConnectionService.addConnectionListener(connection -> ThreadUtils.execute(() -> {
|
||||
xmrConnectionService.addConnectionListener(connection -> {
|
||||
if (wasWalletSynced && !isSyncingWithProgress) {
|
||||
ThreadUtils.execute(() -> {
|
||||
onConnectionChanged(connection);
|
||||
}, THREAD_ID));
|
||||
}, 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
|
||||
walletInitListener = (obs, oldVal, newVal) -> initMainWalletIfConnected();
|
||||
@ -1306,11 +1306,20 @@ public class XmrWalletService {
|
||||
}
|
||||
|
||||
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) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
if (isShutDownStarted) return;
|
||||
|
||||
// open or create wallet main wallet
|
||||
@ -1327,6 +1336,7 @@ public class XmrWalletService {
|
||||
long date = localDateTime.toEpochSecond(ZoneOffset.UTC);
|
||||
user.setWalletCreationDate(date);
|
||||
}
|
||||
walletHeight.set(wallet.getHeight());
|
||||
isClosingWallet = false;
|
||||
}
|
||||
|
||||
@ -1335,6 +1345,7 @@ public class XmrWalletService {
|
||||
log.info("Monero wallet path={}", wallet.getPath());
|
||||
|
||||
// sync main wallet if applicable
|
||||
// TODO: error handling and re-initialization is jenky, refactor
|
||||
if (sync && numAttempts > 0) {
|
||||
try {
|
||||
|
||||
@ -1347,7 +1358,16 @@ public class XmrWalletService {
|
||||
// sync main wallet
|
||||
log.info("Syncing main wallet");
|
||||
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");
|
||||
|
||||
// poll wallet
|
||||
@ -1410,7 +1430,7 @@ public class XmrWalletService {
|
||||
if (baseAddresses.size() > 1 || (baseAddresses.size() == 1 && !baseAddresses.get(0).getAddressString().equals(wallet.getPrimaryAddress()))) {
|
||||
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?
|
||||
HavenoUtils.havenoSetup.getTopErrorMsg().set(warningMsg);
|
||||
HavenoUtils.setTopError(warningMsg);
|
||||
|
||||
// reset address entries
|
||||
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) {
|
||||
|
||||
// must be connected to daemon
|
||||
@ -1529,7 +1461,7 @@ public class XmrWalletService {
|
||||
return walletFull;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
if (walletFull != null) forceCloseMainWallet();
|
||||
if (walletFull != null) forceCloseWallet(walletFull, config.getPath());
|
||||
throw new IllegalStateException("Could not create wallet '" + config.getPath() + "'");
|
||||
}
|
||||
}
|
||||
@ -1661,27 +1593,22 @@ public class XmrWalletService {
|
||||
}
|
||||
|
||||
private void onConnectionChanged(MoneroRpcConnection connection) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
|
||||
// use current connection
|
||||
connection = xmrConnectionService.getConnection();
|
||||
|
||||
// check if ignored
|
||||
if (wallet == null || isShutDownStarted) return;
|
||||
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) return;
|
||||
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);
|
||||
|
||||
// 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();
|
||||
if (HavenoUtils.connectionConfigsEqual(connection, wallet.getDaemonConnection())) {
|
||||
updatePollPeriod();
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 (StringUtils.equals(oldProxyUri, newProxyUri)) {
|
||||
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
|
||||
closeMainWallet(true);
|
||||
doMaybeInitMainWallet(false, MAX_SYNC_ATTEMPTS);
|
||||
return; // wallet is re-initialized
|
||||
return; // wallet re-initializes off thread
|
||||
}
|
||||
} else {
|
||||
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()));
|
||||
log.info("Done changing all wallet passwords");
|
||||
}
|
||||
|
||||
private void closeMainWallet(boolean save) {
|
||||
stopPolling();
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
try {
|
||||
if (wallet != null) {
|
||||
isClosingWallet = true;
|
||||
@ -1758,19 +1685,28 @@ public class XmrWalletService {
|
||||
|
||||
private void forceCloseMainWallet() {
|
||||
stopPolling();
|
||||
if (wallet != null && !isClosingWallet) {
|
||||
isClosingWallet = true;
|
||||
forceCloseWallet(wallet, getWalletPath(MONERO_WALLET_NAME));
|
||||
wallet = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void forceRestartMainWallet() {
|
||||
public void forceRestartMainWallet() {
|
||||
log.warn("Force restarting main wallet");
|
||||
if (isClosingWallet) return;
|
||||
forceCloseMainWallet();
|
||||
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() {
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
if (isShutDownStarted || isPolling()) return;
|
||||
updatePollPeriod();
|
||||
pollLooper = new TaskLooper(() -> pollWallet());
|
||||
@ -1791,15 +1727,15 @@ public class XmrWalletService {
|
||||
|
||||
public void updatePollPeriod() {
|
||||
if (isShutDownStarted) return;
|
||||
setPollPeriod(getPollPeriod());
|
||||
setPollPeriodMs(getPollPeriodMs());
|
||||
}
|
||||
|
||||
private long getPollPeriod() {
|
||||
private long getPollPeriodMs() {
|
||||
return xmrConnectionService.getRefreshPeriodMs();
|
||||
}
|
||||
|
||||
private void setPollPeriod(long pollPeriodMs) {
|
||||
synchronized (WALLET_LOCK) {
|
||||
private void setPollPeriodMs(long pollPeriodMs) {
|
||||
synchronized (walletLock) {
|
||||
if (this.isShutDownStarted) return;
|
||||
if (this.pollPeriodMs != null && this.pollPeriodMs == pollPeriodMs) return;
|
||||
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());
|
||||
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
|
||||
if (walletHeight.get() < xmrConnectionService.getTargetHeight()) {
|
||||
synchronized (WALLET_LOCK) { // avoid long sync from blocking other operations
|
||||
synchronized (walletLock) { // avoid long sync from blocking other operations
|
||||
|
||||
// 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
|
||||
// TODO: ideally wallet should sync every poll and then avoid updating from pool on fetching txs?
|
||||
if (updateTxs) {
|
||||
synchronized (WALLET_LOCK) { // avoid long fetch from blocking other operations
|
||||
synchronized (walletLock) { // avoid long fetch from blocking other operations
|
||||
synchronized (HavenoUtils.getDaemonLock()) {
|
||||
MoneroRpcConnection sourceConnection = xmrConnectionService.getConnection();
|
||||
try {
|
||||
cachedTxs = wallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true));
|
||||
lastPollTxsTimestamp = System.currentTimeMillis();
|
||||
} catch (Exception e) { // fetch from pool can fail
|
||||
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());
|
||||
lastLogPollErrorTimestamp = System.currentTimeMillis();
|
||||
if (System.currentTimeMillis() - lastPollTxsTimestamp > POLL_TXS_TOLERANCE_MS) requestSwitchToNextBestConnection(sourceConnection);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1868,33 +1808,32 @@ public class XmrWalletService {
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (wallet == null || isShutDownStarted) return;
|
||||
boolean isConnectionRefused = e.getMessage() != null && e.getMessage().contains("Connection refused");
|
||||
if (isConnectionRefused) forceRestartMainWallet();
|
||||
if (HavenoUtils.isUnresponsive(e)) forceRestartMainWallet();
|
||||
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();
|
||||
}
|
||||
} finally {
|
||||
synchronized (pollLock) {
|
||||
pollInProgress = false;
|
||||
}
|
||||
|
||||
// cache wallet info last
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
if (wallet != null && !isShutDownStarted) {
|
||||
try {
|
||||
cacheWalletInfo();
|
||||
requestSaveMainWallet();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (pollLock) {
|
||||
pollInProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MoneroSyncResult syncMainWallet() {
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
MoneroSyncResult result = syncWallet(wallet);
|
||||
walletHeight.set(wallet.getHeight());
|
||||
return result;
|
||||
@ -1902,7 +1841,7 @@ public class XmrWalletService {
|
||||
}
|
||||
|
||||
public boolean isWalletConnectedToDaemon() {
|
||||
synchronized (WALLET_LOCK) {
|
||||
synchronized (walletLock) {
|
||||
try {
|
||||
if (wallet == null) return false;
|
||||
return wallet.isConnectedToDaemon();
|
||||
@ -1912,8 +1851,12 @@ public class XmrWalletService {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean requestSwitchToNextBestConnection() {
|
||||
return xmrConnectionService.requestSwitchToNextBestConnection();
|
||||
private boolean requestSwitchToNextBestConnection() {
|
||||
return requestSwitchToNextBestConnection(null);
|
||||
}
|
||||
|
||||
public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
|
||||
return xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection);
|
||||
}
|
||||
|
||||
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 -->
|
||||
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0.9</string>
|
||||
<string>1.0.10</string>
|
||||
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.9</string>
|
||||
<string>1.0.10</string>
|
||||
|
||||
<key>CFBundleExecutable</key>
|
||||
<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 {
|
||||
if (trade == null) {
|
||||
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 {
|
||||
if (txId.equals(trade.getMaker().getDepositTxHash())) tx = trade.getMakerDepositTx();
|
||||
else if (txId.equals(trade.getTaker().getDepositTxHash())) tx = trade.getTakerDepositTx();
|
||||
|
@ -1201,16 +1201,16 @@ textfield */
|
||||
}
|
||||
|
||||
.text-area-no-border {
|
||||
-fx-border-color: -fx-control-inner-background;
|
||||
-fx-border-color: -bs-background-color;
|
||||
}
|
||||
|
||||
.text-area-no-border .content {
|
||||
-fx-background-color: -fx-control-inner-background;
|
||||
-fx-background-color: -bs-background-color;
|
||||
}
|
||||
|
||||
.text-area-no-border:focused {
|
||||
-fx-focus-color: -fx-control-inner-background;
|
||||
-fx-faint-focus-color: -fx-control-inner-background;
|
||||
-fx-focus-color: -bs-background-color;
|
||||
-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.coin.CoinFormatter;
|
||||
import haveno.desktop.common.view.FxmlView;
|
||||
import haveno.desktop.components.AutocompleteComboBox;
|
||||
import haveno.desktop.components.TitledGroupBg;
|
||||
import haveno.desktop.components.paymentmethods.AchTransferForm;
|
||||
import haveno.desktop.components.paymentmethods.AdvancedCashForm;
|
||||
@ -144,7 +145,6 @@ import java.util.stream.Collectors;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.layout.GridPane;
|
||||
@ -181,7 +181,7 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
|
||||
private final AdvancedCashValidator advancedCashValidator;
|
||||
private final TransferwiseValidator transferwiseValidator;
|
||||
private final CoinFormatter formatter;
|
||||
private ComboBox<PaymentMethod> paymentMethodComboBox;
|
||||
private AutocompleteComboBox<PaymentMethod> paymentMethodComboBox;
|
||||
private PaymentMethodForm paymentMethodForm;
|
||||
private TitledGroupBg accountTitledGroupBg;
|
||||
private Button saveNewAccountButton;
|
||||
@ -463,14 +463,16 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
|
||||
removeAccountRows();
|
||||
addAccountButton.setDisable(true);
|
||||
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.setVisibleRowCount(11);
|
||||
paymentMethodComboBox = FormBuilder.addAutocompleteComboBox(
|
||||
root, gridRow, Res.get("shared.selectPaymentMethod"), Layout.FIRST_ROW_AND_GROUP_DISTANCE
|
||||
);
|
||||
paymentMethodComboBox.setVisibleRowCount(Math.min(paymentMethodComboBox.getItems().size(), 10));
|
||||
paymentMethodComboBox.setPrefWidth(250);
|
||||
List<PaymentMethod> list = PaymentMethod.paymentMethods.stream()
|
||||
.filter(PaymentMethod::isTraditional)
|
||||
.sorted()
|
||||
.collect(Collectors.toList());
|
||||
paymentMethodComboBox.setItems(FXCollections.observableArrayList(list));
|
||||
paymentMethodComboBox.setAutocompleteItems(FXCollections.observableArrayList(list));
|
||||
paymentMethodComboBox.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(PaymentMethod paymentMethod) {
|
||||
@ -479,10 +481,15 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
|
||||
|
||||
@Override
|
||||
public PaymentMethod fromString(String s) {
|
||||
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) {
|
||||
FormBuilder.removeRowsFromGridPane(root, 3, paymentMethodForm.getGridRow() + 1);
|
||||
GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1);
|
||||
@ -550,6 +557,7 @@ public class TraditionalAccountsView extends PaymentAccountsView<GridPane, Tradi
|
||||
}
|
||||
|
||||
private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod) {
|
||||
if (paymentMethod == null) return null;
|
||||
final PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod);
|
||||
paymentAccount.init();
|
||||
return getPaymentMethodForm(paymentMethod, paymentAccount);
|
||||
|
@ -65,6 +65,7 @@ import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import monero.common.MoneroRpcConnection;
|
||||
import monero.common.MoneroUtils;
|
||||
import monero.wallet.model.MoneroTxConfig;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
@ -256,6 +257,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
|
||||
// create tx
|
||||
MoneroTxWallet tx = null;
|
||||
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
|
||||
MoneroRpcConnection sourceConnection = xmrWalletService.getXmrConnectionService().getConnection();
|
||||
try {
|
||||
log.info("Creating withdraw tx");
|
||||
long startTime = System.currentTimeMillis();
|
||||
@ -270,7 +272,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
|
||||
if (isNotEnoughMoney(e.getMessage())) throw e;
|
||||
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 (xmrWalletService.getConnectionService().isConnected()) xmrWalletService.requestSwitchToNextBestConnection();
|
||||
if (xmrWalletService.getXmrConnectionService().isConnected()) xmrWalletService.requestSwitchToNextBestConnection(sourceConnection);
|
||||
HavenoUtils.waitFor(TradeProtocol.REPROCESS_DELAY_MS); // wait before retrying
|
||||
}
|
||||
}
|
||||
|
@ -594,7 +594,11 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||
// show confirmation
|
||||
showPayoutTxConfirmation(contract,
|
||||
payoutTx,
|
||||
() -> doClose(closeTicketButton, cancelButton));
|
||||
() -> doClose(closeTicketButton, cancelButton),
|
||||
() -> {
|
||||
closeTicketButton.setDisable(false);
|
||||
cancelButton.setDisable(false);
|
||||
});
|
||||
} else {
|
||||
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)
|
||||
String buyerPayoutAddressString = contract.getBuyerPayoutAddressString();
|
||||
@ -641,6 +645,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||
.actionButtonText(Res.get("shared.yes"))
|
||||
.onAction(() -> resultHandler.handleResult())
|
||||
.closeButtonText(Res.get("shared.cancel"))
|
||||
.onClose(() -> cancelHandler.handleResult())
|
||||
.show();
|
||||
} else {
|
||||
// No payout will be made
|
||||
@ -649,6 +654,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||
.actionButtonText(Res.get("shared.yes"))
|
||||
.onAction(resultHandler::handleResult)
|
||||
.closeButtonText(Res.get("shared.cancel"))
|
||||
.onClose(() -> cancelHandler.handleResult())
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,6 @@ import haveno.core.offer.OfferUtil;
|
||||
import haveno.core.payment.payload.PaymentAccountPayload;
|
||||
import haveno.core.support.SupportType;
|
||||
import haveno.core.support.dispute.Dispute;
|
||||
import haveno.core.support.dispute.DisputeAlreadyOpenException;
|
||||
import haveno.core.support.dispute.DisputeList;
|
||||
import haveno.core.support.dispute.DisputeManager;
|
||||
import haveno.core.support.dispute.arbitration.ArbitrationManager;
|
||||
@ -67,6 +66,7 @@ import haveno.network.p2p.P2PService;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@ -545,32 +545,38 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||
dispute.setExtraData("counterCurrencyExtraData", trade.getCounterCurrencyExtraData());
|
||||
|
||||
trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED);
|
||||
sendDisputeOpenedMessage(dispute, false, disputeManager, trade.getSelf().getUpdatedMultisigHex());
|
||||
sendDisputeOpenedMessage(dispute, disputeManager);
|
||||
tradeManager.requestPersistence();
|
||||
} else if (useArbitration) {
|
||||
// Only if we have completed mediation we allow arbitration
|
||||
disputeManager = arbitrationManager;
|
||||
Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket);
|
||||
sendDisputeOpenedMessage(dispute, false, disputeManager, trade.getSelf().getUpdatedMultisigHex());
|
||||
trade.exportMultisigHex();
|
||||
sendDisputeOpenedMessage(dispute, disputeManager);
|
||||
tradeManager.requestPersistence();
|
||||
} else {
|
||||
log.warn("Invalid dispute state {}", disputeState.name());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDisputeOpenedMessage(Dispute dispute, boolean reOpen, DisputeManager<? extends DisputeList<Dispute>> disputeManager, String senderMultisigHex) {
|
||||
disputeManager.sendDisputeOpenedMessage(dispute, reOpen, senderMultisigHex,
|
||||
() -> navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class), (errorMessage, throwable) -> {
|
||||
if ((throwable instanceof DisputeAlreadyOpenException)) {
|
||||
errorMessage += "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg");
|
||||
new Popup().warning(errorMessage)
|
||||
private void sendDisputeOpenedMessage(Dispute dispute, DisputeManager<? extends DisputeList<Dispute>> disputeManager) {
|
||||
Optional<Dispute> optionalDispute = disputeManager.findDispute(dispute);
|
||||
boolean disputeClosed = optionalDispute.isPresent() && optionalDispute.get().isClosed();
|
||||
if (disputeClosed) {
|
||||
String msg = "We got a dispute already open for that trade and trading peer.\n" + "TradeId = " + dispute.getTradeId();
|
||||
new Popup().warning(msg + "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg"))
|
||||
.actionButtonText(Res.get("portfolio.pending.openAgainDispute.button"))
|
||||
.onAction(() -> sendDisputeOpenedMessage(dispute, true, disputeManager, senderMultisigHex))
|
||||
.onAction(() -> doSendDisputeOpenedMessage(dispute, disputeManager))
|
||||
.closeButtonText(Res.get("shared.cancel")).show();
|
||||
} 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() {
|
||||
|
@ -190,15 +190,13 @@ public abstract class TradeStepView extends AnchorPane {
|
||||
}
|
||||
trade.errorMessageProperty().addListener(errorMessageListener);
|
||||
|
||||
if (!isMediationClosedState()) {
|
||||
tradeStepInfo.setOnAction(e -> {
|
||||
if (this.isTradePeriodOver()) {
|
||||
if (!isArbitrationOpenedState() && this.isTradePeriodOver()) {
|
||||
openSupportTicket();
|
||||
} else {
|
||||
openChat();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -572,7 +570,7 @@ public abstract class TradeStepView extends AnchorPane {
|
||||
}
|
||||
|
||||
private void updateMediationResultState(boolean blockOpeningOfResultAcceptedPopup) {
|
||||
if (isInArbitration()) {
|
||||
if (isInMediation()) {
|
||||
if (isRefundRequestStartedByPeer()) {
|
||||
tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED);
|
||||
} else if (isRefundRequestSelfStarted()) {
|
||||
@ -597,7 +595,7 @@ public abstract class TradeStepView extends AnchorPane {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInArbitration() {
|
||||
private boolean isInMediation() {
|
||||
return isRefundRequestStartedByPeer() || isRefundRequestSelfStarted();
|
||||
}
|
||||
|
||||
@ -613,6 +611,10 @@ public abstract class TradeStepView extends AnchorPane {
|
||||
return trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED;
|
||||
}
|
||||
|
||||
private boolean isArbitrationOpenedState() {
|
||||
return trade.getDisputeState().isOpen();
|
||||
}
|
||||
|
||||
private boolean isTradePeriodOver() {
|
||||
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) {
|
||||
if (trade.getDisputeState() == Trade.DisputeState.NO_DISPUTE) {
|
||||
if (!trade.getDisputeState().isOpen()) {
|
||||
switch (tradePeriodState) {
|
||||
case FIRST_HALF:
|
||||
// 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
|
||||
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);
|
||||
} else {
|
||||
closeDisputeFromButton();
|
||||
|
@ -131,7 +131,7 @@
|
||||
-bs-red-soft: derive(-bs-rd-error-red, 60%);
|
||||
-bs-progress-bar-track: #272728;
|
||||
-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-prompt-text: -bs-color-gray-6;
|
||||
-bs-decimals: #db6300;
|
||||
|
@ -1405,6 +1405,24 @@ public class FormBuilder {
|
||||
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
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -701,7 +701,7 @@ public class GUIUtil {
|
||||
}
|
||||
|
||||
public static boolean isReadyForTxBroadcastOrShowPopup(XmrWalletService xmrWalletService) {
|
||||
XmrConnectionService xmrConnectionService = xmrWalletService.getConnectionService();
|
||||
XmrConnectionService xmrConnectionService = xmrWalletService.getXmrConnectionService();
|
||||
if (!xmrConnectionService.hasSufficientPeersForBroadcast()) {
|
||||
new Popup().information(Res.get("popup.warning.notSufficientConnectionsToXmrNetwork", xmrConnectionService.getMinBroadcastConnections())).show();
|
||||
return false;
|
||||
|
@ -79,17 +79,60 @@ Customize and deploy haveno-pricenode.env and haveno-pricenode.service to run as
|
||||
|
||||
## 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:
|
||||
|
||||
1. [Build the Haveno repository](#fork-and-build-haveno).
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
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.NewTor;
|
||||
import haveno.network.p2p.network.RunningTor;
|
||||
import haveno.network.p2p.network.DirectBindTor;
|
||||
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 javax.annotation.Nullable;
|
||||
|
||||
@ -44,6 +46,7 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
|
||||
@Named(Config.MAX_CONNECTIONS) int maxConnections,
|
||||
@Named(Config.USE_LOCALHOST_FOR_P2P) boolean useLocalhostForP2P,
|
||||
@Named(Config.NODE_PORT) int port,
|
||||
@Named(Config.HIDDEN_SERVICE_ADDRESS) String hiddenServiceAddress,
|
||||
@Named(Config.TOR_DIR) File torDir,
|
||||
@Nullable @Named(Config.TORRC_FILE) File torrcFile,
|
||||
@Named(Config.TORRC_OPTIONS) String torrcOptions,
|
||||
@ -62,10 +65,15 @@ public class NetworkNodeProvider implements Provider<NetworkNode> {
|
||||
torrcOptions,
|
||||
controlHost,
|
||||
controlPort,
|
||||
hiddenServiceAddress,
|
||||
password,
|
||||
cookieFile,
|
||||
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 controlHost,
|
||||
int controlPort,
|
||||
String hiddenServiceAddress,
|
||||
String password,
|
||||
@Nullable File cookieFile,
|
||||
boolean useSafeCookieAuthentication) {
|
||||
return controlPort != Config.UNSPECIFIED_PORT ?
|
||||
new RunningTor(torDir, controlHost, controlPort, password, cookieFile, useSafeCookieAuthentication) :
|
||||
new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider);
|
||||
if (!hiddenServiceAddress.equals("")) {
|
||||
return new DirectBindTor();
|
||||
} else if (controlPort != Config.UNSPECIFIED_PORT) {
|
||||
return new RunningTor(torDir, controlHost, controlPort, password, cookieFile, useSafeCookieAuthentication);
|
||||
} else {
|
||||
return new NewTor(torDir, torrcFile, torrcOptions, bridgeAddressProvider);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -26,6 +26,7 @@ import haveno.common.config.Config;
|
||||
import static haveno.common.config.Config.BAN_LIST;
|
||||
import static haveno.common.config.Config.MAX_CONNECTIONS;
|
||||
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.SOCKS_5_PROXY_HTTP_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(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);
|
||||
|
||||
|
@ -171,9 +171,11 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
||||
private final Capabilities capabilities = new Capabilities();
|
||||
|
||||
// throttle logs of reported invalid requests
|
||||
private static long lastLoggedInvalidRequestReport = 0;
|
||||
private static int unloggedInvalidRequestReports = 0;
|
||||
private static final long LOG_INVALID_REQUEST_REPORTS_INTERVAL_MS = 60000; // log invalid request reports once every 60s
|
||||
private static final long LOG_THROTTLE_INTERVAL_MS = 30000; // throttle logging rule violations and warnings to once every 30 seconds
|
||||
private static long lastLoggedInvalidRequestReportTs = 0;
|
||||
private static int numUnloggedInvalidRequestReports = 0;
|
||||
private static long lastLoggedWarningTs = 0;
|
||||
private static int numUnloggedWarnings = 0;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
@ -218,8 +220,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
||||
if (peersNodeAddress != null) {
|
||||
setPeersNodeAddress(peersNodeAddress);
|
||||
if (banFilter != null && banFilter.isPeerBanned(peersNodeAddress)) {
|
||||
log.warn("We created an outbound connection with a banned peer");
|
||||
reportInvalidRequest(RuleViolation.PEER_BANNED);
|
||||
reportInvalidRequest(RuleViolation.PEER_BANNED, "We created an outbound connection with a banned peer");
|
||||
}
|
||||
}
|
||||
ThreadUtils.execute(() -> connectionListener.onConnection(this), THREAD_ID);
|
||||
@ -249,8 +250,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
||||
if (banFilter != null &&
|
||||
peersNodeAddressOptional.isPresent() &&
|
||||
banFilter.isPeerBanned(peersNodeAddressOptional.get())) {
|
||||
log.warn("We tried to send a message to a banned peer. message={}", networkEnvelope.getClass().getSimpleName());
|
||||
reportInvalidRequest(RuleViolation.PEER_BANNED);
|
||||
String errorMessage = "We tried to send a message to a banned peer. message=" + networkEnvelope.getClass().getSimpleName();
|
||||
reportInvalidRequest(RuleViolation.PEER_BANNED, errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -419,7 +420,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
||||
if (networkEnvelope instanceof SendersNodeAddressMessage) {
|
||||
boolean isValid = processSendersNodeAddressMessage((SendersNodeAddressMessage) networkEnvelope);
|
||||
if (!isValid) {
|
||||
log.warn("Received an invalid {} at processing BundleOfEnvelopes", networkEnvelope.getClass().getSimpleName());
|
||||
throttleWarn("Received an invalid " + networkEnvelope.getClass().getSimpleName() + " at processing BundleOfEnvelopes");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -610,20 +611,20 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
||||
* Runs in same thread as Connection
|
||||
*/
|
||||
|
||||
public boolean reportInvalidRequest(RuleViolation ruleViolation) {
|
||||
return Connection.reportInvalidRequest(this, ruleViolation);
|
||||
public boolean reportInvalidRequest(RuleViolation ruleViolation, String errorMessage) {
|
||||
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
|
||||
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
|
||||
if (!logReport) unloggedInvalidRequestReports++;
|
||||
if (!logReport) numUnloggedInvalidRequestReports++;
|
||||
|
||||
// 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;
|
||||
numRuleViolations = connection.ruleViolations.getOrDefault(ruleViolation, 0);
|
||||
numRuleViolations++;
|
||||
@ -654,9 +655,9 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
||||
|
||||
private static synchronized void resetReportedInvalidRequestsThrottle(boolean logReport) {
|
||||
if (logReport) {
|
||||
if (unloggedInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", unloggedInvalidRequestReports);
|
||||
unloggedInvalidRequestReports = 0;
|
||||
lastLoggedInvalidRequestReport = System.currentTimeMillis();
|
||||
if (numUnloggedInvalidRequestReports > 0) log.warn("We received {} other reports of invalid requests since the last log entry", numUnloggedInvalidRequestReports);
|
||||
numUnloggedInvalidRequestReports = 0;
|
||||
lastLoggedInvalidRequestReportTs = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
@ -673,25 +674,22 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
||||
else
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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 {
|
||||
// TODO sometimes we get StreamCorruptedException, OptionalDataException, IllegalStateException
|
||||
closeConnectionReason = CloseConnectionReason.UNKNOWN_EXCEPTION;
|
||||
log.warn("Unknown reason for exception at socket: {}\n\t" +
|
||||
"peer={}\n\t" +
|
||||
"Exception={}",
|
||||
socket.toString(),
|
||||
this.peersNodeAddressOptional,
|
||||
e.toString());
|
||||
throttleWarn("Unknown reason for exception at socket: " + socket.toString() + "\n\t" +
|
||||
"peer=" + this.peersNodeAddressOptional + "\n\t" +
|
||||
"Exception=" + e.toString());
|
||||
}
|
||||
shutDown(closeConnectionReason);
|
||||
}
|
||||
@ -712,8 +710,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
||||
}
|
||||
|
||||
if (banFilter != null && banFilter.isPeerBanned(senderNodeAddress)) {
|
||||
log.warn("We got a message from a banned peer. message={}", sendersNodeAddressMessage.getClass().getSimpleName());
|
||||
reportInvalidRequest(RuleViolation.PEER_BANNED);
|
||||
String errorMessage = "We got a message from a banned peer. message=" + sendersNodeAddressMessage.getClass().getSimpleName();
|
||||
reportInvalidRequest(RuleViolation.PEER_BANNED, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -745,7 +743,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
||||
try {
|
||||
if (socket != null &&
|
||||
socket.isClosed()) {
|
||||
log.warn("Socket is null or closed socket={}", socket);
|
||||
throttleWarn("Socket is null or closed socket=" + socket);
|
||||
shutDown(CloseConnectionReason.SOCKET_CLOSED);
|
||||
return;
|
||||
}
|
||||
@ -757,7 +755,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
||||
|
||||
if (socket != null &&
|
||||
socket.isClosed()) {
|
||||
log.warn("Socket is null or closed socket={}", socket);
|
||||
throttleWarn("Socket is null or closed socket=" + socket);
|
||||
shutDown(CloseConnectionReason.SOCKET_CLOSED);
|
||||
return;
|
||||
}
|
||||
@ -767,9 +765,9 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
log.warn("proto is null. protoInputStream.read()=" + protoInputStream.read());
|
||||
throttleWarn("proto is null. protoInputStream.read()=" + protoInputStream.read());
|
||||
}
|
||||
shutDown(CloseConnectionReason.NO_PROTO_BUFFER_ENV);
|
||||
return;
|
||||
@ -778,8 +776,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
||||
if (banFilter != null &&
|
||||
peersNodeAddressOptional.isPresent() &&
|
||||
banFilter.isPeerBanned(peersNodeAddressOptional.get())) {
|
||||
log.warn("We got a message from a banned peer. proto={}", Utilities.toTruncatedString(proto));
|
||||
reportInvalidRequest(RuleViolation.PEER_BANNED);
|
||||
String errorMessage = "We got a message from a banned peer. proto=" + Utilities.toTruncatedString(proto);
|
||||
reportInvalidRequest(RuleViolation.PEER_BANNED, errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -814,30 +812,28 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
||||
|
||||
if (networkEnvelope instanceof AddPersistableNetworkPayloadMessage &&
|
||||
!((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().verifyHashSize()) {
|
||||
log.warn("PersistableNetworkPayload.verifyHashSize failed. hashSize={}; object={}",
|
||||
((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().getHash().length,
|
||||
Utilities.toTruncatedString(proto));
|
||||
if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED))
|
||||
String errorMessage = "PersistableNetworkPayload.verifyHashSize failed. hashSize=" +
|
||||
((AddPersistableNetworkPayloadMessage) networkEnvelope).getPersistableNetworkPayload().getHash().length + "; object=" +
|
||||
Utilities.toTruncatedString(proto);
|
||||
if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED, errorMessage))
|
||||
return;
|
||||
}
|
||||
|
||||
if (exceeds) {
|
||||
log.warn("size > MAX_MSG_SIZE. size={}; object={}", size, Utilities.toTruncatedString(proto));
|
||||
|
||||
if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED))
|
||||
String errorMessage = "size > MAX_MSG_SIZE. size=" + size + "; object=" + Utilities.toTruncatedString(proto);
|
||||
if (reportInvalidRequest(RuleViolation.MAX_MSG_SIZE_EXCEEDED, errorMessage))
|
||||
return;
|
||||
}
|
||||
|
||||
if (violatesThrottleLimit() && reportInvalidRequest(RuleViolation.THROTTLE_LIMIT_EXCEEDED))
|
||||
if (violatesThrottleLimit() && reportInvalidRequest(RuleViolation.THROTTLE_LIMIT_EXCEEDED, "Violates throttle limit"))
|
||||
return;
|
||||
|
||||
// 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())
|
||||
&& reportInvalidRequest(RuleViolation.WRONG_NETWORK_ID)) {
|
||||
log.warn("RuleViolation.WRONG_NETWORK_ID. version of message={}, app version={}, " +
|
||||
"proto.toTruncatedString={}", proto.getMessageVersion(),
|
||||
Version.getP2PMessageVersion(),
|
||||
Utilities.toTruncatedString(proto.toString()));
|
||||
&& reportInvalidRequest(RuleViolation.WRONG_NETWORK_ID, errorMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -879,12 +875,9 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
||||
ThreadUtils.execute(() -> connectionStatistics.addReceivedMsgMetrics(System.currentTimeMillis() - ts, size), THREAD_ID);
|
||||
}
|
||||
} catch (InvalidClassException e) {
|
||||
log.error(e.getMessage());
|
||||
e.printStackTrace();
|
||||
reportInvalidRequest(RuleViolation.INVALID_CLASS);
|
||||
reportInvalidRequest(RuleViolation.INVALID_CLASS, e.getMessage());
|
||||
} catch (ProtobufferException | NoClassDefFoundError | InvalidProtocolBufferException e) {
|
||||
log.error(e.getMessage());
|
||||
reportInvalidRequest(RuleViolation.INVALID_DATA_TYPE);
|
||||
reportInvalidRequest(RuleViolation.INVALID_DATA_TYPE, e.getMessage());
|
||||
} catch (Throwable t) {
|
||||
handleException(t);
|
||||
}
|
||||
@ -943,4 +936,16 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
|
||||
NodeAddress nodeAddress = getSenderNodeAddress(networkEnvelope);
|
||||
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;
|
||||
|
||||
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.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 java.security.SecureRandom;
|
||||
|
||||
import java.net.Socket;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
@Slf4j
|
||||
public class TorNetworkNode extends NetworkNode {
|
||||
private static final long SHUT_DOWN_TIMEOUT = 2;
|
||||
public abstract class TorNetworkNode extends NetworkNode {
|
||||
|
||||
private final String torControlHost;
|
||||
|
||||
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;
|
||||
protected final ExecutorService executor;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
@ -69,15 +45,9 @@ public class TorNetworkNode extends NetworkNode {
|
||||
|
||||
public TorNetworkNode(int servicePort,
|
||||
NetworkProtoResolver networkProtoResolver,
|
||||
boolean useStreamIsolation,
|
||||
TorMode torMode,
|
||||
@Nullable BanFilter banFilter,
|
||||
int maxConnections, String torControlHost) {
|
||||
int maxConnections) {
|
||||
super(servicePort, networkProtoResolver, banFilter, maxConnections);
|
||||
this.torMode = torMode;
|
||||
this.streamIsolation = useStreamIsolation;
|
||||
this.torControlHost = torControlHost;
|
||||
|
||||
executor = SingleThreadExecutorUtils.getSingleThreadExecutor("StartTor");
|
||||
}
|
||||
|
||||
@ -87,121 +57,19 @@ public class TorNetworkNode extends NetworkNode {
|
||||
|
||||
@Override
|
||||
public void start(@Nullable SetupListener setupListener) {
|
||||
torMode.doRollingBackup();
|
||||
|
||||
if (setupListener != null)
|
||||
addSetupListener(setupListener);
|
||||
|
||||
createTorAndHiddenService(Utils.findFreeSystemPort(), servicePort);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
createTorAndHiddenService();
|
||||
}
|
||||
|
||||
public void shutDown(@Nullable Runnable shutDownCompleteHandler) {
|
||||
log.info("TorNetworkNode shutdown started");
|
||||
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();
|
||||
}
|
||||
});
|
||||
super.shutDown(shutDownCompleteHandler);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Create tor and hidden service
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
public abstract Socks5Proxy getSocksProxy();
|
||||
|
||||
private void createTorAndHiddenService(int localPort, int servicePort) {
|
||||
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;
|
||||
});
|
||||
}
|
||||
protected abstract Socket createSocket(NodeAddress peerNodeAddress) throws IOException;
|
||||
|
||||
protected abstract void createTorAndHiddenService();
|
||||
}
|
||||
|
@ -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.
|
||||
// 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
|
||||
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 {
|
||||
latch = new CountDownLatch(1);
|
||||
int port = 9001;
|
||||
TorNetworkNode node1 = new TorNetworkNode(port, TestUtils.getNetworkProtoResolver(), false,
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
||||
TorNetworkNode node1 = new TorNetworkNodeNetlayer(port,
|
||||
TestUtils.getNetworkProtoResolver(),
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||
null,
|
||||
12,
|
||||
false,
|
||||
"127.0.0.1");
|
||||
node1.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
@ -77,8 +82,13 @@ public class TorNetworkNodeTest {
|
||||
|
||||
latch = new CountDownLatch(1);
|
||||
int port2 = 9002;
|
||||
TorNetworkNode node2 = new TorNetworkNode(port2, TestUtils.getNetworkProtoResolver(), false,
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
||||
TorNetworkNode node2 = new TorNetworkNodeNetlayer(port2,
|
||||
TestUtils.getNetworkProtoResolver(),
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||
null,
|
||||
12,
|
||||
false,
|
||||
"127.0.0.1");
|
||||
node2.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
@ -135,8 +145,13 @@ public class TorNetworkNodeTest {
|
||||
public void testTorNodeAfterBothReady() throws InterruptedException, IOException {
|
||||
latch = new CountDownLatch(2);
|
||||
int port = 9001;
|
||||
TorNetworkNode node1 = new TorNetworkNode(port, TestUtils.getNetworkProtoResolver(), false,
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
||||
TorNetworkNode node1 = new TorNetworkNodeNetlayer(port,
|
||||
TestUtils.getNetworkProtoResolver(),
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||
null,
|
||||
12,
|
||||
false,
|
||||
"127.0.0.1");
|
||||
node1.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
@ -161,8 +176,12 @@ public class TorNetworkNodeTest {
|
||||
});
|
||||
|
||||
int port2 = 9002;
|
||||
TorNetworkNode node2 = new TorNetworkNode(port2, TestUtils.getNetworkProtoResolver(), false,
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses), null, 12, "127.0.0.1");
|
||||
TorNetworkNode node2 = new TorNetworkNodeNetlayer(port2, TestUtils.getNetworkProtoResolver(),
|
||||
new NewTor(new File("torNode_" + port), null, "", this::getBridgeAddresses),
|
||||
null,
|
||||
12,
|
||||
false,
|
||||
"127.0.0.1");
|
||||
node2.start(new SetupListener() {
|
||||
@Override
|
||||
public void onTorNodeReady() {
|
||||
|
@ -10,6 +10,8 @@ SyslogIdentifier=Haveno-Seednode
|
||||
ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\
|
||||
--useLocalhostForP2P=false\
|
||||
--useDevPrivilegeKeys=false\
|
||||
# Uncomment the following line to use external tor
|
||||
# --hiddenServiceAddress=example.onion\
|
||||
--nodePort=2002\
|
||||
--appName=haveno-XMR_STAGENET_Seed_2002\
|
||||
# --logLevel=trace\
|
||||
|
@ -10,8 +10,10 @@ SyslogIdentifier=Haveno-Seednode2
|
||||
ExecStart=/bin/sh /home/haveno/haveno/haveno-seednode --baseCurrencyNetwork=XMR_STAGENET\
|
||||
--useLocalhostForP2P=false\
|
||||
--useDevPrivilegeKeys=false\
|
||||
--nodePort=3003\
|
||||
--appName=haveno-XMR_STAGENET_Seed_3003\
|
||||
# Uncomment the following line to use external tor
|
||||
# --hiddenServiceAddress=example.onion\
|
||||
--nodePort=2003\
|
||||
--appName=haveno-XMR_STAGENET_Seed_2003\
|
||||
# --logLevel=trace\
|
||||
--xmrNode=http://[::1]:38088\
|
||||
--xmrNodeUsername=admin\
|
||||
|
@ -2,10 +2,10 @@
|
||||
|
||||
## 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```
|
||||
## 4. As amnesia run ```source ~/.bashrc```
|
||||
## 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\
|
||||
--useLocalhostForP2P=false\
|
||||
--useDevPrivilegeKeys=false\
|
||||
# Uncomment the following line to use external tor
|
||||
# --hiddenServiceAddress=example.onion\
|
||||
--nodePort=2002\
|
||||
--appName=haveno-XMR_STAGENET_Seed_2002
|
||||
--xmrNode=[::1]:38088
|
||||
--appName=haveno-XMR_STAGENET_Seed_2002\
|
||||
--xmrNode=http://[::1]:38088
|
||||
|
||||
ExecStop=/bin/kill ${MAINPID} ; sleep 5
|
||||
Restart=always
|
||||
|
@ -41,7 +41,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
public class SeedNodeMain extends ExecutableForAppWithP2p {
|
||||
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 Timer checkConnectionLossTime;
|
||||
|
||||
|
165
seednode/torrc
165
seednode/torrc
@ -1,18 +1,155 @@
|
||||
RunAsDaemon 1
|
||||
SOCKSPort 9050
|
||||
ControlPort 9051
|
||||
Log notice syslog
|
||||
## Configuration file for Haveno Seednode
|
||||
##
|
||||
## To start/reload/etc this instance, run "systemctl start tor" (or reload, or..).
|
||||
## 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
|
||||
CookieAuthFileGroupReadable 1
|
||||
DataDirectoryGroupReadable 1
|
||||
## Tor opens a socks proxy on port 9050 by default -- even if you don't
|
||||
## configure one below. Set "SocksPort 0" if you plan to run Tor only
|
||||
## 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
|
||||
HiddenServiceStatistics 0
|
||||
## Entry policies to allow/deny SOCKS requests based on IP address.
|
||||
## 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
|
||||
|
||||
#MaxClientCircuitsPending 64
|
||||
#KeepalivePeriod 2
|
||||
#CircuitBuildTimeout 5
|
||||
#NewCircuitPeriod 15
|
||||
#NumEntryGuards 8
|
||||
## TODO: This option has no effect. Bisq/Haveno is tor client &/or hidden service. 'man torrc':
|
||||
## Relays and bridges only. When this option is enabled, a Tor relay writes obfuscated statistics on its
|
||||
## role as hidden-service directory, introduction point, or rendezvous point to disk every 24 hours.
|
||||
## If ExtraInfoStatistics is enabled, it will be published as part of the extra-info document. (Default: 1)
|
||||
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