mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 05:06:45 +00:00
fetch transaction output transactions, handle coinbase input
This commit is contained in:
parent
86eb8b8294
commit
b1b4734094
17 changed files with 491 additions and 48 deletions
|
@ -9,6 +9,7 @@ import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||||
import com.sparrowwallet.drongo.crypto.Key;
|
import com.sparrowwallet.drongo.crypto.Key;
|
||||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||||
|
@ -23,6 +24,7 @@ import com.sparrowwallet.sparrow.wallet.WalletController;
|
||||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||||
import de.codecentric.centerdevice.MenuToolkit;
|
import de.codecentric.centerdevice.MenuToolkit;
|
||||||
import javafx.animation.*;
|
import javafx.animation.*;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.concurrent.Worker;
|
import javafx.concurrent.Worker;
|
||||||
|
@ -57,6 +59,9 @@ public class AppController implements Initializable {
|
||||||
@FXML
|
@FXML
|
||||||
private Menu fileMenu;
|
private Menu fileMenu;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private MenuItem openTransactionIdItem;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private CheckMenuItem showTxHex;
|
private CheckMenuItem showTxHex;
|
||||||
|
|
||||||
|
@ -166,6 +171,8 @@ public class AppController implements Initializable {
|
||||||
connectionService.start();
|
connectionService.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openTransactionIdItem.disableProperty().bind(onlineProperty.not());
|
||||||
|
|
||||||
openWalletFile(new File("/Users/scy/.sparrow/wallets/sparta.json"));
|
openWalletFile(new File("/Users/scy/.sparrow/wallets/sparta.json"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -294,6 +301,32 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void openTransactionFromId(ActionEvent event) {
|
||||||
|
TransactionIdDialog dialog = new TransactionIdDialog();
|
||||||
|
Optional<Sha256Hash> optionalTxId = dialog.showAndWait();
|
||||||
|
if(optionalTxId.isPresent()) {
|
||||||
|
Sha256Hash txId = optionalTxId.get();
|
||||||
|
ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(Set.of(txId));
|
||||||
|
transactionReferenceService.setOnSucceeded(successEvent -> {
|
||||||
|
BlockTransaction blockTransaction = transactionReferenceService.getValue().get(txId);
|
||||||
|
if(blockTransaction == null) {
|
||||||
|
showErrorDialog("Invalid transaction ID", "A transaction with that ID could not be found.");
|
||||||
|
} else {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
EventManager.get().post(new ViewTransactionEvent(blockTransaction));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
transactionReferenceService.setOnFailed(failEvent -> {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
Throwable e = failEvent.getSource().getException();
|
||||||
|
showErrorDialog("Error fetching transaction", e.getCause() != null ? e.getCause().getMessage() : e.getMessage());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
transactionReferenceService.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isOnline() {
|
public static boolean isOnline() {
|
||||||
return onlineProperty.get();
|
return onlineProperty.get();
|
||||||
}
|
}
|
||||||
|
@ -564,7 +597,7 @@ public class AppController implements Initializable {
|
||||||
TabData tabData = (TabData)tab.getUserData();
|
TabData tabData = (TabData)tab.getUserData();
|
||||||
if(tabData instanceof TransactionTabData) {
|
if(tabData instanceof TransactionTabData) {
|
||||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||||
if(transactionTabData.getTransaction() == transaction) {
|
if(transactionTabData.getTransaction().getTxId().equals(transaction.getTxId())) {
|
||||||
return tab;
|
return tab;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,9 @@ public class CoinLabel extends CopyableLabel {
|
||||||
|
|
||||||
public static final DecimalFormat BTC_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
public static final DecimalFormat BTC_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||||
|
|
||||||
private final LongProperty value = new SimpleLongProperty();
|
private final LongProperty value = new SimpleLongProperty(-1);
|
||||||
private Tooltip tooltip;
|
private final Tooltip tooltip;
|
||||||
private CoinContextMenu contextMenu;
|
private final CoinContextMenu contextMenu;
|
||||||
|
|
||||||
public CoinLabel() {
|
public CoinLabel() {
|
||||||
this("Unknown");
|
this("Unknown");
|
||||||
|
@ -62,11 +62,8 @@ public class CoinLabel extends CopyableLabel {
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CoinContextMenu extends ContextMenu {
|
private class CoinContextMenu extends ContextMenu {
|
||||||
private MenuItem copySatsValue;
|
|
||||||
private MenuItem copyBtcValue;
|
|
||||||
|
|
||||||
public CoinContextMenu() {
|
public CoinContextMenu() {
|
||||||
copySatsValue = new MenuItem("Copy Value in Satoshis");
|
MenuItem copySatsValue = new MenuItem("Copy Value in Satoshis");
|
||||||
copySatsValue.setOnAction(AE -> {
|
copySatsValue.setOnAction(AE -> {
|
||||||
hide();
|
hide();
|
||||||
ClipboardContent content = new ClipboardContent();
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
@ -74,7 +71,7 @@ public class CoinLabel extends CopyableLabel {
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
copyBtcValue = new MenuItem("Copy Value in BTC");
|
MenuItem copyBtcValue = new MenuItem("Copy Value in BTC");
|
||||||
copyBtcValue.setOnAction(AE -> {
|
copyBtcValue.setOnAction(AE -> {
|
||||||
hide();
|
hide();
|
||||||
ClipboardContent content = new ClipboardContent();
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
|
import com.sparrowwallet.sparrow.AppController;
|
||||||
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
import org.controlsfx.control.textfield.CustomTextField;
|
||||||
|
import org.controlsfx.control.textfield.TextFields;
|
||||||
|
import org.controlsfx.glyphfont.FontAwesome;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
import org.controlsfx.validation.ValidationResult;
|
||||||
|
import org.controlsfx.validation.ValidationSupport;
|
||||||
|
import org.controlsfx.validation.Validator;
|
||||||
|
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||||
|
|
||||||
|
public class TransactionIdDialog extends Dialog<Sha256Hash> {
|
||||||
|
private final CustomTextField txid;
|
||||||
|
|
||||||
|
public TransactionIdDialog() {
|
||||||
|
this.txid = (CustomTextField) TextFields.createClearableTextField();
|
||||||
|
final DialogPane dialogPane = getDialogPane();
|
||||||
|
|
||||||
|
setTitle("Load Transaction");
|
||||||
|
dialogPane.setHeaderText("Enter the transaction ID:");
|
||||||
|
dialogPane.getStylesheets().add(AppController.class.getResource("general.css").toExternalForm());
|
||||||
|
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||||
|
dialogPane.setPrefWidth(380);
|
||||||
|
dialogPane.setPrefHeight(200);
|
||||||
|
|
||||||
|
Glyph wallet = new Glyph("FontAwesome", FontAwesome.Glyph.BITCOIN);
|
||||||
|
wallet.setFontSize(50);
|
||||||
|
dialogPane.setGraphic(wallet);
|
||||||
|
|
||||||
|
final VBox content = new VBox(10);
|
||||||
|
content.getChildren().add(txid);
|
||||||
|
|
||||||
|
dialogPane.setContent(content);
|
||||||
|
|
||||||
|
ValidationSupport validationSupport = new ValidationSupport();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
validationSupport.registerValidator(txid, Validator.combine(
|
||||||
|
Validator.createEmptyValidator("Transaction id is required"),
|
||||||
|
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Transaction ID length incorrect", newValue.length() != 64),
|
||||||
|
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Transaction ID must be hexadecimal", !Utils.isHex(newValue))
|
||||||
|
));
|
||||||
|
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||||
|
});
|
||||||
|
|
||||||
|
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Open Transaction", ButtonBar.ButtonData.OK_DONE);
|
||||||
|
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||||
|
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
|
||||||
|
BooleanBinding isInvalid = Bindings.createBooleanBinding(() ->
|
||||||
|
txid.getText().length() != 64 || !Utils.isHex(txid.getText()), txid.textProperty());
|
||||||
|
okButton.disableProperty().bind(isInvalid);
|
||||||
|
|
||||||
|
txid.setPromptText("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16");
|
||||||
|
txid.requestFocus();
|
||||||
|
setResultConverter(dialogButton -> dialogButton == okButtonType ? Sha256Hash.wrap(txid.getText()) : null);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
|
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class BlockTransactionOutputsFetchedEvent {
|
||||||
|
private final Sha256Hash txId;
|
||||||
|
private final List<BlockTransaction> outputTransactions;
|
||||||
|
|
||||||
|
public BlockTransactionOutputsFetchedEvent(Sha256Hash txId, List<BlockTransaction> outputTransactions) {
|
||||||
|
this.txId = txId;
|
||||||
|
this.outputTransactions = outputTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Sha256Hash getTxId() {
|
||||||
|
return txId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BlockTransaction> getOutputTransactions() {
|
||||||
|
return outputTransactions;
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,8 @@ import java.util.stream.Collectors;
|
||||||
public class ElectrumServer {
|
public class ElectrumServer {
|
||||||
private static final String[] SUPPORTED_VERSIONS = new String[]{"1.3", "1.4.2"};
|
private static final String[] SUPPORTED_VERSIONS = new String[]{"1.3", "1.4.2"};
|
||||||
|
|
||||||
|
public static final BlockTransaction UNFETCHABLE_BLOCK_TRANSACTION = new BlockTransaction(Sha256Hash.ZERO_HASH, 0, null, null, null);
|
||||||
|
|
||||||
private static Transport transport;
|
private static Transport transport;
|
||||||
|
|
||||||
private static synchronized Transport getTransport() throws ServerException {
|
private static synchronized Transport getTransport() throws ServerException {
|
||||||
|
@ -134,7 +136,8 @@ public class ElectrumServer {
|
||||||
|
|
||||||
public void getHistory(Wallet wallet, KeyPurpose keyPurpose, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap) throws ServerException {
|
public void getHistory(Wallet wallet, KeyPurpose keyPurpose, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap) throws ServerException {
|
||||||
getHistory(wallet, wallet.getNode(keyPurpose).getChildren(), nodeTransactionMap);
|
getHistory(wallet, wallet.getNode(keyPurpose).getChildren(), nodeTransactionMap);
|
||||||
getMempool(wallet, wallet.getNode(keyPurpose).getChildren(), nodeTransactionMap);
|
//Not necessary, mempool transactions included in history
|
||||||
|
//getMempool(wallet, wallet.getNode(keyPurpose).getChildren(), nodeTransactionMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void getHistory(Wallet wallet, Collection<WalletNode> nodes, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap) throws ServerException {
|
public void getHistory(Wallet wallet, Collection<WalletNode> nodes, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap) throws ServerException {
|
||||||
|
@ -189,6 +192,47 @@ public class ElectrumServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public List<Set<BlockTransactionHash>> getOutputTransactionReferences(Transaction transaction) throws ServerException {
|
||||||
|
try {
|
||||||
|
JsonRpcClient client = new JsonRpcClient(getTransport());
|
||||||
|
BatchRequestBuilder<Integer, ScriptHashTx[]> batchRequest = client.createBatchRequest().keysType(Integer.class).returnType(ScriptHashTx[].class);
|
||||||
|
for(int i = 0; i < transaction.getOutputs().size(); i++) {
|
||||||
|
TransactionOutput output = transaction.getOutputs().get(i);
|
||||||
|
batchRequest.add(i, "blockchain.scripthash.get_history", getScriptHash(output));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Integer, ScriptHashTx[]> result;
|
||||||
|
try {
|
||||||
|
result = batchRequest.execute();
|
||||||
|
} catch (JsonRpcBatchException e) {
|
||||||
|
result = (Map<Integer, ScriptHashTx[]>)e.getSuccesses();
|
||||||
|
for(Object index : e.getErrors().keySet()) {
|
||||||
|
Integer i = (Integer)index;
|
||||||
|
result.put(i, new ScriptHashTx[] {ScriptHashTx.ERROR_TX});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Set<BlockTransactionHash>> blockTransactionHashes = new ArrayList<>(transaction.getOutputs().size());
|
||||||
|
for(int i = 0; i < transaction.getOutputs().size(); i++) {
|
||||||
|
blockTransactionHashes.add(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(Integer index : result.keySet()) {
|
||||||
|
ScriptHashTx[] txes = result.get(index);
|
||||||
|
|
||||||
|
Set<BlockTransactionHash> references = Arrays.stream(txes).map(ScriptHashTx::getBlockchainTransactionHash).filter(ref -> !ref.getHash().equals(transaction.getTxId())).collect(Collectors.toCollection(TreeSet::new));
|
||||||
|
blockTransactionHashes.set(index, references);
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockTransactionHashes;
|
||||||
|
} catch (IllegalStateException e) {
|
||||||
|
throw new ServerException(e.getCause());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new ServerException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void getReferencedTransactions(Wallet wallet, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap) throws ServerException {
|
public void getReferencedTransactions(Wallet wallet, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap) throws ServerException {
|
||||||
Set<BlockTransactionHash> references = new TreeSet<>();
|
Set<BlockTransactionHash> references = new TreeSet<>();
|
||||||
for(Set<BlockTransactionHash> nodeReferences : nodeTransactionMap.values()) {
|
for(Set<BlockTransactionHash> nodeReferences : nodeTransactionMap.values()) {
|
||||||
|
@ -209,6 +253,7 @@ public class ElectrumServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public Map<Integer, BlockHeader> getBlockHeaders(Set<BlockTransactionHash> references) throws ServerException {
|
public Map<Integer, BlockHeader> getBlockHeaders(Set<BlockTransactionHash> references) throws ServerException {
|
||||||
try {
|
try {
|
||||||
Set<Integer> blockHeights = new TreeSet<>();
|
Set<Integer> blockHeights = new TreeSet<>();
|
||||||
|
@ -221,7 +266,13 @@ public class ElectrumServer {
|
||||||
for(Integer height : blockHeights) {
|
for(Integer height : blockHeights) {
|
||||||
batchRequest.add(height, "blockchain.block.header", height);
|
batchRequest.add(height, "blockchain.block.header", height);
|
||||||
}
|
}
|
||||||
Map<Integer, String> result = batchRequest.execute();
|
|
||||||
|
Map<Integer, String> result;
|
||||||
|
try {
|
||||||
|
result = batchRequest.execute();
|
||||||
|
} catch (JsonRpcBatchException e) {
|
||||||
|
result = (Map<Integer, String>)e.getSuccesses();
|
||||||
|
}
|
||||||
|
|
||||||
Map<Integer, BlockHeader> blockHeaderMap = new TreeMap<>();
|
Map<Integer, BlockHeader> blockHeaderMap = new TreeMap<>();
|
||||||
for(Integer height : result.keySet()) {
|
for(Integer height : result.keySet()) {
|
||||||
|
@ -232,7 +283,7 @@ public class ElectrumServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!blockHeights.isEmpty()) {
|
if(!blockHeights.isEmpty()) {
|
||||||
throw new IllegalStateException("Could not retrieve blocks " + blockHeights);
|
System.out.println("Could not retrieve " + blockHeights.size() + " blocks");
|
||||||
}
|
}
|
||||||
|
|
||||||
return blockHeaderMap;
|
return blockHeaderMap;
|
||||||
|
@ -243,6 +294,7 @@ public class ElectrumServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public Map<Sha256Hash, BlockTransaction> getTransactions(Set<BlockTransactionHash> references, Map<Integer, BlockHeader> blockHeaderMap) throws ServerException {
|
public Map<Sha256Hash, BlockTransaction> getTransactions(Set<BlockTransactionHash> references, Map<Integer, BlockHeader> blockHeaderMap) throws ServerException {
|
||||||
try {
|
try {
|
||||||
Set<BlockTransactionHash> checkReferences = new TreeSet<>(references);
|
Set<BlockTransactionHash> checkReferences = new TreeSet<>(references);
|
||||||
|
@ -252,11 +304,28 @@ public class ElectrumServer {
|
||||||
for(BlockTransactionHash reference : references) {
|
for(BlockTransactionHash reference : references) {
|
||||||
batchRequest.add(reference.getHashAsString(), "blockchain.transaction.get", reference.getHashAsString());
|
batchRequest.add(reference.getHashAsString(), "blockchain.transaction.get", reference.getHashAsString());
|
||||||
}
|
}
|
||||||
Map<String, String> result = batchRequest.execute();
|
|
||||||
|
Map<String, String> result;
|
||||||
|
try {
|
||||||
|
result = batchRequest.execute();
|
||||||
|
} catch (JsonRpcBatchException e) {
|
||||||
|
result = (Map<String, String>)e.getSuccesses();
|
||||||
|
for(Object hash : e.getErrors().keySet()) {
|
||||||
|
String txhash = (String)hash;
|
||||||
|
result.put(txhash, Sha256Hash.ZERO_HASH.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
|
Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
|
||||||
for(String txid : result.keySet()) {
|
for(String txid : result.keySet()) {
|
||||||
Sha256Hash hash = Sha256Hash.wrap(txid);
|
Sha256Hash hash = Sha256Hash.wrap(txid);
|
||||||
|
|
||||||
|
if(hash.equals(Sha256Hash.ZERO_HASH)) {
|
||||||
|
transactionMap.put(hash, UNFETCHABLE_BLOCK_TRANSACTION);
|
||||||
|
checkReferences.removeIf(ref -> ref.getHash().equals(hash));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
byte[] rawtx = Utils.hexToBytes(result.get(txid));
|
byte[] rawtx = Utils.hexToBytes(result.get(txid));
|
||||||
Transaction transaction = new Transaction(rawtx);
|
Transaction transaction = new Transaction(rawtx);
|
||||||
|
|
||||||
|
@ -268,7 +337,9 @@ public class ElectrumServer {
|
||||||
|
|
||||||
BlockHeader blockHeader = blockHeaderMap.get(reference.getHeight());
|
BlockHeader blockHeader = blockHeaderMap.get(reference.getHeight());
|
||||||
if(blockHeader == null) {
|
if(blockHeader == null) {
|
||||||
throw new IllegalStateException("Block header at height " + reference.getHeight() + " not retrieved");
|
transactionMap.put(hash, UNFETCHABLE_BLOCK_TRANSACTION);
|
||||||
|
checkReferences.removeIf(ref -> ref.getHash().equals(hash));
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
BlockTransaction blockchainTransaction = new BlockTransaction(reference.getHash(), reference.getHeight(), blockHeader.getTimeAsDate(), reference.getFee(), transaction);
|
BlockTransaction blockchainTransaction = new BlockTransaction(reference.getHash(), reference.getHeight(), blockHeader.getTimeAsDate(), reference.getFee(), transaction);
|
||||||
|
@ -400,7 +471,20 @@ public class ElectrumServer {
|
||||||
return Utils.bytesToHex(reversed);
|
return Utils.bytesToHex(reversed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getScriptHash(TransactionOutput output) {
|
||||||
|
byte[] hash = Sha256Hash.hash(output.getScript().getProgram());
|
||||||
|
byte[] reversed = Utils.reverseBytes(hash);
|
||||||
|
return Utils.bytesToHex(reversed);
|
||||||
|
}
|
||||||
|
|
||||||
private static class ScriptHashTx {
|
private static class ScriptHashTx {
|
||||||
|
public static final ScriptHashTx ERROR_TX = new ScriptHashTx() {
|
||||||
|
@Override
|
||||||
|
public BlockTransactionHash getBlockchainTransactionHash() {
|
||||||
|
return UNFETCHABLE_BLOCK_TRANSACTION;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public int height;
|
public int height;
|
||||||
public String tx_hash;
|
public String tx_hash;
|
||||||
public long fee;
|
public long fee;
|
||||||
|
@ -800,6 +884,62 @@ public class ElectrumServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class TransactionOutputsReferenceService extends Service<List<BlockTransaction>> {
|
||||||
|
private final Transaction transaction;
|
||||||
|
|
||||||
|
public TransactionOutputsReferenceService(Transaction transaction) {
|
||||||
|
this.transaction = transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<List<BlockTransaction>> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
protected List<BlockTransaction> call() throws ServerException {
|
||||||
|
ElectrumServer electrumServer = new ElectrumServer();
|
||||||
|
List<Set<BlockTransactionHash>> outputTransactionReferences = electrumServer.getOutputTransactionReferences(transaction);
|
||||||
|
|
||||||
|
Set<BlockTransactionHash> setReferences = new HashSet<>();
|
||||||
|
for(Set<BlockTransactionHash> outputReferences : outputTransactionReferences) {
|
||||||
|
setReferences.addAll(outputReferences);
|
||||||
|
}
|
||||||
|
setReferences.remove(null);
|
||||||
|
setReferences.remove(UNFETCHABLE_BLOCK_TRANSACTION);
|
||||||
|
|
||||||
|
List<BlockTransaction> blockTransactions = new ArrayList<>(transaction.getOutputs().size());
|
||||||
|
for(int i = 0; i < transaction.getOutputs().size(); i++) {
|
||||||
|
blockTransactions.add(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
|
||||||
|
if(!setReferences.isEmpty()) {
|
||||||
|
Map<Integer, BlockHeader> blockHeaderMap = electrumServer.getBlockHeaders(setReferences);
|
||||||
|
transactionMap = electrumServer.getTransactions(setReferences, blockHeaderMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i = 0; i < outputTransactionReferences.size(); i++) {
|
||||||
|
Set<BlockTransactionHash> outputReferences = outputTransactionReferences.get(i);
|
||||||
|
for(BlockTransactionHash reference : outputReferences) {
|
||||||
|
if(reference == UNFETCHABLE_BLOCK_TRANSACTION) {
|
||||||
|
blockTransactions.set(i, UNFETCHABLE_BLOCK_TRANSACTION);
|
||||||
|
} else {
|
||||||
|
BlockTransaction blockTransaction = transactionMap.get(reference.getHash());
|
||||||
|
for(TransactionInput input : blockTransaction.getTransaction().getInputs()) {
|
||||||
|
if(input.getOutpoint().getHash().equals(transaction.getTxId())) {
|
||||||
|
if(blockTransactions.set(i, blockTransaction) != null) {
|
||||||
|
throw new IllegalStateException("Double spend detected on hash " + reference.getHash());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockTransactions;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum Protocol {
|
public enum Protocol {
|
||||||
TCP {
|
TCP {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -210,6 +210,8 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
Long feeAmt = null;
|
Long feeAmt = null;
|
||||||
if(headersForm.getPsbt() != null) {
|
if(headersForm.getPsbt() != null) {
|
||||||
feeAmt = headersForm.getPsbt().getFee();
|
feeAmt = headersForm.getPsbt().getFee();
|
||||||
|
} else if(headersForm.getTransaction().getInputs().size() == 1 && headersForm.getTransaction().getInputs().get(0).isCoinBase()) {
|
||||||
|
feeAmt = 0L;
|
||||||
} else if(headersForm.getInputTransactions() != null) {
|
} else if(headersForm.getInputTransactions() != null) {
|
||||||
feeAmt = calculateFee(headersForm.getInputTransactions());
|
feeAmt = calculateFee(headersForm.getInputTransactions());
|
||||||
}
|
}
|
||||||
|
@ -229,6 +231,10 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
private long calculateFee(Map<Sha256Hash, BlockTransaction> inputTransactions) {
|
private long calculateFee(Map<Sha256Hash, BlockTransaction> inputTransactions) {
|
||||||
long feeAmt = 0L;
|
long feeAmt = 0L;
|
||||||
for(TransactionInput input : headersForm.getTransaction().getInputs()) {
|
for(TransactionInput input : headersForm.getTransaction().getInputs()) {
|
||||||
|
if(input.isCoinBase()) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
|
||||||
BlockTransaction inputTx = inputTransactions.get(input.getOutpoint().getHash());
|
BlockTransaction inputTx = inputTransactions.get(input.getOutpoint().getHash());
|
||||||
if(inputTx == null) {
|
if(inputTx == null) {
|
||||||
throw new IllegalStateException("Cannot find transaction for hash " + input.getOutpoint().getHash());
|
throw new IllegalStateException("Cannot find transaction for hash " + input.getOutpoint().getHash());
|
||||||
|
|
|
@ -121,7 +121,7 @@ public class InputController extends TransactionFormController implements Initia
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
public void initialize(URL location, ResourceBundle resources) {
|
||||||
|
EventManager.get().register(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initializeView() {
|
public void initializeView() {
|
||||||
|
@ -184,14 +184,17 @@ public class InputController extends TransactionFormController implements Initia
|
||||||
BlockTransaction linkedTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
|
BlockTransaction linkedTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
|
||||||
EventManager.get().post(new ViewTransactionEvent(linkedTransaction, TransactionView.OUTPUT, (int)txInput.getOutpoint().getIndex()));
|
EventManager.get().post(new ViewTransactionEvent(linkedTransaction, TransactionView.OUTPUT, (int)txInput.getOutpoint().getIndex()));
|
||||||
});
|
});
|
||||||
|
linkedOutpoint.setContextMenu(new TransactionReferenceContextMenu(linkedOutpoint.getText()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSpends(Map<Sha256Hash, BlockTransaction> inputTransactions) {
|
private void updateSpends(Map<Sha256Hash, BlockTransaction> inputTransactions) {
|
||||||
TransactionInput txInput = inputForm.getTransactionInput();
|
TransactionInput txInput = inputForm.getTransactionInput();
|
||||||
|
if(!txInput.isCoinBase()) {
|
||||||
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
|
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
|
||||||
TransactionOutput output = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
TransactionOutput output = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
||||||
updateSpends(output);
|
updateSpends(output);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void updateSpends(TransactionOutput output) {
|
private void updateSpends(TransactionOutput output) {
|
||||||
if (output != null) {
|
if (output != null) {
|
||||||
|
|
|
@ -113,6 +113,16 @@ public class InputsController extends TransactionFormController implements Initi
|
||||||
foundSigs += input.getScriptSig().getSignatures().size();
|
foundSigs += input.getScriptSig().getSignatures().size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(input.isCoinBase()) {
|
||||||
|
long totalAmt = 0;
|
||||||
|
for(TransactionOutput output : inputsForm.getTransaction().getOutputs()) {
|
||||||
|
totalAmt += output.getValue();
|
||||||
|
}
|
||||||
|
total.setValue(totalAmt);
|
||||||
|
signatures.setText("N/A");
|
||||||
|
addCoinbasePieData(inputsPie, totalAmt);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
BlockTransaction inputTx = inputTransactions.get(input.getOutpoint().getHash());
|
BlockTransaction inputTx = inputTransactions.get(input.getOutpoint().getHash());
|
||||||
if(inputTx == null) {
|
if(inputTx == null) {
|
||||||
throw new IllegalStateException("Cannot find transaction for hash " + input.getOutpoint().getHash());
|
throw new IllegalStateException("Cannot find transaction for hash " + input.getOutpoint().getHash());
|
||||||
|
@ -121,6 +131,7 @@ public class InputsController extends TransactionFormController implements Initi
|
||||||
TransactionOutput output = inputTx.getTransaction().getOutputs().get((int)input.getOutpoint().getIndex());
|
TransactionOutput output = inputTx.getTransaction().getOutputs().get((int)input.getOutpoint().getIndex());
|
||||||
outputs.add(output);
|
outputs.add(output);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
long totalAmt = 0;
|
long totalAmt = 0;
|
||||||
for(TransactionOutput output : outputs) {
|
for(TransactionOutput output : outputs) {
|
||||||
|
|
|
@ -1,17 +1,28 @@
|
||||||
package com.sparrowwallet.sparrow.transaction;
|
package com.sparrowwallet.sparrow.transaction;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
|
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
|
||||||
|
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||||
|
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.control.AddressLabel;
|
import com.sparrowwallet.sparrow.control.AddressLabel;
|
||||||
import com.sparrowwallet.sparrow.control.CoinLabel;
|
import com.sparrowwallet.sparrow.control.CoinLabel;
|
||||||
import com.sparrowwallet.sparrow.control.CopyableLabel;
|
import com.sparrowwallet.sparrow.control.CopyableLabel;
|
||||||
|
import com.sparrowwallet.sparrow.event.BlockTransactionOutputsFetchedEvent;
|
||||||
|
import com.sparrowwallet.sparrow.event.ViewTransactionEvent;
|
||||||
|
import com.sparrowwallet.sparrow.io.ElectrumServer;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.Initializable;
|
import javafx.fxml.Initializable;
|
||||||
|
import javafx.scene.control.Hyperlink;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
import org.fxmisc.richtext.CodeArea;
|
import org.fxmisc.richtext.CodeArea;
|
||||||
|
import tornadofx.control.Field;
|
||||||
import tornadofx.control.Fieldset;
|
import tornadofx.control.Fieldset;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.List;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
public class OutputController extends TransactionFormController implements Initializable {
|
public class OutputController extends TransactionFormController implements Initializable {
|
||||||
|
@ -29,12 +40,24 @@ public class OutputController extends TransactionFormController implements Initi
|
||||||
@FXML
|
@FXML
|
||||||
private AddressLabel address;
|
private AddressLabel address;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Field spentField;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label spent;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Field spentByField;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Hyperlink spentBy;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private CodeArea scriptPubKeyArea;
|
private CodeArea scriptPubKeyArea;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
public void initialize(URL location, ResourceBundle resources) {
|
||||||
|
EventManager.get().register(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initializeView() {
|
public void initializeView() {
|
||||||
|
@ -56,12 +79,63 @@ public class OutputController extends TransactionFormController implements Initi
|
||||||
//ignore
|
//ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spentField.managedProperty().bind(spentField.visibleProperty());
|
||||||
|
spentByField.managedProperty().bind(spentByField.visibleProperty());
|
||||||
|
spentByField.setVisible(false);
|
||||||
|
|
||||||
|
if(outputForm.getPsbt() != null) {
|
||||||
|
spent.setText("Unspent");
|
||||||
|
} else if(outputForm.getOutputTransactions() != null) {
|
||||||
|
updateSpent(outputForm.getOutputTransactions());
|
||||||
|
} else {
|
||||||
|
spent.setText("Unknown");
|
||||||
|
}
|
||||||
|
|
||||||
scriptPubKeyArea.clear();
|
scriptPubKeyArea.clear();
|
||||||
appendScript(scriptPubKeyArea, txOutput.getScript(), null, null);
|
appendScript(scriptPubKeyArea, txOutput.getScript(), null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateSpent(List<BlockTransaction> outputTransactions) {
|
||||||
|
int outputIndex = outputForm.getTransactionOutputIndex();
|
||||||
|
spent.setText("Unspent");
|
||||||
|
|
||||||
|
if(outputIndex >= 0 && outputIndex < outputTransactions.size()) {
|
||||||
|
BlockTransaction outputBlockTransaction = outputTransactions.get(outputIndex);
|
||||||
|
if(outputBlockTransaction != null) {
|
||||||
|
spent.setText("Spent");
|
||||||
|
|
||||||
|
if(outputBlockTransaction == ElectrumServer.UNFETCHABLE_BLOCK_TRANSACTION) {
|
||||||
|
spent.setText("Spent (Spending transaction history too large to fetch)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i = 0; i < outputBlockTransaction.getTransaction().getInputs().size(); i++) {
|
||||||
|
TransactionInput input = outputBlockTransaction.getTransaction().getInputs().get(i);
|
||||||
|
if(input.getOutpoint().getHash().equals(outputForm.getTransaction().getTxId()) && input.getOutpoint().getIndex() == outputIndex) {
|
||||||
|
spentField.setVisible(false);
|
||||||
|
spentByField.setVisible(true);
|
||||||
|
|
||||||
|
final Integer inputIndex = i;
|
||||||
|
spentBy.setText(outputBlockTransaction.getHash().toString() + ":" + inputIndex);
|
||||||
|
spentBy.setOnAction(event -> {
|
||||||
|
EventManager.get().post(new ViewTransactionEvent(outputBlockTransaction, TransactionView.INPUT, inputIndex));
|
||||||
|
});
|
||||||
|
spentBy.setContextMenu(new TransactionReferenceContextMenu(spentBy.getText()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setModel(OutputForm form) {
|
public void setModel(OutputForm form) {
|
||||||
this.outputForm = form;
|
this.outputForm = form;
|
||||||
initializeView();
|
initializeView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void blockTransactionOutputsFetched(BlockTransactionOutputsFetchedEvent event) {
|
||||||
|
if(event.getTxId().equals(outputForm.getTransaction().getTxId()) && outputForm.getPsbt() == null) {
|
||||||
|
updateSpent(event.getOutputTransactions());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import javafx.scene.Node;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class OutputForm extends TransactionForm {
|
public class OutputForm extends TransactionForm {
|
||||||
private TransactionOutput transactionOutput;
|
private final TransactionOutput transactionOutput;
|
||||||
private PSBTOutput psbtOutput;
|
private PSBTOutput psbtOutput;
|
||||||
|
|
||||||
public OutputForm(PSBT psbt, PSBTOutput psbtOutput) {
|
public OutputForm(PSBT psbt, PSBTOutput psbtOutput) {
|
||||||
|
@ -38,6 +38,10 @@ public class OutputForm extends TransactionForm {
|
||||||
return psbtOutput;
|
return psbtOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getTransactionOutputIndex() {
|
||||||
|
return getTransaction().getOutputs().indexOf(transactionOutput);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Node getContents() throws IOException {
|
public Node getContents() throws IOException {
|
||||||
FXMLLoader loader = new FXMLLoader(getClass().getResource("output.fxml"));
|
FXMLLoader loader = new FXMLLoader(getClass().getResource("output.fxml"));
|
||||||
|
|
|
@ -62,7 +62,8 @@ public class TransactionController implements Initializable {
|
||||||
initializeTxTree();
|
initializeTxTree();
|
||||||
transactionMasterDetail.setShowDetailNode(AppController.showTxHexProperty);
|
transactionMasterDetail.setShowDetailNode(AppController.showTxHexProperty);
|
||||||
refreshTxHex();
|
refreshTxHex();
|
||||||
fetchBlockTransactions();
|
fetchThisAndInputBlockTransactions();
|
||||||
|
fetchOutputBlockTransactions();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeTxTree() {
|
private void initializeTxTree() {
|
||||||
|
@ -134,7 +135,7 @@ public class TransactionController implements Initializable {
|
||||||
selectedOutputIndex = outputForm.getTransactionOutput().getIndex();
|
selectedOutputIndex = outputForm.getTransactionOutput().getIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshTxHex();
|
Platform.runLater(this::refreshTxHex);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new IllegalStateException("Can't find pane", e);
|
throw new IllegalStateException("Can't find pane", e);
|
||||||
|
@ -167,6 +168,7 @@ public class TransactionController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
void refreshTxHex() {
|
void refreshTxHex() {
|
||||||
|
//TODO: Handle large transactions like efd513fffbbc2977c2d3933dfaab590b5cab5841ee791b3116e531ac9f8034ed better by not replacing text
|
||||||
txhex.clear();
|
txhex.clear();
|
||||||
|
|
||||||
String hex = "";
|
String hex = "";
|
||||||
|
@ -242,15 +244,17 @@ public class TransactionController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchBlockTransactions() {
|
private void fetchThisAndInputBlockTransactions() {
|
||||||
if (AppController.isOnline()) {
|
if (AppController.isOnline()) {
|
||||||
Set<Sha256Hash> references = new HashSet<>();
|
Set<Sha256Hash> references = new HashSet<>();
|
||||||
if (psbt == null) {
|
if (psbt == null) {
|
||||||
references.add(transaction.getTxId());
|
references.add(transaction.getTxId());
|
||||||
}
|
}
|
||||||
for (TransactionInput input : transaction.getInputs()) {
|
for (TransactionInput input : transaction.getInputs()) {
|
||||||
|
if(!input.isCoinBase()) {
|
||||||
references.add(input.getOutpoint().getHash());
|
references.add(input.getOutpoint().getHash());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(references);
|
ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(references);
|
||||||
transactionReferenceService.setOnSucceeded(successEvent -> {
|
transactionReferenceService.setOnSucceeded(successEvent -> {
|
||||||
|
@ -285,6 +289,22 @@ public class TransactionController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void fetchOutputBlockTransactions() {
|
||||||
|
if (AppController.isOnline() && psbt == null) {
|
||||||
|
ElectrumServer.TransactionOutputsReferenceService transactionOutputsReferenceService = new ElectrumServer.TransactionOutputsReferenceService(transaction);
|
||||||
|
transactionOutputsReferenceService.setOnSucceeded(successEvent -> {
|
||||||
|
List<BlockTransaction> outputTransactions = transactionOutputsReferenceService.getValue();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
EventManager.get().post(new BlockTransactionOutputsFetchedEvent(transaction.getTxId(), outputTransactions));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
transactionOutputsReferenceService.setOnFailed(failedEvent -> {
|
||||||
|
failedEvent.getSource().getException().printStackTrace();
|
||||||
|
});
|
||||||
|
transactionOutputsReferenceService.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String getIndexedStyleClass(int iterableIndex, int selectedIndex, String styleClass) {
|
private String getIndexedStyleClass(int iterableIndex, int selectedIndex, String styleClass) {
|
||||||
if (selectedIndex == -1 || selectedIndex == iterableIndex) {
|
if (selectedIndex == -1 || selectedIndex == iterableIndex) {
|
||||||
return styleClass;
|
return styleClass;
|
||||||
|
@ -346,4 +366,20 @@ public class TransactionController implements Initializable {
|
||||||
setBlockTransaction(childItem, event);
|
setBlockTransaction(childItem, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void blockTransactionOutputsFetched(BlockTransactionOutputsFetchedEvent event) {
|
||||||
|
if (event.getTxId().equals(transaction.getTxId())) {
|
||||||
|
setBlockTransactionOutputs(txtree.getRoot(), event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setBlockTransactionOutputs(TreeItem<TransactionForm> treeItem, BlockTransactionOutputsFetchedEvent event) {
|
||||||
|
TransactionForm form = treeItem.getValue();
|
||||||
|
form.setOutputTransactions(event.getOutputTransactions());
|
||||||
|
|
||||||
|
for (TreeItem<TransactionForm> childItem : treeItem.getChildren()) {
|
||||||
|
setBlockTransactionOutputs(childItem, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public abstract class TransactionForm {
|
public abstract class TransactionForm {
|
||||||
|
@ -14,6 +15,7 @@ public abstract class TransactionForm {
|
||||||
private PSBT psbt;
|
private PSBT psbt;
|
||||||
private BlockTransaction blockTransaction;
|
private BlockTransaction blockTransaction;
|
||||||
private Map<Sha256Hash, BlockTransaction> inputTransactions;
|
private Map<Sha256Hash, BlockTransaction> inputTransactions;
|
||||||
|
private List<BlockTransaction> outputTransactions;
|
||||||
|
|
||||||
public TransactionForm(PSBT psbt) {
|
public TransactionForm(PSBT psbt) {
|
||||||
this.transaction = psbt.getTransaction();
|
this.transaction = psbt.getTransaction();
|
||||||
|
@ -53,6 +55,14 @@ public abstract class TransactionForm {
|
||||||
this.inputTransactions = inputTransactions;
|
this.inputTransactions = inputTransactions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<BlockTransaction> getOutputTransactions() {
|
||||||
|
return outputTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOutputTransactions(List<BlockTransaction> outputTransactions) {
|
||||||
|
this.outputTransactions = outputTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEditable() {
|
public boolean isEditable() {
|
||||||
return blockTransaction == null;
|
return blockTransaction == null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,11 @@ import com.sparrowwallet.sparrow.BaseController;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.scene.chart.PieChart;
|
import javafx.scene.chart.PieChart;
|
||||||
|
import javafx.scene.control.ContextMenu;
|
||||||
|
import javafx.scene.control.MenuItem;
|
||||||
import javafx.scene.control.Tooltip;
|
import javafx.scene.control.Tooltip;
|
||||||
|
import javafx.scene.input.Clipboard;
|
||||||
|
import javafx.scene.input.ClipboardContent;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -33,9 +37,17 @@ public abstract class TransactionFormController extends BaseController {
|
||||||
outputsPieData.add(new PieChart.Data(name, output.getValue()));
|
outputsPieData.add(new PieChart.Data(name, output.getValue()));
|
||||||
}
|
}
|
||||||
|
|
||||||
pie.setData(outputsPieData);
|
addPieData(pie, outputsPieData);
|
||||||
|
}
|
||||||
|
|
||||||
final double totalSum = totalAmt;
|
protected void addCoinbasePieData(PieChart pie, long value) {
|
||||||
|
ObservableList<PieChart.Data> outputsPieData = FXCollections.observableList(List.of(new PieChart.Data("Coinbase", value)));
|
||||||
|
addPieData(pie, outputsPieData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addPieData(PieChart pie, ObservableList<PieChart.Data> outputsPieData) {
|
||||||
|
pie.setData(outputsPieData);
|
||||||
|
final double totalSum = outputsPieData.stream().map(PieChart.Data::getPieValue).mapToDouble(Double::doubleValue).sum();
|
||||||
pie.getData().forEach(data -> {
|
pie.getData().forEach(data -> {
|
||||||
Tooltip tooltip = new Tooltip();
|
Tooltip tooltip = new Tooltip();
|
||||||
double percent = 100.0 * (data.getPieValue() / totalSum);
|
double percent = 100.0 * (data.getPieValue() / totalSum);
|
||||||
|
@ -44,4 +56,17 @@ public abstract class TransactionFormController extends BaseController {
|
||||||
data.pieValueProperty().addListener((observable, oldValue, newValue) -> tooltip.setText(newValue + "%"));
|
data.pieValueProperty().addListener((observable, oldValue, newValue) -> tooltip.setText(newValue + "%"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class TransactionReferenceContextMenu extends ContextMenu {
|
||||||
|
public TransactionReferenceContextMenu(String reference) {
|
||||||
|
MenuItem referenceItem = new MenuItem("Copy Reference");
|
||||||
|
referenceItem.setOnAction(AE -> {
|
||||||
|
hide();
|
||||||
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
content.putString(reference);
|
||||||
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
|
});
|
||||||
|
getItems().add(referenceItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
<Menu mnemonicParsing="false" text="Open Transaction">
|
<Menu mnemonicParsing="false" text="Open Transaction">
|
||||||
<items>
|
<items>
|
||||||
<MenuItem text="File..." onAction="#openTransactionFromFile"/>
|
<MenuItem text="File..." onAction="#openTransactionFromFile"/>
|
||||||
|
<MenuItem fx:id="openTransactionIdItem" text="From ID..." onAction="#openTransactionFromId"/>
|
||||||
<MenuItem text="From Text..." onAction="#openTransactionFromText"/>
|
<MenuItem text="From Text..." onAction="#openTransactionFromText"/>
|
||||||
<MenuItem text="Examples" onAction="#openExamples"/>
|
<MenuItem text="Examples" onAction="#openExamples"/>
|
||||||
</items>
|
</items>
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<Fieldset fx:id="inputFieldset" inputGrow="SOMETIMES" text="Input" wrapWidth="680">
|
<Fieldset fx:id="inputFieldset" inputGrow="SOMETIMES" text="Input" wrapWidth="680">
|
||||||
<Field text="Outpoint:" styleClass="label-button">
|
<Field text="Outpoint:" styleClass="label-button">
|
||||||
<IdLabel fx:id="outpoint" />
|
<IdLabel fx:id="outpoint" />
|
||||||
<Hyperlink fx:id="linkedOutpoint" visible="false" />
|
<Hyperlink fx:id="linkedOutpoint" styleClass="id" visible="false" />
|
||||||
<Button fx:id="outpointSelect" maxWidth="25" minWidth="-Infinity" prefWidth="30" text="Ed">
|
<Button fx:id="outpointSelect" maxWidth="25" minWidth="-Infinity" prefWidth="30" text="Ed">
|
||||||
<graphic>
|
<graphic>
|
||||||
<Glyph fontFamily="FontAwesome" icon="EDIT" prefWidth="15" />
|
<Glyph fontFamily="FontAwesome" icon="EDIT" prefWidth="15" />
|
||||||
|
|
|
@ -21,3 +21,11 @@
|
||||||
.witness-data { -fx-fill: #e5e5e6 }
|
.witness-data { -fx-fill: #e5e5e6 }
|
||||||
|
|
||||||
.locktime { -fx-fill: #e5e5e6 }
|
.locktime { -fx-fill: #e5e5e6 }
|
||||||
|
|
||||||
|
#spentField .input-container {
|
||||||
|
-fx-alignment: center-left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spentByField .input-container {
|
||||||
|
-fx-alignment: center-left;
|
||||||
|
}
|
|
@ -34,6 +34,12 @@
|
||||||
<CopyableLabel fx:id="to" text="to" />
|
<CopyableLabel fx:id="to" text="to" />
|
||||||
<AddressLabel fx:id="address" />
|
<AddressLabel fx:id="address" />
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field fx:id="spentField" text="Spent?">
|
||||||
|
<Label fx:id="spent" />
|
||||||
|
</Field>
|
||||||
|
<Field fx:id="spentByField" text="Spent By:">
|
||||||
|
<Hyperlink fx:id="spentBy" styleClass="id" />
|
||||||
|
</Field>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue