add wallet transactions loading log viewer, and improve loading messages

This commit is contained in:
Craig Raw 2021-05-04 15:17:57 +02:00
parent c067300ee8
commit 438c13fe2d
11 changed files with 182 additions and 22 deletions

View file

@ -120,6 +120,9 @@ public class AppController implements Initializable {
@FXML @FXML
private CheckMenuItem useHdCameraResolution; private CheckMenuItem useHdCameraResolution;
@FXML
private CheckMenuItem showLoadingLog;
@FXML @FXML
private CheckMenuItem showTxHex; private CheckMenuItem showTxHex;
@ -259,6 +262,7 @@ public class AppController implements Initializable {
hideEmptyUsedAddresses.setSelected(Config.get().isHideEmptyUsedAddresses()); hideEmptyUsedAddresses.setSelected(Config.get().isHideEmptyUsedAddresses());
useHdCameraResolution.setSelected(Config.get().isHdCapture()); useHdCameraResolution.setSelected(Config.get().isHdCapture());
showTxHex.setSelected(Config.get().isShowTransactionHex()); showTxHex.setSelected(Config.get().isShowTransactionHex());
showLoadingLog.setSelected(Config.get().isShowLoadingLog());
savePSBT.visibleProperty().bind(saveTransaction.visibleProperty().not()); savePSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
exportWallet.setDisable(true); exportWallet.setDisable(true);
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()))); refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
@ -664,6 +668,12 @@ public class AppController implements Initializable {
Config.get().setHdCapture(item.isSelected()); Config.get().setHdCapture(item.isSelected());
} }
public void showLoadingLog(ActionEvent event) {
CheckMenuItem item = (CheckMenuItem)event.getSource();
Config.get().setShowLoadingLog(item.isSelected());
EventManager.get().post(new LoadingLogChangedEvent(item.isSelected()));
}
public void showTxHex(ActionEvent event) { public void showTxHex(ActionEvent event) {
CheckMenuItem item = (CheckMenuItem)event.getSource(); CheckMenuItem item = (CheckMenuItem)event.getSource();
Config.get().setShowTransactionHex(item.isSelected()); Config.get().setShowTransactionHex(item.isSelected());
@ -1364,6 +1374,7 @@ public class AppController implements Initializable {
saveTransaction.setVisible(false); saveTransaction.setVisible(false);
} }
exportWallet.setDisable(true); exportWallet.setDisable(true);
showLoadingLog.setDisable(true);
showTxHex.setDisable(false); showTxHex.setDisable(false);
} else if(event instanceof WalletTabSelectedEvent) { } else if(event instanceof WalletTabSelectedEvent) {
WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event; WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event;
@ -1371,6 +1382,7 @@ public class AppController implements Initializable {
saveTransaction.setVisible(true); saveTransaction.setVisible(true);
saveTransaction.setDisable(true); saveTransaction.setDisable(true);
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid()); exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid());
showLoadingLog.setDisable(false);
showTxHex.setDisable(true); showTxHex.setDisable(true);
} }
} }

View file

@ -219,7 +219,7 @@ public class AppServices {
connectionService.setRestartOnFailure(false); connectionService.setRestartOnFailure(false);
if(tlsServerException.getCause().getMessage().contains("PKIX path building failed")) { if(tlsServerException.getCause().getMessage().contains("PKIX path building failed")) {
File crtFile = Config.get().getElectrumServerCert(); File crtFile = Config.get().getElectrumServerCert();
if(crtFile != null) { if(crtFile != null && Config.get().getServerType() == ServerType.ELECTRUM_SERVER) {
AppServices.showErrorDialog("SSL Handshake Failed", "The configured server certificate at " + crtFile.getAbsolutePath() + " did not match the certificate provided by the server at " + tlsServerException.getServer().getHost() + "." + AppServices.showErrorDialog("SSL Handshake Failed", "The configured server certificate at " + crtFile.getAbsolutePath() + " did not match the certificate provided by the server at " + tlsServerException.getServer().getHost() + "." +
"\n\nThis may indicate a man-in-the-middle attack!" + "\n\nThis may indicate a man-in-the-middle attack!" +
"\n\nChange the configured server certificate if you would like to proceed."); "\n\nChange the configured server certificate if you would like to proceed.");

View file

@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
public class LoadingLogChangedEvent {
private final boolean visible;
public LoadingLogChangedEvent(boolean visible) {
this.visible = visible;
}
public boolean isVisible() {
return visible;
}
}

View file

@ -6,7 +6,7 @@ public class WalletHistoryFailedEvent extends WalletHistoryStatusEvent {
private final Throwable exception; private final Throwable exception;
public WalletHistoryFailedEvent(Wallet wallet, Throwable exception) { public WalletHistoryFailedEvent(Wallet wallet, Throwable exception) {
super(wallet, exception.getCause() == null ? exception.getMessage() : exception.getCause().getMessage()); super(wallet, exception.getMessage());
this.exception = exception; this.exception = exception;
} }

View file

@ -38,6 +38,7 @@ public class Config {
private boolean openWalletsInNewWindows = false; private boolean openWalletsInNewWindows = false;
private boolean hideEmptyUsedAddresses = false; private boolean hideEmptyUsedAddresses = false;
private boolean showTransactionHex = true; private boolean showTransactionHex = true;
private boolean showLoadingLog = false;
private List<File> recentWalletFiles; private List<File> recentWalletFiles;
private Integer keyDerivationPeriod; private Integer keyDerivationPeriod;
private File hwi; private File hwi;
@ -249,6 +250,15 @@ public class Config {
flush(); flush();
} }
public boolean isShowLoadingLog() {
return showLoadingLog;
}
public void setShowLoadingLog(boolean showLoadingLog) {
this.showLoadingLog = showLoadingLog;
flush();
}
public List<File> getRecentWalletFiles() { public List<File> getRecentWalletFiles() {
return recentWalletFiles; return recentWalletFiles;
} }

View file

@ -5,6 +5,8 @@ import com.github.arteam.simplejsonrpc.client.Transport;
import com.github.arteam.simplejsonrpc.client.builder.BatchRequestBuilder; import com.github.arteam.simplejsonrpc.client.builder.BatchRequestBuilder;
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException; import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException;
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException; import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
@ -12,11 +14,9 @@ import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.List; import java.util.*;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
public class BatchedElectrumServerRpc implements ElectrumServerRpc { public class BatchedElectrumServerRpc implements ElectrumServerRpc {
private static final Logger log = LoggerFactory.getLogger(BatchedElectrumServerRpc.class); private static final Logger log = LoggerFactory.getLogger(BatchedElectrumServerRpc.class);
@ -74,7 +74,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
public Map<String, ScriptHashTx[]> getScriptHashHistory(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes, boolean failOnError) { public Map<String, ScriptHashTx[]> getScriptHashHistory(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes, boolean failOnError) {
JsonRpcClient client = new JsonRpcClient(transport); JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, ScriptHashTx[]> batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class); BatchRequestBuilder<String, ScriptHashTx[]> batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions")); EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions for " + getScriptHashesAbbreviation(pathScriptHashes.keySet())));
for(String path : pathScriptHashes.keySet()) { for(String path : pathScriptHashes.keySet()) {
batchRequest.add(path, "blockchain.scripthash.get_history", pathScriptHashes.get(path)); batchRequest.add(path, "blockchain.scripthash.get_history", pathScriptHashes.get(path));
@ -84,7 +84,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
return new RetryLogic<Map<String, ScriptHashTx[]>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(batchRequest::execute); return new RetryLogic<Map<String, ScriptHashTx[]>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(batchRequest::execute);
} catch (JsonRpcBatchException e) { } catch (JsonRpcBatchException e) {
if(failOnError) { if(failOnError) {
throw new ElectrumServerRpcException("Failed to retrieve references for paths: " + e.getErrors().keySet(), e); throw new ElectrumServerRpcException("Failed to retrieve transaction history for paths: " + getScriptHashesAbbreviation((Collection<String>)e.getErrors().keySet()), e);
} }
Map<String, ScriptHashTx[]> result = (Map<String, ScriptHashTx[]>)e.getSuccesses(); Map<String, ScriptHashTx[]> result = (Map<String, ScriptHashTx[]>)e.getSuccesses();
@ -94,7 +94,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
return result; return result;
} catch(Exception e) { } catch(Exception e) {
throw new ElectrumServerRpcException("Failed to retrieve references for paths: " + pathScriptHashes.keySet(), e); throw new ElectrumServerRpcException("Failed to retrieve transaction history for paths: " + getScriptHashesAbbreviation(pathScriptHashes.keySet()), e);
} }
} }
@ -112,7 +112,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
return new RetryLogic<Map<String, ScriptHashTx[]>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(batchRequest::execute); return new RetryLogic<Map<String, ScriptHashTx[]>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(batchRequest::execute);
} catch(JsonRpcBatchException e) { } catch(JsonRpcBatchException e) {
if(failOnError) { if(failOnError) {
throw new ElectrumServerRpcException("Failed to retrieve references for paths: " + e.getErrors().keySet(), e); throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for paths: " + getScriptHashesAbbreviation((Collection<String>)e.getErrors().keySet()), e);
} }
Map<String, ScriptHashTx[]> result = (Map<String, ScriptHashTx[]>)e.getSuccesses(); Map<String, ScriptHashTx[]> result = (Map<String, ScriptHashTx[]>)e.getSuccesses();
@ -122,15 +122,16 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
return result; return result;
} catch(Exception e) { } catch(Exception e) {
throw new ElectrumServerRpcException("Failed to retrieve references for paths: " + pathScriptHashes.keySet(), e); throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for paths: " + getScriptHashesAbbreviation(pathScriptHashes.keySet()), e);
} }
} }
@Override @Override
@SuppressWarnings("unchecked")
public Map<String, String> subscribeScriptHashes(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes) { public Map<String, String> subscribeScriptHashes(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes) {
JsonRpcClient client = new JsonRpcClient(transport); JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class); BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions")); EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions for " + getScriptHashesAbbreviation(pathScriptHashes.keySet())));
for(String path : pathScriptHashes.keySet()) { for(String path : pathScriptHashes.keySet()) {
batchRequest.add(path, "blockchain.scripthash.subscribe", pathScriptHashes.get(path)); batchRequest.add(path, "blockchain.scripthash.subscribe", pathScriptHashes.get(path));
@ -140,9 +141,9 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
return new RetryLogic<Map<String, String>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(batchRequest::execute); return new RetryLogic<Map<String, String>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(batchRequest::execute);
} catch(JsonRpcBatchException e) { } catch(JsonRpcBatchException e) {
//Even if we have some successes, failure to subscribe for all script hashes will result in outdated wallet view. Don't proceed. //Even if we have some successes, failure to subscribe for all script hashes will result in outdated wallet view. Don't proceed.
throw new ElectrumServerRpcException("Failed to subscribe for updates for paths: " + e.getErrors().keySet(), e); throw new ElectrumServerRpcException("Failed to subscribe to paths: " + getScriptHashesAbbreviation((Collection<String>)e.getErrors().keySet()), e);
} catch(Exception e) { } catch(Exception e) {
throw new ElectrumServerRpcException("Failed to subscribe for updates for paths: " + pathScriptHashes.keySet(), e); throw new ElectrumServerRpcException("Failed to subscribe to paths: " + getScriptHashesAbbreviation(pathScriptHashes.keySet()), e);
} }
} }
@ -151,7 +152,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
public Map<Integer, String> getBlockHeaders(Transport transport, Wallet wallet, Set<Integer> blockHeights) { public Map<Integer, String> getBlockHeaders(Transport transport, Wallet wallet, Set<Integer> blockHeights) {
JsonRpcClient client = new JsonRpcClient(transport); JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<Integer, String> batchRequest = client.createBatchRequest().keysType(Integer.class).returnType(String.class); BatchRequestBuilder<Integer, String> batchRequest = client.createBatchRequest().keysType(Integer.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving blocks")); EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving " + blockHeights.size() + " block headers"));
for(Integer height : blockHeights) { for(Integer height : blockHeights) {
batchRequest.add(height, "blockchain.block.header", height); batchRequest.add(height, "blockchain.block.header", height);
@ -162,7 +163,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
} catch(JsonRpcBatchException e) { } catch(JsonRpcBatchException e) {
return (Map<Integer, String>)e.getSuccesses(); return (Map<Integer, String>)e.getSuccesses();
} catch(Exception e) { } catch(Exception e) {
throw new ElectrumServerRpcException("Failed to block headers for block heights: " + blockHeights, e); throw new ElectrumServerRpcException("Failed to retrieve block headers for block heights: " + blockHeights, e);
} }
} }
@ -171,7 +172,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
public Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids) { public Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids) {
JsonRpcClient client = new JsonRpcClient(transport); JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class); BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving transactions")); EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving " + txids.size() + " transactions"));
for(String txid : txids) { for(String txid : txids) {
batchRequest.add(txid, "blockchain.transaction.get", txid); batchRequest.add(txid, "blockchain.transaction.get", txid);
@ -190,7 +191,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
return result; return result;
} catch(Exception e) { } catch(Exception e) {
throw new ElectrumServerRpcException("Failed to retrieve transactions for txids: " + txids, e); throw new ElectrumServerRpcException("Failed to retrieve transactions for txids: " + txids.stream().map(txid -> "[" + txid.substring(0, 6) + "]").collect(Collectors.toList()), e);
} }
} }
@ -272,4 +273,65 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
throw new ElectrumServerRpcException("Error broadcasting transaction", e); throw new ElectrumServerRpcException("Error broadcasting transaction", e);
} }
} }
private static String getScriptHashesAbbreviation(Collection<String> scriptHashes) {
List<String> sortedHashes = new ArrayList<>(scriptHashes);
if(scriptHashes.isEmpty()) {
return "[]";
}
List<List<String>> contiguous = splitToContiguous(sortedHashes);
String abbrev = "[";
for(Iterator<List<String>> iter = contiguous.iterator(); iter.hasNext(); ) {
List<String> range = iter.next();
abbrev += range.get(0);
if(range.size() > 1) {
abbrev += "-" + range.get(range.size() - 1);
}
if(iter.hasNext()) {
abbrev += ", ";
}
}
abbrev += "]";
return abbrev;
}
static List<List<String>> splitToContiguous(List<String> input) {
List<List<String>> result = new ArrayList<>();
int prev = 0;
int keyPurpose = getKeyPurpose(input.get(0));
int index = getIndex(input.get(0));
for (int cur = 0; cur < input.size(); cur++) {
if(getKeyPurpose(input.get(cur)) != keyPurpose || getIndex(input.get(cur)) != index) {
result.add(input.subList(prev, cur));
prev = cur;
}
index = getIndex(input.get(cur)) + 1;
keyPurpose = getKeyPurpose(input.get(cur));
}
result.add(input.subList(prev, input.size()));
return result;
}
private static int getKeyPurpose(String path) {
List<ChildNumber> childNumbers = KeyDerivation.parsePath(path);
if(childNumbers.isEmpty()) {
return -1;
}
return childNumbers.get(0).num();
}
private static int getIndex(String path) {
List<ChildNumber> childNumbers = KeyDerivation.parsePath(path);
if(childNumbers.isEmpty()) {
return -1;
}
return childNumbers.get(childNumbers.size() - 1).num();
}
} }

View file

@ -433,6 +433,8 @@ public class ElectrumServer {
return blockTransactionHashes; return blockTransactionHashes;
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
throw new ServerException(e.getCause()); throw new ServerException(e.getCause());
} catch (ElectrumServerRpcException e) {
throw new ServerException(e.getMessage(), e.getCause());
} catch (Exception e) { } catch (Exception e) {
throw new ServerException(e); throw new ServerException(e);
} }
@ -493,6 +495,8 @@ public class ElectrumServer {
return blockHeaderMap; return blockHeaderMap;
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
throw new ServerException(e.getCause()); throw new ServerException(e.getCause());
} catch (ElectrumServerRpcException e) {
throw new ServerException(e.getMessage(), e.getCause());
} catch (Exception e) { } catch (Exception e) {
throw new ServerException(e); throw new ServerException(e);
} }
@ -561,6 +565,8 @@ public class ElectrumServer {
return transactionMap; return transactionMap;
} catch (IllegalStateException e) { } catch (IllegalStateException e) {
throw new ServerException(e.getCause()); throw new ServerException(e.getCause());
} catch (ElectrumServerRpcException e) {
throw new ServerException(e.getMessage(), e.getCause());
} catch (Exception e) { } catch (Exception e) {
throw new ServerException(e); throw new ServerException(e);
} }

View file

@ -82,7 +82,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
result.put(path, scriptHashTxes); result.put(path, scriptHashTxes);
} catch(Exception e) { } catch(Exception e) {
if(failOnError) { if(failOnError) {
throw new ElectrumServerRpcException("Failed to retrieve reference for path: " + path, e); throw new ElectrumServerRpcException("Failed to retrieve transaction history for path: " + path, e);
} }
result.put(path, new ScriptHashTx[] {ScriptHashTx.ERROR_TX}); result.put(path, new ScriptHashTx[] {ScriptHashTx.ERROR_TX});
@ -104,7 +104,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
result.put(path, scriptHashTxes); result.put(path, scriptHashTxes);
} catch(Exception e) { } catch(Exception e) {
if(failOnError) { if(failOnError) {
throw new ElectrumServerRpcException("Failed to retrieve reference for path: " + path, e); throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for path: " + path, e);
} }
result.put(path, new ScriptHashTx[] {ScriptHashTx.ERROR_TX}); result.put(path, new ScriptHashTx[] {ScriptHashTx.ERROR_TX});
@ -127,7 +127,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
result.put(path, scriptHash); result.put(path, scriptHash);
} catch(Exception e) { } catch(Exception e) {
//Even if we have some successes, failure to subscribe for all script hashes will result in outdated wallet view. Don't proceed. //Even if we have some successes, failure to subscribe for all script hashes will result in outdated wallet view. Don't proceed.
throw new ElectrumServerRpcException("Failed to retrieve reference for path: " + path, e); throw new ElectrumServerRpcException("Failed to subscribe to path: " + path, e);
} }
} }
@ -169,6 +169,9 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
String rawTxHex = new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() -> String rawTxHex = new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
client.createRequest().returnAs(String.class).method("blockchain.transaction.get").id(idCounter.incrementAndGet()).params(txid).execute()); client.createRequest().returnAs(String.class).method("blockchain.transaction.get").id(idCounter.incrementAndGet()).params(txid).execute());
result.put(txid, rawTxHex); result.put(txid, rawTxHex);
} catch(ServerException e) {
//If there is an error with the server connection, don't keep trying - this may take too long given many txids
throw new ElectrumServerRpcException("Failed to retrieve transaction for txid [" + txid.substring(0, 6) + "]", e);
} catch(Exception e) { } catch(Exception e) {
result.put(txid, Sha256Hash.ZERO_HASH.toString()); result.put(txid, Sha256Hash.ZERO_HASH.toString());
} }

View file

@ -9,15 +9,19 @@ import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*; import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ExchangeSource; import com.sparrowwallet.sparrow.net.ExchangeSource;
import javafx.application.Platform;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.control.TreeItem; import javafx.scene.control.TreeItem;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.controlsfx.control.MasterDetailPane;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -26,13 +30,18 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collections; import java.util.Collections;
import java.util.Date;
import java.util.Locale; import java.util.Locale;
import java.util.ResourceBundle; import java.util.ResourceBundle;
public class TransactionsController extends WalletFormController implements Initializable { public class TransactionsController extends WalletFormController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(TransactionsController.class); private static final Logger log = LoggerFactory.getLogger(TransactionsController.class);
private static final DateFormat LOG_DATE_FORMAT = new SimpleDateFormat("[MMM dd HH:mm:ss]");
@FXML @FXML
private CoinLabel balance; private CoinLabel balance;
@ -48,9 +57,15 @@ public class TransactionsController extends WalletFormController implements Init
@FXML @FXML
private CopyableLabel transactionCount; private CopyableLabel transactionCount;
@FXML
private MasterDetailPane transactionsMasterDetail;
@FXML @FXML
private TransactionsTreeTable transactionsTable; private TransactionsTreeTable transactionsTable;
@FXML
private TextArea loadingLog;
@FXML @FXML
private BalanceChart balanceChart; private BalanceChart balanceChart;
@ -85,6 +100,10 @@ public class TransactionsController extends WalletFormController implements Init
balanceChart.select((TransactionEntry)selectedItem.getValue()); balanceChart.select((TransactionEntry)selectedItem.getValue());
} }
}); });
transactionsMasterDetail.setShowDetailNode(Config.get().isShowLoadingLog());
loadingLog.appendText("Wallet loading history for " + getWalletForm().getWallet().getName());
loadingLog.setEditable(false);
} }
private void setFiatBalance(FiatLabel fiatLabel, CurrencyRate currencyRate, long balance) { private void setFiatBalance(FiatLabel fiatLabel, CurrencyRate currencyRate, long balance) {
@ -136,6 +155,15 @@ public class TransactionsController extends WalletFormController implements Init
String.format(Locale.ENGLISH, "%d", value); String.format(Locale.ENGLISH, "%d", value);
} }
private void logMessage(String logMessage) {
if(logMessage != null) {
logMessage = logMessage.replace("m/", "/");
String date = LOG_DATE_FORMAT.format(new Date());
String logLine = "\n" + date + " " + logMessage;
Platform.runLater(() -> loadingLog.appendText(logLine));
}
}
@Subscribe @Subscribe
public void walletNodesChanged(WalletNodesChangedEvent event) { public void walletNodesChanged(WalletNodesChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) { if(event.getWallet().equals(walletForm.getWallet())) {
@ -200,6 +228,18 @@ public class TransactionsController extends WalletFormController implements Init
@Subscribe @Subscribe
public void walletHistoryStatus(WalletHistoryStatusEvent event) { public void walletHistoryStatus(WalletHistoryStatusEvent event) {
transactionsTable.updateHistoryStatus(event); transactionsTable.updateHistoryStatus(event);
if(event.getWallet() != null && getWalletForm().getWallet() == event.getWallet()) {
String logMessage = event.getStatusMessage();
if(logMessage == null) {
if(event instanceof WalletHistoryFinishedEvent) {
logMessage = "Finished loading.";
} else if(event instanceof WalletHistoryFailedEvent) {
logMessage = event.getErrorMessage();
}
}
logMessage(logMessage);
}
} }
@Subscribe @Subscribe
@ -233,4 +273,9 @@ public class TransactionsController extends WalletFormController implements Init
public void includeMempoolOutputsChangedEvent(IncludeMempoolOutputsChangedEvent event) { public void includeMempoolOutputsChangedEvent(IncludeMempoolOutputsChangedEvent event) {
walletHistoryChanged(new WalletHistoryChangedEvent(getWalletForm().getWallet(), getWalletForm().getStorage(), Collections.emptyList())); walletHistoryChanged(new WalletHistoryChangedEvent(getWalletForm().getWallet(), getWalletForm().getStorage(), Collections.emptyList()));
} }
@Subscribe
public void loadingLogChanged(LoadingLogChangedEvent event) {
transactionsMasterDetail.setShowDetailNode(event.isVisible());
}
} }

View file

@ -88,6 +88,7 @@
<CheckMenuItem fx:id="openWalletsInNewWindows" mnemonicParsing="false" text="Open Wallets in New Windows" onAction="#openWalletsInNewWindows"/> <CheckMenuItem fx:id="openWalletsInNewWindows" mnemonicParsing="false" text="Open Wallets in New Windows" onAction="#openWalletsInNewWindows"/>
<CheckMenuItem fx:id="hideEmptyUsedAddresses" mnemonicParsing="false" text="Hide Empty Used Addresses" onAction="#hideEmptyUsedAddresses"/> <CheckMenuItem fx:id="hideEmptyUsedAddresses" mnemonicParsing="false" text="Hide Empty Used Addresses" onAction="#hideEmptyUsedAddresses"/>
<CheckMenuItem fx:id="useHdCameraResolution" mnemonicParsing="false" text="Use HD Camera Resolution" onAction="#useHdCameraResolution"/> <CheckMenuItem fx:id="useHdCameraResolution" mnemonicParsing="false" text="Use HD Camera Resolution" onAction="#useHdCameraResolution"/>
<CheckMenuItem fx:id="showLoadingLog" mnemonicParsing="false" text="Show Wallet Loading Log" onAction="#showLoadingLog" />
<CheckMenuItem fx:id="showTxHex" mnemonicParsing="false" text="Show Transaction Hex" onAction="#showTxHex"/> <CheckMenuItem fx:id="showTxHex" mnemonicParsing="false" text="Show Transaction Hex" onAction="#showTxHex"/>
<SeparatorMenuItem /> <SeparatorMenuItem />
<MenuItem fx:id="minimizeToTray" mnemonicParsing="false" text="Minimize to System Tray" accelerator="Shortcut+Y" onAction="#minimizeToTray"/> <MenuItem fx:id="minimizeToTray" mnemonicParsing="false" text="Minimize to System Tray" accelerator="Shortcut+Y" onAction="#minimizeToTray"/>

View file

@ -16,6 +16,7 @@
<?import com.sparrowwallet.sparrow.control.FiatLabel?> <?import com.sparrowwallet.sparrow.control.FiatLabel?>
<?import com.sparrowwallet.sparrow.control.CopyableLabel?> <?import com.sparrowwallet.sparrow.control.CopyableLabel?>
<?import org.controlsfx.glyphfont.Glyph?> <?import org.controlsfx.glyphfont.Glyph?>
<?import org.controlsfx.control.MasterDetailPane?>
<BorderPane stylesheets="@transactions.css, @wallet.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.TransactionsController"> <BorderPane stylesheets="@transactions.css, @wallet.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.TransactionsController">
<center> <center>
@ -61,7 +62,14 @@
</yAxis> </yAxis>
</BalanceChart> </BalanceChart>
</GridPane> </GridPane>
<TransactionsTreeTable fx:id="transactionsTable" VBox.vgrow="ALWAYS" /> <MasterDetailPane fx:id="transactionsMasterDetail" detailSide="BOTTOM" VBox.vgrow="ALWAYS">
<masterNode>
<TransactionsTreeTable fx:id="transactionsTable" />
</masterNode>
<detailNode>
<TextArea fx:id="loadingLog" styleClass="readonly,fixed-width" />
</detailNode>
</MasterDetailPane>
</VBox> </VBox>
</center> </center>
</BorderPane> </BorderPane>