configure a block explorer url, and open a txid in the configured block explorer

This commit is contained in:
Craig Raw 2023-02-28 13:19:24 +02:00
parent 90a9030ecb
commit dfe1f16495
10 changed files with 185 additions and 34 deletions

2
drongo

@ -1 +1 @@
Subproject commit d0da764aadfc9edc2a7fb35540eca0ee4c20ba0f Subproject commit 0f78efc373e99cf48a749ced25cd112ca083b88f

View file

@ -811,6 +811,20 @@ public class AppServices {
} }
} }
public static void openBlockExplorer(String txid) {
Server blockExplorer = Config.get().getBlockExplorer() == null ? BlockExplorer.MEMPOOL_SPACE.getServer() : Config.get().getBlockExplorer();
String url = blockExplorer.getUrl();
if(url.contains("{0}")) {
url = url.replace("{0}", txid);
} else {
if(Network.get() != Network.MAINNET) {
url += "/" + Network.get().getName();
}
url += "/tx/" + txid;
}
AppServices.get().getApplication().getHostServices().showDocument(url);
}
static void parseFileUriArguments(List<String> fileUriArguments) { static void parseFileUriArguments(List<String> fileUriArguments) {
for(String fileUri : fileUriArguments) { for(String fileUri : fileUriArguments) {
try { try {

View file

@ -537,6 +537,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
getItems().add(createCpfp); getItems().add(createCpfp);
} }
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
openBlockExplorer.setOnAction(AE -> {
hide();
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
});
getItems().add(openBlockExplorer);
MenuItem copyTxid = new MenuItem("Copy Transaction ID"); MenuItem copyTxid = new MenuItem("Copy Transaction ID");
copyTxid.setOnAction(AE -> { copyTxid.setOnAction(AE -> {
hide(); hide();
@ -558,6 +565,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction)); EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
}); });
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
openBlockExplorer.setOnAction(AE -> {
hide();
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
});
MenuItem copyDate = new MenuItem("Copy Date"); MenuItem copyDate = new MenuItem("Copy Date");
copyDate.setOnAction(AE -> { copyDate.setOnAction(AE -> {
hide(); hide();
@ -582,7 +595,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
Clipboard.getSystemClipboard().setContent(content); Clipboard.getSystemClipboard().setContent(content);
}); });
getItems().addAll(viewTransaction, copyDate, copyTxid, copyHeight); getItems().addAll(viewTransaction, openBlockExplorer, copyDate, copyTxid, copyHeight);
} }
} }

View file

@ -76,6 +76,7 @@ public class FontAwesome5 extends GlyphFont {
TOGGLE_OFF('\uf204'), TOGGLE_OFF('\uf204'),
TOGGLE_ON('\uf205'), TOGGLE_ON('\uf205'),
TOOLS('\uf7d9'), TOOLS('\uf7d9'),
UP_RIGHT_FROM_SQUARE('\uf35d'),
UNDO('\uf0e2'), UNDO('\uf0e2'),
USER('\uf007'), USER('\uf007'),
USER_FRIENDS('\uf500'), USER_FRIENDS('\uf500'),

View file

@ -30,6 +30,7 @@ public class Config {
private Mode mode; private Mode mode;
private BitcoinUnit bitcoinUnit; private BitcoinUnit bitcoinUnit;
private UnitFormat unitFormat; private UnitFormat unitFormat;
private Server blockExplorer;
private FeeRatesSource feeRatesSource; private FeeRatesSource feeRatesSource;
private FeeRatesSelection feeRatesSelection; private FeeRatesSelection feeRatesSelection;
private OptimizationStrategy sendOptimizationStrategy; private OptimizationStrategy sendOptimizationStrategy;
@ -150,6 +151,15 @@ public class Config {
flush(); flush();
} }
public Server getBlockExplorer() {
return blockExplorer;
}
public void setBlockExplorer(Server blockExplorer) {
this.blockExplorer = blockExplorer;
flush();
}
public FeeRatesSource getFeeRatesSource() { public FeeRatesSource getFeeRatesSource() {
return feeRatesSource; return feeRatesSource;
} }

View file

@ -0,0 +1,18 @@
package com.sparrowwallet.sparrow.net;
import com.sparrowwallet.sparrow.io.Server;
public enum BlockExplorer {
MEMPOOL_SPACE("https://mempool.space"),
BLOCKSTREAM_INFO("https://blockstream.info");
private final Server server;
BlockExplorer(String url) {
this.server = new Server(url);
}
public Server getServer() {
return server;
}
}

View file

@ -1,32 +1,44 @@
package com.sparrowwallet.sparrow.preferences; package com.sparrowwallet.sparrow.preferences;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch; import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Server;
import com.sparrowwallet.sparrow.net.BlockExplorer;
import com.sparrowwallet.sparrow.net.ExchangeSource; import com.sparrowwallet.sparrow.net.ExchangeSource;
import com.sparrowwallet.sparrow.net.FeeRatesSource; import com.sparrowwallet.sparrow.net.FeeRatesSource;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
import javafx.scene.control.TextInputDialog;
import javafx.util.StringConverter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Currency; import java.util.Currency;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class GeneralPreferencesController extends PreferencesDetailController { public class GeneralPreferencesController extends PreferencesDetailController {
private static final Logger log = LoggerFactory.getLogger(GeneralPreferencesController.class); private static final Logger log = LoggerFactory.getLogger(GeneralPreferencesController.class);
@FXML private static final Server CUSTOM_BLOCK_EXPLORER = new Server("http://custom.block.explorer");
private ComboBox<BitcoinUnit> bitcoinUnit;
@FXML @FXML
private ComboBox<FeeRatesSource> feeRatesSource; private ComboBox<FeeRatesSource> feeRatesSource;
@FXML
private ComboBox<Server> blockExplorers;
@FXML @FXML
private ComboBox<Currency> fiatCurrency; private ComboBox<Currency> fiatCurrency;
@ -63,17 +75,6 @@ public class GeneralPreferencesController extends PreferencesDetailController {
@Override @Override
public void initializeView(Config config) { public void initializeView(Config config) {
if(config.getBitcoinUnit() != null) {
bitcoinUnit.setValue(config.getBitcoinUnit());
} else {
bitcoinUnit.setValue(BitcoinUnit.AUTO);
}
bitcoinUnit.valueProperty().addListener((observable, oldValue, newValue) -> {
config.setBitcoinUnit(newValue);
EventManager.get().post(new BitcoinUnitChangedEvent(newValue));
});
if(config.getFeeRatesSource() != null) { if(config.getFeeRatesSource() != null) {
feeRatesSource.setValue(config.getFeeRatesSource()); feeRatesSource.setValue(config.getFeeRatesSource());
} else { } else {
@ -86,6 +87,62 @@ public class GeneralPreferencesController extends PreferencesDetailController {
EventManager.get().post(new FeeRatesSourceChangedEvent(newValue)); EventManager.get().post(new FeeRatesSourceChangedEvent(newValue));
}); });
blockExplorers.setItems(getBlockExplorerList());
blockExplorers.setConverter(new StringConverter<>() {
@Override
public String toString(Server server) {
if(server == null) {
return "None Available";
}
if(server == CUSTOM_BLOCK_EXPLORER) {
return "Custom...";
}
return server.getHost();
}
@Override
public Server fromString(String string) {
return null;
}
});
blockExplorers.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null) {
if(newValue == CUSTOM_BLOCK_EXPLORER) {
TextInputDialog textInputDialog = new TextInputDialog();
textInputDialog.setTitle("Enter Block Explorer URL");
textInputDialog.setHeaderText("Enter the URL of the block explorer.\n\nIf present, the characters {0} will be replaced with the txid.\nFor example, https://localhost or https://localhost/tx/{0}\n");
textInputDialog.getEditor().setPromptText("https://localhost");
Optional<String> optUrl = textInputDialog.showAndWait();
if(optUrl.isPresent() && !optUrl.get().isEmpty()) {
try {
Server server = getBlockExplorer(optUrl.get());
config.setBlockExplorer(server);
Platform.runLater(() -> {
blockExplorers.getSelectionModel().select(-1);
blockExplorers.setItems(getBlockExplorerList());
blockExplorers.setValue(Config.get().getBlockExplorer());
});
} catch(Exception e) {
AppServices.showErrorDialog("Invalid URL", "The URL " + optUrl.get() + " is not valid.");
blockExplorers.setValue(oldValue);
}
} else {
blockExplorers.setValue(oldValue);
}
} else {
Config.get().setBlockExplorer(newValue);
}
}
});
if(config.getBlockExplorer() != null) {
blockExplorers.setValue(config.getBlockExplorer());
} else {
blockExplorers.getSelectionModel().select(0);
}
if(config.getExchangeSource() != null) { if(config.getExchangeSource() != null) {
exchangeSource.setValue(config.getExchangeSource()); exchangeSource.setValue(config.getExchangeSource());
} else { } else {
@ -134,6 +191,23 @@ public class GeneralPreferencesController extends PreferencesDetailController {
}); });
} }
private static Server getBlockExplorer(String serverUrl) {
String url = serverUrl.trim();
if(url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
return new Server(url);
}
private ObservableList<Server> getBlockExplorerList() {
List<Server> servers = Arrays.stream(BlockExplorer.values()).map(BlockExplorer::getServer).collect(Collectors.toList());
if(Config.get().getBlockExplorer() != null && !servers.contains(Config.get().getBlockExplorer())) {
servers.add(Config.get().getBlockExplorer());
}
servers.add(CUSTOM_BLOCK_EXPLORER);
return FXCollections.observableList(servers);
}
private void updateCurrencies(ExchangeSource exchangeSource) { private void updateCurrencies(ExchangeSource exchangeSource) {
ExchangeSource.CurrenciesService currenciesService = new ExchangeSource.CurrenciesService(exchangeSource); ExchangeSource.CurrenciesService currenciesService = new ExchangeSource.CurrenciesService(exchangeSource);
currenciesService.setOnSucceeded(event -> { currenciesService.setOnSucceeded(event -> {

View file

@ -78,6 +78,12 @@ public class HeadersController extends TransactionFormController implements Init
@FXML @FXML
private IdLabel id; private IdLabel id;
@FXML
private ToggleButton copyTxid;
@FXML
private ToggleButton openBlockExplorer;
@FXML @FXML
private TransactionDiagram transactionDiagram; private TransactionDiagram transactionDiagram;
@ -806,11 +812,13 @@ public class HeadersController extends TransactionFormController implements Init
addStyleClass(size, UNFINALIZED_TXID_CLASS); addStyleClass(size, UNFINALIZED_TXID_CLASS);
addStyleClass(virtualSize, UNFINALIZED_TXID_CLASS); addStyleClass(virtualSize, UNFINALIZED_TXID_CLASS);
addStyleClass(feeRate, UNFINALIZED_TXID_CLASS); addStyleClass(feeRate, UNFINALIZED_TXID_CLASS);
openBlockExplorer.setDisable(true);
} else { } else {
id.getStyleClass().remove(UNFINALIZED_TXID_CLASS); id.getStyleClass().remove(UNFINALIZED_TXID_CLASS);
size.getStyleClass().remove(UNFINALIZED_TXID_CLASS); size.getStyleClass().remove(UNFINALIZED_TXID_CLASS);
virtualSize.getStyleClass().remove(UNFINALIZED_TXID_CLASS); virtualSize.getStyleClass().remove(UNFINALIZED_TXID_CLASS);
feeRate.getStyleClass().remove(UNFINALIZED_TXID_CLASS); feeRate.getStyleClass().remove(UNFINALIZED_TXID_CLASS);
openBlockExplorer.setDisable(false);
} }
} }
@ -821,11 +829,18 @@ public class HeadersController extends TransactionFormController implements Init
} }
public void copyId(ActionEvent event) { public void copyId(ActionEvent event) {
copyTxid.setSelected(false);
ClipboardContent content = new ClipboardContent(); ClipboardContent content = new ClipboardContent();
content.putString(headersForm.getTransaction().calculateTxId(false).toString()); content.putString(headersForm.getTransaction().calculateTxId(false).toString());
Clipboard.getSystemClipboard().setContent(content); Clipboard.getSystemClipboard().setContent(content);
} }
public void openInBlockExplorer(ActionEvent event) {
openBlockExplorer.setSelected(false);
String txid = headersForm.getTransaction().calculateTxId(false).toString();
AppServices.openBlockExplorer(txid);
}
public void setLocktimeToCurrentHeight(ActionEvent event) { public void setLocktimeToCurrentHeight(ActionEvent event) {
if(AppServices.getCurrentBlockHeight() != null && locktimeBlock.isEditable()) { if(AppServices.getCurrentBlockHeight() != null && locktimeBlock.isEditable()) {
locktimeBlock.getValueFactory().setValue(AppServices.getCurrentBlockHeight()); locktimeBlock.getValueFactory().setValue(AppServices.getCurrentBlockHeight());

View file

@ -11,7 +11,6 @@
<?import tornadofx.control.Field?> <?import tornadofx.control.Field?>
<?import javafx.collections.FXCollections?> <?import javafx.collections.FXCollections?>
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import com.sparrowwallet.drongo.BitcoinUnit?>
<?import com.sparrowwallet.sparrow.net.ExchangeSource?> <?import com.sparrowwallet.sparrow.net.ExchangeSource?>
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?> <?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
<?import com.sparrowwallet.sparrow.control.HelpLabel?> <?import com.sparrowwallet.sparrow.control.HelpLabel?>
@ -30,18 +29,6 @@
<Form GridPane.columnIndex="0" GridPane.rowIndex="0"> <Form GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset inputGrow="SOMETIMES" text="Bitcoin" styleClass="wideLabelFieldSet"> <Fieldset inputGrow="SOMETIMES" text="Bitcoin" styleClass="wideLabelFieldSet">
<Field text="Display unit:">
<ComboBox fx:id="bitcoinUnit">
<items>
<FXCollections fx:factory="observableArrayList">
<BitcoinUnit fx:constant="AUTO" />
<BitcoinUnit fx:constant="BTC" />
<BitcoinUnit fx:constant="SATOSHIS" />
</FXCollections>
</items>
</ComboBox>
<HelpLabel helpText="Display unit for bitcoin amounts.\nAuto displays amounts over 1 BTC in BTC, and amounts under that in satoshis."/>
</Field>
<Field text="Fee rates source:"> <Field text="Fee rates source:">
<ComboBox fx:id="feeRatesSource"> <ComboBox fx:id="feeRatesSource">
<items> <items>
@ -54,6 +41,10 @@
</ComboBox> </ComboBox>
<HelpLabel helpText="Recommended fee rates can be sourced from your connected Electrum server, or an external source."/> <HelpLabel helpText="Recommended fee rates can be sourced from your connected Electrum server, or an external source."/>
</Field> </Field>
<Field text="Block explorer:">
<ComboBox fx:id="blockExplorers" />
<HelpLabel helpText="An external block explorer can be configured to open a transaction from any transaction tab."/>
</Field>
</Fieldset> </Fieldset>
<Fieldset inputGrow="SOMETIMES" text="Fiat" styleClass="wideLabelFieldSet"> <Fieldset inputGrow="SOMETIMES" text="Fiat" styleClass="wideLabelFieldSet">
<Field text="Currency:"> <Field text="Currency:">

View file

@ -45,11 +45,26 @@
<Fieldset text="Transaction" inputGrow="SOMETIMES" wrapWidth="620"> <Fieldset text="Transaction" inputGrow="SOMETIMES" wrapWidth="620">
<Field text="Txid:" styleClass="label-button"> <Field text="Txid:" styleClass="label-button">
<IdLabel fx:id="id"/> <IdLabel fx:id="id"/>
<Button maxWidth="25" minWidth="-Infinity" prefWidth="30" text="Cy" onAction="#copyId"> <SegmentedButton>
<graphic> <buttons>
<Glyph fontFamily="FontAwesome" icon="COPY" prefWidth="15" /> <ToggleButton fx:id="copyTxid" onAction="#copyId">
</graphic> <graphic>
</Button> <Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="COPY" />
</graphic>
<tooltip>
<Tooltip text="Copy the transaction id to the clipboard"/>
</tooltip>
</ToggleButton>
<ToggleButton fx:id="openBlockExplorer" onAction="#openInBlockExplorer">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="UP_RIGHT_FROM_SQUARE" />
</graphic>
<tooltip>
<Tooltip text="Open transaction in the configured block explorer"/>
</tooltip>
</ToggleButton>
</buttons>
</SegmentedButton>
</Field> </Field>
</Fieldset> </Fieldset>
</Form> </Form>