Merge branch 'haveno-dex:master' into haveno-reto

This commit is contained in:
boldsuck 2024-12-29 23:03:08 +01:00 committed by GitHub
commit cfbd7dc2e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 144 additions and 66 deletions

View file

@ -33,6 +33,7 @@ import haveno.core.xmr.model.XmrAddressEntry;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroOutputWallet;
import monero.wallet.model.MoneroTxWallet;
@Slf4j
@ -62,7 +63,6 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
model.getXmrWalletService().getXmrConnectionService().verifyConnection();
// create reserve tx
MoneroTxWallet reserveTx = null;
synchronized (HavenoUtils.xmrWalletService.getWalletLock()) {
// reset protocol timeout
@ -79,6 +79,7 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
Integer preferredSubaddressIndex = fundingEntry == null ? null : fundingEntry.getSubaddressIndex();
// attempt creating reserve tx
MoneroTxWallet reserveTx = null;
try {
synchronized (HavenoUtils.getWalletFunctionLock()) {
for (int i = 0; i < TradeProtocol.MAX_ATTEMPTS; i++) {
@ -121,6 +122,19 @@ public class MakerReserveOfferFunds extends Task<PlaceOfferModel> {
openOffer.setReserveTxHex(reserveTx.getFullHex());
openOffer.setReserveTxKey(reserveTx.getKey());
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
// reset offer funding address entry if unused
if (fundingEntry != null) {
List<MoneroOutputWallet> inputs = model.getXmrWalletService().getOutputs(reservedKeyImages);
boolean usesFundingEntry = false;
for (MoneroOutputWallet input : inputs) {
if (input.getAccountIndex() == 0 && input.getSubaddressIndex() == fundingEntry.getSubaddressIndex()) {
usesFundingEntry = true;
break;
}
}
if (!usesFundingEntry) model.getXmrWalletService().swapAddressEntryToAvailable(offer.getId(), XmrAddressEntry.Context.OFFER_FUNDING);
}
}
complete();
} catch (Throwable t) {

View file

@ -852,14 +852,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
}
public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
if (xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection)) {
onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread
return true;
}
return false;
}
public boolean isIdling() {
return this instanceof ArbitratorTrade && isDepositsConfirmed() && walletExists() && pollNormalStartTimeMs == null; // arbitrator idles trade after deposits confirm unless overriden
}
@ -909,6 +901,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}
}
@Override
public void requestSaveWallet() {
// save wallet off main thread
@ -919,6 +912,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
}, getId());
}
@Override
public void saveWallet() {
synchronized (walletLock) {
if (!walletExists()) {
@ -2386,7 +2380,8 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
return tradeVolumeProperty;
}
private void onConnectionChanged(MoneroRpcConnection connection) {
@Override
protected void onConnectionChanged(MoneroRpcConnection connection) {
synchronized (walletLock) {
// use current connection
@ -2682,7 +2677,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
pollInProgress = false;
}
}
requestSaveWallet();
saveWalletWithDelay();
}
}

View file

@ -17,6 +17,7 @@ import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import monero.common.MoneroRpcConnection;
import monero.common.TaskLooper;
import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet;
@ -24,16 +25,18 @@ import monero.wallet.MoneroWalletFull;
import monero.wallet.model.MoneroWalletListener;
@Slf4j
public class XmrWalletBase {
public abstract class XmrWalletBase {
// constants
public static final int SYNC_PROGRESS_TIMEOUT_SECONDS = 120;
public static final int DIRECT_SYNC_WITHIN_BLOCKS = 100;
public static final int SAVE_WALLET_DELAY_SECONDS = 300;
// inherited
protected MoneroWallet wallet;
@Getter
protected final Object walletLock = new Object();
protected Timer saveWalletDelayTimer;
@Getter
protected XmrConnectionService xmrConnectionService;
protected boolean wasWalletSynced;
@ -137,6 +140,34 @@ public class XmrWalletBase {
}
}
public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
if (xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection)) {
onConnectionChanged(xmrConnectionService.getConnection()); // change connection on same thread
return true;
}
return false;
}
public void saveWalletWithDelay() {
// delay writing to disk to avoid frequent write operations
if (saveWalletDelayTimer == null) {
saveWalletDelayTimer = UserThread.runAfter(() -> {
requestSaveWallet();
UserThread.execute(() -> saveWalletDelayTimer = null);
}, SAVE_WALLET_DELAY_SECONDS, TimeUnit.SECONDS);
}
}
// --------------------------------- ABSTRACT -----------------------------
public abstract void saveWallet();
public abstract void requestSaveWallet();
protected abstract void onConnectionChanged(MoneroRpcConnection connection);
// ------------------------------ PRIVATE HELPERS -------------------------
private void updateSyncProgress(long height, long targetHeight) {
resetSyncProgressTimeout();
UserThread.execute(() -> {

View file

@ -245,16 +245,20 @@ public class XmrWalletService extends XmrWalletBase {
return user.getWalletCreationDate();
}
public void saveMainWallet() {
saveMainWallet(!(Utilities.isWindows() && wallet != null));
@Override
public void saveWallet() {
saveWallet(!(Utilities.isWindows() && wallet != null));
}
public void saveMainWallet(boolean backup) {
saveWallet(getWallet(), backup);
public void saveWallet(boolean backup) {
synchronized (walletLock) {
saveWallet(getWallet(), backup);
}
}
public void requestSaveMainWallet() {
ThreadUtils.submitToPool(() -> saveMainWallet()); // save wallet off main thread
@Override
public void requestSaveWallet() {
ThreadUtils.submitToPool(() -> saveWallet()); // save wallet off main thread
}
public boolean isWalletAvailable() {
@ -376,8 +380,8 @@ public class XmrWalletService extends XmrWalletBase {
}
public void saveWallet(MoneroWallet wallet, boolean backup) {
wallet.save();
if (backup) backupWallet(getWalletName(wallet.getPath()));
wallet.save();
}
public void closeWallet(MoneroWallet wallet, boolean save) {
@ -385,8 +389,8 @@ public class XmrWalletService extends XmrWalletBase {
MoneroError err = null;
String path = wallet.getPath();
try {
wallet.close(save);
if (save) backupWallet(getWalletName(path));
if (save) saveWallet(wallet, true);
wallet.close();
} catch (MoneroError e) {
err = e;
}
@ -443,7 +447,7 @@ public class XmrWalletService extends XmrWalletBase {
if (Boolean.TRUE.equals(txConfig.getRelay())) {
cachedTxs.addFirst(tx);
cacheWalletInfo();
requestSaveMainWallet();
requestSaveWallet();
}
return tx;
}
@ -453,7 +457,7 @@ public class XmrWalletService extends XmrWalletBase {
public String relayTx(String metadata) {
synchronized (walletLock) {
String txId = wallet.relayTx(metadata);
requestSaveMainWallet();
requestSaveWallet();
return txId;
}
}
@ -552,7 +556,7 @@ public class XmrWalletService extends XmrWalletBase {
// freeze outputs
for (String keyImage : unfrozenKeyImages) wallet.freezeOutput(keyImage);
cacheWalletInfo();
requestSaveMainWallet();
requestSaveWallet();
}
}
@ -574,19 +578,10 @@ public class XmrWalletService extends XmrWalletBase {
// thaw outputs
for (String keyImage : frozenKeyImages) wallet.thawOutput(keyImage);
cacheWalletInfo();
requestSaveMainWallet();
requestSaveWallet();
}
}
public BigInteger getOutputsAmount(Collection<String> keyImages) {
BigInteger sum = BigInteger.ZERO;
for (String keyImage : keyImages) {
List<MoneroOutputWallet> outputs = getOutputs(new MoneroOutputQuery().setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage)));
if (!outputs.isEmpty()) sum = sum.add(outputs.get(0).getAmount());
}
return sum;
}
private List<Integer> getSubaddressesWithExactInput(BigInteger amount) {
// fetch unspent, unfrozen, unlocked outputs
@ -1125,6 +1120,15 @@ public class XmrWalletService extends XmrWalletBase {
return subaddress == null ? BigInteger.ZERO : subaddress.getBalance();
}
public BigInteger getBalanceForSubaddress(int subaddressIndex, boolean includeFrozen) {
return getBalanceForSubaddress(subaddressIndex).add(includeFrozen ? getFrozenBalanceForSubaddress(subaddressIndex) : BigInteger.ZERO);
}
public BigInteger getFrozenBalanceForSubaddress(int subaddressIndex) {
List<MoneroOutputWallet> outputs = getOutputs(new MoneroOutputQuery().setIsFrozen(true).setIsSpent(false).setAccountIndex(0).setSubaddressIndex(subaddressIndex));
return outputs.stream().map(output -> output.getAmount()).reduce(BigInteger.ZERO, BigInteger::add);
}
public BigInteger getAvailableBalanceForSubaddress(int subaddressIndex) {
MoneroSubaddress subaddress = getSubaddress(subaddressIndex);
return subaddress == null ? BigInteger.ZERO : subaddress.getUnlockedBalance();
@ -1250,6 +1254,19 @@ public class XmrWalletService extends XmrWalletBase {
return filteredOutputs;
}
public List<MoneroOutputWallet> getOutputs(Collection<String> keyImages) {
List<MoneroOutputWallet> outputs = new ArrayList<MoneroOutputWallet>();
for (String keyImage : keyImages) {
List<MoneroOutputWallet> outputList = getOutputs(new MoneroOutputQuery().setIsSpent(false).setKeyImage(new MoneroKeyImage(keyImage)));
if (!outputList.isEmpty()) outputs.add(outputList.get(0));
}
return outputs;
}
public BigInteger getOutputsAmount(Collection<String> keyImages) {
return getOutputs(keyImages).stream().map(output -> output.getAmount()).reduce(BigInteger.ZERO, BigInteger::add);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Util
///////////////////////////////////////////////////////////////////////////////////////////
@ -1321,7 +1338,7 @@ public class XmrWalletService extends XmrWalletBase {
maybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS);
}
private void maybeInitMainWallet(boolean sync, int numAttempts) {
private void maybeInitMainWallet(boolean sync, int numSyncAttempts) {
ThreadUtils.execute(() -> {
try {
doMaybeInitMainWallet(sync, MAX_SYNC_ATTEMPTS);
@ -1333,7 +1350,7 @@ public class XmrWalletService extends XmrWalletBase {
}, THREAD_ID);
}
private void doMaybeInitMainWallet(boolean sync, int numAttempts) {
private void doMaybeInitMainWallet(boolean sync, int numSyncAttempts) {
synchronized (walletLock) {
if (isShutDownStarted) return;
@ -1361,7 +1378,7 @@ public class XmrWalletService extends XmrWalletBase {
// sync main wallet if applicable
// TODO: error handling and re-initialization is jenky, refactor
if (sync && numAttempts > 0) {
if (sync && numSyncAttempts > 0) {
try {
// switch connection if disconnected
@ -1380,7 +1397,7 @@ public class XmrWalletService extends XmrWalletBase {
log.warn("Error syncing wallet with progress on startup: " + e.getMessage());
forceCloseMainWallet();
requestSwitchToNextBestConnection(sourceConnection);
maybeInitMainWallet(true, numAttempts - 1); // re-initialize wallet and sync again
maybeInitMainWallet(true, numSyncAttempts - 1); // re-initialize wallet and sync again
return;
}
log.info("Done syncing main wallet in " + (System.currentTimeMillis() - time) + " ms");
@ -1411,14 +1428,14 @@ public class XmrWalletService extends XmrWalletBase {
HavenoUtils.havenoSetup.getWalletInitialized().set(true);
// save but skip backup on initialization
saveMainWallet(false);
saveWallet(false);
} catch (Exception e) {
if (isClosingWallet || isShutDownStarted || HavenoUtils.havenoSetup.getWalletInitialized().get()) return; // ignore if wallet closing, shut down started, or app already initialized
log.warn("Error initially syncing main wallet: {}", e.getMessage());
if (numAttempts <= 1) {
log.warn("Failed to sync main wallet. Opening app without syncing", numAttempts);
if (numSyncAttempts <= 1) {
log.warn("Failed to sync main wallet. Opening app without syncing", numSyncAttempts);
HavenoUtils.havenoSetup.getWalletInitialized().set(true);
saveMainWallet(false);
saveWallet(false);
// reschedule to init main wallet
UserThread.runAfter(() -> {
@ -1427,7 +1444,7 @@ public class XmrWalletService extends XmrWalletBase {
} else {
log.warn("Trying again in {} seconds", xmrConnectionService.getRefreshPeriodMs() / 1000);
UserThread.runAfter(() -> {
maybeInitMainWallet(true, numAttempts - 1);
maybeInitMainWallet(true, numSyncAttempts - 1);
}, xmrConnectionService.getRefreshPeriodMs() / 1000);
}
}
@ -1741,7 +1758,8 @@ public class XmrWalletService extends XmrWalletBase {
return MONERO_WALLET_RPC_MANAGER.startInstance(cmd);
}
private void onConnectionChanged(MoneroRpcConnection connection) {
@Override
protected void onConnectionChanged(MoneroRpcConnection connection) {
synchronized (walletLock) {
// use current connection
@ -1795,7 +1813,7 @@ public class XmrWalletService extends XmrWalletBase {
tasks.add(() -> {
try {
wallet.changePassword(oldPassword, newPassword);
saveMainWallet();
saveWallet();
} catch (Exception e) {
log.warn("Error changing main wallet password: " + e.getMessage() + "\n", e);
throw e;
@ -1845,13 +1863,13 @@ public class XmrWalletService extends XmrWalletBase {
log.warn("Force restarting main wallet");
if (isClosingWallet) return;
forceCloseMainWallet();
maybeInitMainWallet(true);
doMaybeInitMainWallet(true, MAX_SYNC_ATTEMPTS);
}
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
requestSwitchToNextBestConnection(sourceConnection);
if (wallet == null) doMaybeInitMainWallet(true, MAX_SYNC_ATTEMPTS);
}
private void startPolling() {
@ -1988,7 +2006,7 @@ public class XmrWalletService extends XmrWalletBase {
if (wallet != null && !isShutDownStarted) {
try {
cacheWalletInfo();
requestSaveMainWallet();
saveWalletWithDelay();
} catch (Exception e) {
log.warn("Error caching wallet info: " + e.getMessage() + "\n", e);
}
@ -2020,10 +2038,6 @@ public class XmrWalletService extends XmrWalletBase {
return requestSwitchToNextBestConnection(null);
}
public boolean requestSwitchToNextBestConnection(MoneroRpcConnection sourceConnection) {
return xmrConnectionService.requestSwitchToNextBestConnection(sourceConnection);
}
private void onNewBlock(long height) {
UserThread.execute(() -> {
walletHeight.set(height);

View file

@ -29,6 +29,7 @@ import haveno.desktop.setup.DesktopPersistedDataHost;
import haveno.desktop.util.ImageUtil;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Pos;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
@ -210,9 +211,13 @@ public class HavenoAppMain extends HavenoExecutable {
Label errorMessageField = new Label(errorMessage);
errorMessageField.setTextFill(Color.color(1, 0, 0));
// Create the version field
Label versionField = new Label("v" + Version.VERSION);
// Set the dialog content
VBox vbox = new VBox(10);
vbox.getChildren().addAll(new ImageView(ImageUtil.getImageByPath("logo_splash.png")), passwordField, errorMessageField);
vbox.getChildren().addAll(new ImageView(ImageUtil.getImageByPath("logo_splash.png")), versionField, passwordField, errorMessageField);
vbox.setAlignment(Pos.TOP_CENTER);
getDialogPane().setContent(vbox);
// Add OK and Cancel buttons

View file

@ -20,6 +20,7 @@ package haveno.desktop.main;
import com.google.inject.Inject;
import com.jfoenix.controls.JFXBadge;
import com.jfoenix.controls.JFXComboBox;
import haveno.common.app.Version;
import haveno.common.HavenoException;
import haveno.common.Timer;
import haveno.common.UserThread;
@ -510,6 +511,8 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
ImageView logo = new ImageView();
logo.setId(Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_MAINNET ? "image-splash-logo" : "image-splash-testnet-logo");
Label versionLabel = new Label("v" + Version.VERSION);
// createBitcoinInfoBox
xmrSplashInfo = new AutoTooltipLabel();
xmrSplashInfo.textProperty().bind(model.getXmrInfo());
@ -621,7 +624,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
splashP2PNetworkBox.setPrefHeight(40);
splashP2PNetworkBox.getChildren().addAll(splashP2PNetworkLabel, splashP2PNetworkBusyAnimation, splashP2PNetworkIcon, showTorNetworkSettingsButton);
vBox.getChildren().addAll(logo, blockchainSyncBox, xmrSyncIndicator, splashP2PNetworkBox);
vBox.getChildren().addAll(logo, versionLabel, blockchainSyncBox, xmrSyncIndicator, splashP2PNetworkBox);
return vBox;
}

View file

@ -60,7 +60,7 @@ class DepositListItem {
this.xmrWalletService = xmrWalletService;
this.addressEntry = addressEntry;
balanceAsBI = xmrWalletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex());
balanceAsBI = xmrWalletService.getBalanceForSubaddress(addressEntry.getSubaddressIndex(), true);
balance.set(HavenoUtils.formatXmr(balanceAsBI));
updateUsage(addressEntry.getSubaddressIndex());

View file

@ -508,12 +508,14 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
}
private void updateP2PTable() {
if (connectionService.isShutDownStarted()) return; // ignore if shutting down
p2pPeersTableView.getItems().forEach(P2pNetworkListItem::cleanup);
p2pNetworkListItems.clear();
p2pNetworkListItems.setAll(p2PService.getNetworkNode().getAllConnections().stream()
.map(connection -> new P2pNetworkListItem(connection, clockWatcher))
.collect(Collectors.toList()));
UserThread.execute(() -> {
if (connectionService.isShutDownStarted()) return; // ignore if shutting down
p2pPeersTableView.getItems().forEach(P2pNetworkListItem::cleanup);
p2pNetworkListItems.clear();
p2pNetworkListItems.setAll(p2PService.getNetworkNode().getAllConnections().stream()
.map(connection -> new P2pNetworkListItem(connection, clockWatcher))
.collect(Collectors.toList()));
});
}
private void updateMoneroConnectionsTable() {
@ -521,7 +523,7 @@ public class NetworkSettingsView extends ActivatableView<GridPane, Void> {
if (connectionService.isShutDownStarted()) return; // ignore if shutting down
moneroNetworkListItems.clear();
moneroNetworkListItems.setAll(connectionService.getConnections().stream()
.map(connection -> new MoneroNetworkListItem(connection, Boolean.TRUE.equals(connection.isConnected()) && connection == connectionService.getConnection()))
.map(connection -> new MoneroNetworkListItem(connection, connection == connectionService.getConnection() && Boolean.TRUE.equals(connectionService.isConnected())))
.collect(Collectors.toList()));
updateChainHeightTextField(connectionService.chainHeightProperty().get());
});

View file

@ -178,6 +178,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
private static int numThrottledInvalidRequestReports = 0;
private static long lastLoggedWarningTs = 0;
private static int numThrottledWarnings = 0;
private static long lastLoggedInfoTs = 0;
private static int numThrottledInfos = 0;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -676,7 +678,7 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
throttleWarn("SocketException (expected if connection lost). closeConnectionReason=" + closeConnectionReason + "; connection=" + this);
} else if (e instanceof SocketTimeoutException || e instanceof TimeoutException) {
closeConnectionReason = CloseConnectionReason.SOCKET_TIMEOUT;
throttleWarn("Shut down caused by exception " + e.getMessage() + " on connection=" + this);
throttleInfo("Shut down caused by exception " + e.getMessage() + " on connection=" + this);
} else if (e instanceof EOFException) {
closeConnectionReason = CloseConnectionReason.TERMINATED;
throttleWarn("Shut down caused by exception " + e.getMessage() + " on connection=" + this);
@ -937,8 +939,8 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
}
private synchronized void throttleWarn(String msg) {
boolean logWarning = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS;
if (logWarning) {
boolean doLog = System.currentTimeMillis() - lastLoggedWarningTs > LOG_THROTTLE_INTERVAL_MS;
if (doLog) {
log.warn(msg);
if (numThrottledWarnings > 0) log.warn("{} warnings were throttled since the last log entry", numThrottledWarnings);
numThrottledWarnings = 0;
@ -947,4 +949,16 @@ public class Connection implements HasCapabilities, Runnable, MessageListener {
numThrottledWarnings++;
}
}
private synchronized void throttleInfo(String msg) {
boolean doLog = System.currentTimeMillis() - lastLoggedInfoTs > LOG_THROTTLE_INTERVAL_MS;
if (doLog) {
log.info(msg);
if (numThrottledInfos > 0) log.info("{} info logs were throttled since the last log entry", numThrottledInfos);
numThrottledInfos = 0;
lastLoggedInfoTs = System.currentTimeMillis();
} else {
numThrottledInfos++;
}
}
}