Compare commits

...

7 commits

10 changed files with 131 additions and 52 deletions

View file

@ -74,7 +74,7 @@ public class FileUtil {
}
}
public static File getLatestBackupFile(File dir, String fileName) {
public static List<File> getBackupFiles(File dir, String fileName) {
File backupDir = new File(Paths.get(dir.getAbsolutePath(), BACKUP_DIR).toString());
if (!backupDir.exists()) return null;
String dirName = "backups_" + fileName;
@ -82,9 +82,14 @@ public class FileUtil {
File backupFileDir = new File(Paths.get(backupDir.getAbsolutePath(), dirName).toString());
if (!backupFileDir.exists()) return null;
File[] files = backupFileDir.listFiles();
if (files == null || files.length == 0) return null;
Arrays.sort(files, Comparator.comparing(File::getName));
return files[files.length - 1];
return Arrays.asList(files);
}
public static File getLatestBackupFile(File dir, String fileName) {
List<File> files = getBackupFiles(dir, fileName);
if (files.isEmpty()) return null;
files.sort(Comparator.comparing(File::getName));
return files.get(files.size() - 1);
}
public static void deleteRollingBackup(File dir, String fileName) {

View file

@ -466,6 +466,10 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
return this.disputeState == State.NEW;
}
public boolean isOpen() {
return this.disputeState == State.OPEN || this.disputeState == State.REOPENED;
}
public boolean isClosed() {
return this.disputeState == State.CLOSED;
}

View file

@ -393,8 +393,14 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
chatMessage.setSystemMessage(true);
dispute.addAndPersistChatMessage(chatMessage);
// export latest multisig hex
try {
trade.exportMultisigHex();
} catch (Exception e) {
log.error("Failed to export multisig hex", e);
}
// create dispute opened message
trade.exportMultisigHex();
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
DisputeOpenedMessage disputeOpenedMessage = new DisputeOpenedMessage(dispute,
p2PService.getAddress(),

View file

@ -70,7 +70,7 @@ public abstract class DisputeSession extends SupportSession {
@Override
public boolean chatIsOpen() {
return dispute != null && !dispute.isClosed();
return dispute != null && dispute.isOpen();
}
@Override

View file

@ -937,6 +937,7 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
if (wallet == null) throw new RuntimeException("Trade wallet to close is not open for trade " + getId());
stopPolling();
xmrWalletService.closeWallet(wallet, true);
maybeBackupWallet();
wallet = null;
pollPeriodMs = null;
}
@ -1545,9 +1546,6 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
forceCloseWallet();
}
// backup trade wallet if applicable
maybeBackupWallet();
// de-initialize
if (idlePayoutSyncer != null) {
xmrWalletService.removeWalletListener(idlePayoutSyncer);

View file

@ -450,6 +450,13 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
return;
}
// skip if marked as failed
if (failedTradesManager.getObservableList().contains(trade)) {
log.warn("Skipping initialization of failed trade {} {}", trade.getClass().getSimpleName(), trade.getId());
tradesToSkip.add(trade);
return;
}
// initialize trade
initPersistedTrade(trade);
@ -1059,7 +1066,11 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
private void addTradeToPendingTrades(Trade trade) {
if (!trade.isInitialized()) {
initPersistedTrade(trade);
try {
initPersistedTrade(trade);
} catch (Exception e) {
log.warn("Error initializing {} {} on move to pending trades", trade.getClass().getSimpleName(), trade.getShortId(), e);
}
}
addTrade(trade);
}

View file

@ -121,7 +121,7 @@ public class XmrWalletService extends XmrWalletBase {
private static final String MONERO_WALLET_NAME = "haveno_XMR";
private static final String KEYS_FILE_POSTFIX = ".keys";
private static final String ADDRESS_FILE_POSTFIX = ".address.txt";
private static final int NUM_MAX_WALLET_BACKUPS = 1;
private static final int NUM_MAX_WALLET_BACKUPS = 2;
private static final int MAX_SYNC_ATTEMPTS = 3;
private static final boolean PRINT_RPC_STACK_TRACE = false;
private static final String THREAD_ID = XmrWalletService.class.getSimpleName();
@ -1477,26 +1477,33 @@ public class XmrWalletService extends XmrWalletBase {
try {
walletFull = MoneroWalletFull.openWallet(config);
} catch (Exception e) {
log.warn("Failed to open full wallet '{}', attempting to use backup cache, error={}", config.getPath(), e.getMessage());
log.warn("Failed to open full wallet '{}', attempting to use backup cache files, error={}", config.getPath(), e.getMessage());
boolean retrySuccessful = false;
try {
// rename wallet cache to backup
String cachePath = walletDir.toString() + File.separator + MONERO_WALLET_NAME;
String cachePath = walletDir.toString() + File.separator + getWalletName(config.getPath());
File originalCacheFile = new File(cachePath);
if (originalCacheFile.exists()) originalCacheFile.renameTo(new File(cachePath + ".backup"));
// copy latest wallet cache backup to main folder
File backupCacheFile = FileUtil.getLatestBackupFile(walletDir, MONERO_WALLET_NAME);
if (backupCacheFile != null) FileUtil.copyFile(backupCacheFile, new File(cachePath));
// try opening wallet with backup cache files in descending order
List<File> backupCacheFiles = FileUtil.getBackupFiles(walletDir, getWalletName(config.getPath()));
Collections.reverse(backupCacheFiles);
for (File backupCacheFile : backupCacheFiles) {
try {
FileUtil.copyFile(backupCacheFile, new File(cachePath));
walletFull = MoneroWalletFull.openWallet(config);
log.warn("Successfully opened full wallet using backup cache");
retrySuccessful = true;
break;
} catch (Exception e2) {
// retry opening wallet without original cache
try {
walletFull = MoneroWalletFull.openWallet(config);
log.info("Successfully opened full wallet using backup cache");
retrySuccessful = true;
} catch (Exception e2) {
// ignore
// delete cache file if failed to open
File cacheFile = new File(cachePath);
if (cacheFile.exists()) cacheFile.delete();
File unportableCacheFile = new File(cachePath + ".unportable");
if (unportableCacheFile.exists()) unportableCacheFile.delete();
}
}
// handle success or failure
@ -1505,14 +1512,30 @@ public class XmrWalletService extends XmrWalletBase {
if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup
} else {
// restore original wallet cache
log.warn("Failed to open full wallet using backup cache, restoring original cache");
File cacheFile = new File(cachePath);
if (cacheFile.exists()) cacheFile.delete();
if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath));
// retry opening wallet after cache deleted
try {
log.warn("Failed to open full wallet using backup cache files, retrying with cache deleted");
walletFull = MoneroWalletFull.openWallet(config);
log.warn("Successfully opened full wallet after cache deleted");
retrySuccessful = true;
} catch (Exception e2) {
// ignore
}
// throw exception
throw e;
// handle success or failure
if (retrySuccessful) {
if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup
} else {
// restore original wallet cache
log.warn("Failed to open full wallet after deleting cache, restoring original cache");
File cacheFile = new File(cachePath);
if (cacheFile.exists()) cacheFile.delete();
if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath));
// throw original exception
throw e;
}
}
} catch (Exception e2) {
throw e; // throw original exception
@ -1582,26 +1605,33 @@ public class XmrWalletService extends XmrWalletBase {
try {
walletRpc.openWallet(config);
} catch (Exception e) {
log.warn("Failed to open RPC wallet '{}', attempting to use backup cache, error={}", config.getPath(), e.getMessage());
log.warn("Failed to open RPC wallet '{}', attempting to use backup cache files, error={}", config.getPath(), e.getMessage());
boolean retrySuccessful = false;
try {
// rename wallet cache to backup
String cachePath = walletDir.toString() + File.separator + MONERO_WALLET_NAME;
String cachePath = walletDir.toString() + File.separator + config.getPath();
File originalCacheFile = new File(cachePath);
if (originalCacheFile.exists()) originalCacheFile.renameTo(new File(cachePath + ".backup"));
// copy latest wallet cache backup to main folder
File backupCacheFile = FileUtil.getLatestBackupFile(walletDir, MONERO_WALLET_NAME);
if (backupCacheFile != null) FileUtil.copyFile(backupCacheFile, new File(cachePath));
// try opening wallet with backup cache files in descending order
List<File> backupCacheFiles = FileUtil.getBackupFiles(walletDir, config.getPath());
Collections.reverse(backupCacheFiles);
for (File backupCacheFile : backupCacheFiles) {
try {
FileUtil.copyFile(backupCacheFile, new File(cachePath));
walletRpc.openWallet(config);
log.warn("Successfully opened RPC wallet using backup cache");
retrySuccessful = true;
break;
} catch (Exception e2) {
// retry opening wallet without original cache
try {
walletRpc.openWallet(config);
log.info("Successfully opened RPC wallet using backup cache");
retrySuccessful = true;
} catch (Exception e2) {
// ignore
// delete cache file if failed to open
File cacheFile = new File(cachePath);
if (cacheFile.exists()) cacheFile.delete();
File unportableCacheFile = new File(cachePath + ".unportable");
if (unportableCacheFile.exists()) unportableCacheFile.delete();
}
}
// handle success or failure
@ -1610,14 +1640,30 @@ public class XmrWalletService extends XmrWalletBase {
if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup
} else {
// restore original wallet cache
log.warn("Failed to open RPC wallet using backup cache, restoring original cache");
File cacheFile = new File(cachePath);
if (cacheFile.exists()) cacheFile.delete();
if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath));
// retry opening wallet after cache deleted
try {
log.warn("Failed to open RPC wallet using backup cache files, retrying with cache deleted");
walletRpc.openWallet(config);
log.warn("Successfully opened RPC wallet after cache deleted");
retrySuccessful = true;
} catch (Exception e2) {
// ignore
}
// throw exception
throw e;
// handle success or failure
if (retrySuccessful) {
if (originalCacheBackup.exists()) originalCacheBackup.delete(); // delete original wallet cache backup
} else {
// restore original wallet cache
log.warn("Failed to open RPC wallet after deleting cache, restoring original cache");
File cacheFile = new File(cachePath);
if (cacheFile.exists()) cacheFile.delete();
if (originalCacheBackup.exists()) originalCacheBackup.renameTo(new File(cachePath));
// throw original exception
throw e;
}
}
} catch (Exception e2) {
throw e; // throw original exception

View file

@ -548,10 +548,17 @@ public class PendingTradesDataModel extends ActivatableDataModel {
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);
trade.exportMultisigHex();
// export latest multisig hex
try {
trade.exportMultisigHex();
} catch (Exception e) {
log.error("Failed to export multisig hex", e);
}
// send dispute opened message
sendDisputeOpenedMessage(dispute, disputeManager);
tradeManager.requestPersistence();
} else {

View file

@ -404,7 +404,7 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
}
if (trade.getTakerDepositTx() == null) {
return Res.get("portfolio.pending.failedTrade.missingDepositTx"); // TODO (woodser): use .missingTakerDepositTx, .missingMakerDepositTx
return Res.get("portfolio.pending.failedTrade.missingDepositTx"); // TODO (woodser): use .missingTakerDepositTx, .missingMakerDepositTx, update translation to 2-of-3 multisig
}
if (trade.hasErrorMessage()) {

View file

@ -1420,6 +1420,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> implements
}
if (dispute.isClosed()) return Res.get("support.closed");
switch (trade.getDisputeState()) {
case NO_DISPUTE:
return Res.get("shared.pending");
case DISPUTE_REQUESTED:
return Res.get("support.requested");
default: