From 86eb8b82940794ffed44a05595b58997ad89f9e6 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 11 Jun 2020 17:00:16 +0200 Subject: [PATCH] transaction viewer block transaction fetch and display --- drongo | 2 +- .../sparrowwallet/sparrow/AppController.java | 42 ++++--- .../sparrow/control/AddressTreeTable.java | 5 +- .../event/BlockTransactionFetchedEvent.java | 30 +++++ .../event/TransactionInputSelectedEvent.java | 13 --- .../event/TransactionOutputSelectedEvent.java | 13 --- .../sparrow/event/ViewTransactionEvent.java | 21 +++- .../sparrow/io/ElectrumServer.java | 87 ++++++++++++++- .../transaction/HeadersController.java | 105 ++++++++++++++++-- .../sparrow/transaction/HeadersForm.java | 6 + .../sparrow/transaction/InputController.java | 79 ++++++++++--- .../sparrow/transaction/InputForm.java | 6 + .../sparrow/transaction/InputsController.java | 47 +++++++- .../sparrow/transaction/InputsForm.java | 6 + .../sparrow/transaction/OutputForm.java | 6 + .../sparrow/transaction/OutputsForm.java | 6 + .../transaction/TransactionController.java | 97 +++++++++++++--- .../sparrow/transaction/TransactionForm.java | 19 +++- .../sparrow/transaction/TransactionView.java | 5 + .../sparrow/transaction/headers.fxml | 8 +- .../sparrow/transaction/input.fxml | 1 + 21 files changed, 508 insertions(+), 96 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/BlockTransactionFetchedEvent.java delete mode 100644 src/main/java/com/sparrowwallet/sparrow/event/TransactionInputSelectedEvent.java delete mode 100644 src/main/java/com/sparrowwallet/sparrow/event/TransactionOutputSelectedEvent.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/transaction/TransactionView.java diff --git a/drongo b/drongo index 728b6ce5..75701c72 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 728b6ce5ef21233b5e1687d3fb4277b79eb2e4e1 +Subproject commit 75701c725d00a11f7a3931190d029378a04be8f6 diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index f78f4cfd..418e3b0f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -18,12 +18,13 @@ import com.sparrowwallet.sparrow.event.*; import com.sparrowwallet.sparrow.io.*; import com.sparrowwallet.sparrow.preferences.PreferencesDialog; import com.sparrowwallet.sparrow.transaction.TransactionController; -import com.sparrowwallet.sparrow.wallet.HashIndexEntry; +import com.sparrowwallet.sparrow.transaction.TransactionView; import com.sparrowwallet.sparrow.wallet.WalletController; import com.sparrowwallet.sparrow.wallet.WalletForm; import de.codecentric.centerdevice.MenuToolkit; import javafx.animation.*; -import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.concurrent.Worker; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -74,6 +75,8 @@ public class AppController implements Initializable { //Determines if a change in serverToggle changes the offline/online mode private boolean changeMode = true; + private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false); + private Timeline statusTimeline; private ElectrumServer.ConnectionService connectionService; @@ -155,6 +158,8 @@ public class AppController implements Initializable { } }); + onlineProperty.bindBidirectional(serverToggle.selectedProperty()); + connectionService = createConnectionService(); Config config = Config.get(); if(config.getMode() == Mode.ONLINE && config.getElectrumServer() != null && !config.getElectrumServer().isEmpty()) { @@ -171,7 +176,7 @@ public class AppController implements Initializable { connectionService.setOnSucceeded(successEvent -> { changeMode = false; - serverToggle.setSelected(true); + onlineProperty.setValue(true); changeMode = true; if(connectionService.getValue() != null) { @@ -180,7 +185,7 @@ public class AppController implements Initializable { }); connectionService.setOnFailed(failEvent -> { changeMode = false; - serverToggle.setSelected(false); + onlineProperty.setValue(false); changeMode = true; EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException())); @@ -289,6 +294,10 @@ public class AppController implements Initializable { } } + public static boolean isOnline() { + return onlineProperty.get(); + } + public static Integer getCurrentBlockHeight() { return currentBlockHeight; } @@ -539,18 +548,18 @@ public class AppController implements Initializable { } private Tab addTransactionTab(String name, Transaction transaction) { - return addTransactionTab(name, transaction, null, null); + return addTransactionTab(name, transaction, null, null, null, null); } private Tab addTransactionTab(String name, PSBT psbt) { - return addTransactionTab(name, psbt.getTransaction(), psbt, null); + return addTransactionTab(name, psbt.getTransaction(), psbt, null, null, null); } - private Tab addTransactionTab(BlockTransaction blockTransaction) { - return addTransactionTab(null, blockTransaction.getTransaction(), null, blockTransaction); + private Tab addTransactionTab(BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) { + return addTransactionTab(null, blockTransaction.getTransaction(), null, blockTransaction, initialView, initialIndex); } - private Tab addTransactionTab(String name, Transaction transaction, PSBT psbt, BlockTransaction blockTransaction) { + private Tab addTransactionTab(String name, Transaction transaction, PSBT psbt, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) { for(Tab tab : tabs.getTabs()) { TabData tabData = (TabData)tab.getUserData(); if(tabData instanceof TransactionTabData) { @@ -581,6 +590,12 @@ public class AppController implements Initializable { controller.setBlockTransaction(blockTransaction); controller.setTransaction(transaction); + if(initialView != null) { + controller.setTreeSelection(initialView, initialIndex); + } else { + controller.setTreeSelection(TransactionView.HEADERS, null); + } + tabs.getTabs().add(tab); return tab; } catch(IOException e) { @@ -715,14 +730,7 @@ public class AppController implements Initializable { @Subscribe public void viewTransaction(ViewTransactionEvent event) { - Tab tab = addTransactionTab(event.getBlockTransaction()); + Tab tab = addTransactionTab(event.getBlockTransaction(), event.getInitialView(), event.getInitialIndex()); tabs.getSelectionModel().select(tab); - Platform.runLater(() -> { - if(event.getHashIndexEntry().getType().equals(HashIndexEntry.Type.INPUT)) { - EventManager.get().post(new TransactionInputSelectedEvent(event.getHashIndexEntry().getHashIndex().getIndex())); - } else { - EventManager.get().post(new TransactionOutputSelectedEvent(event.getHashIndexEntry().getHashIndex().getIndex())); - } - }); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java index aac1930c..86b75f2f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java @@ -71,7 +71,10 @@ public class AddressTreeTable extends TreeTableView { setEditable(true); setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); - scrollTo(rootEntry.getNode().getHighestUsedIndex()); + Integer highestUsedIndex = rootEntry.getNode().getHighestUsedIndex(); + if(highestUsedIndex != null) { + scrollTo(highestUsedIndex); + } setOnMouseClicked(mouseEvent -> { if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){ diff --git a/src/main/java/com/sparrowwallet/sparrow/event/BlockTransactionFetchedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/BlockTransactionFetchedEvent.java new file mode 100644 index 00000000..56c2d506 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/BlockTransactionFetchedEvent.java @@ -0,0 +1,30 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.protocol.Sha256Hash; +import com.sparrowwallet.drongo.wallet.BlockTransaction; + +import java.util.Map; + +public class BlockTransactionFetchedEvent { + private final Sha256Hash txId; + private final BlockTransaction blockTransaction; + private final Map inputTransactions; + + public BlockTransactionFetchedEvent(Sha256Hash txId, BlockTransaction blockTransaction, Map inputTransactions) { + this.txId = txId; + this.blockTransaction = blockTransaction; + this.inputTransactions = inputTransactions; + } + + public Sha256Hash getTxId() { + return txId; + } + + public BlockTransaction getBlockTransaction() { + return blockTransaction; + } + + public Map getInputTransactions() { + return inputTransactions; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/TransactionInputSelectedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/TransactionInputSelectedEvent.java deleted file mode 100644 index 416951fe..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/event/TransactionInputSelectedEvent.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.sparrowwallet.sparrow.event; - -public class TransactionInputSelectedEvent { - private final long index; - - public TransactionInputSelectedEvent(long index) { - this.index = index; - } - - public long getIndex() { - return index; - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/TransactionOutputSelectedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/TransactionOutputSelectedEvent.java deleted file mode 100644 index 9406fcd5..00000000 --- a/src/main/java/com/sparrowwallet/sparrow/event/TransactionOutputSelectedEvent.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.sparrowwallet.sparrow.event; - -public class TransactionOutputSelectedEvent { - private final long index; - - public TransactionOutputSelectedEvent(long index) { - this.index = index; - } - - public long getIndex() { - return index; - } -} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ViewTransactionEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ViewTransactionEvent.java index c179ec7a..91bdfafd 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/ViewTransactionEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/ViewTransactionEvent.java @@ -1,26 +1,37 @@ package com.sparrowwallet.sparrow.event; import com.sparrowwallet.drongo.wallet.BlockTransaction; +import com.sparrowwallet.sparrow.transaction.TransactionView; import com.sparrowwallet.sparrow.wallet.HashIndexEntry; public class ViewTransactionEvent { public final BlockTransaction blockTransaction; - public final HashIndexEntry hashIndexEntry; + public final TransactionView initialView; + public final Integer initialIndex; public ViewTransactionEvent(BlockTransaction blockTransaction) { - this(blockTransaction, null); + this(blockTransaction, null, null); } public ViewTransactionEvent(BlockTransaction blockTransaction, HashIndexEntry hashIndexEntry) { + this(blockTransaction, hashIndexEntry.getType().equals(HashIndexEntry.Type.INPUT) ? TransactionView.INPUT : TransactionView.OUTPUT, (int)hashIndexEntry.getHashIndex().getIndex()); + } + + public ViewTransactionEvent(BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) { this.blockTransaction = blockTransaction; - this.hashIndexEntry = hashIndexEntry; + this.initialView = initialView; + this.initialIndex = initialIndex; } public BlockTransaction getBlockTransaction() { return blockTransaction; } - public HashIndexEntry getHashIndexEntry() { - return hashIndexEntry; + public TransactionView getInitialView() { + return initialView; + } + + public Integer getInitialIndex() { + return initialIndex; } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java b/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java index ef843ea6..a3053bc8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ElectrumServer.java @@ -1,7 +1,9 @@ package com.sparrowwallet.sparrow.io; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.github.arteam.simplejsonrpc.client.*; import com.github.arteam.simplejsonrpc.client.builder.BatchRequestBuilder; +import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException; import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcMethod; import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam; import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService; @@ -11,6 +13,7 @@ import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.wallet.*; +import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.ConnectionEvent; import com.sparrowwallet.sparrow.event.NewBlockEvent; @@ -365,6 +368,32 @@ public class ElectrumServer { } } + @SuppressWarnings("unchecked") + public Map getReferencedTransactions(Set references) throws ServerException { + JsonRpcClient client = new JsonRpcClient(getTransport()); + BatchRequestBuilder batchRequest = client.createBatchRequest().keysType(String.class).returnType(VerboseTransaction.class); + for(Sha256Hash reference : references) { + batchRequest.add(reference.toString(), "blockchain.transaction.get", reference.toString(), true); + } + + Map result; + try { + result = batchRequest.execute(); + } catch (JsonRpcBatchException e) { + System.out.println("Some errors retrieving transactions: " + e.getErrors()); + result = (Map)e.getSuccesses(); + } + + Map transactionMap = new HashMap<>(); + for(String txid : result.keySet()) { + Sha256Hash hash = Sha256Hash.wrap(txid); + BlockTransaction blockTransaction = result.get(txid).getBlockTransaction(); + transactionMap.put(hash, blockTransaction); + } + + return transactionMap; + } + private String getScriptHash(Wallet wallet, WalletNode node) { byte[] hash = Sha256Hash.hash(wallet.getOutputScript(node).getProgram()); byte[] reversed = Utils.reverseBytes(hash); @@ -397,6 +426,36 @@ public class ElectrumServer { } } + @JsonIgnoreProperties(ignoreUnknown=true) + private static class VerboseTransaction { + public String blockhash; + public long blocktime; + public int confirmations; + public String hash; + public String hex; + public int locktime; + public long size; + public String txid; + public int version; + + public int getHeight() { + Integer currentHeight = AppController.getCurrentBlockHeight(); + if(currentHeight != null) { + return currentHeight - confirmations + 1; + } + + return -1; + } + + public Date getDate() { + return new Date(blocktime * 1000); + } + + public BlockTransaction getBlockTransaction() { + return new BlockTransaction(Sha256Hash.wrap(txid), getHeight(), getDate(), 0L, new Transaction(Utils.hexToBytes(hex)), Sha256Hash.wrap(blockhash)); + } + } + @JsonRpcService public static class SubscriptionService { @JsonRpcMethod("blockchain.headers.subscribe") @@ -571,7 +630,7 @@ public class ElectrumServer { public static class ProxyTcpOverTlsTransport extends TcpOverTlsTransport { public static final int DEFAULT_PROXY_PORT = 1080; - private HostAndPort proxy; + private final HostAndPort proxy; public ProxyTcpOverTlsTransport(HostAndPort server, HostAndPort proxy) throws KeyManagementException, NoSuchAlgorithmException { super(server); @@ -715,6 +774,32 @@ public class ElectrumServer { } } + public static class TransactionReferenceService extends Service> { + private final Set references; + + public TransactionReferenceService(Transaction transaction) { + references = new HashSet<>(); + references.add(transaction.getTxId()); + for(TransactionInput input : transaction.getInputs()) { + references.add(input.getOutpoint().getHash()); + } + } + + public TransactionReferenceService(Set references) { + this.references = references; + } + + @Override + protected Task> createTask() { + return new Task<>() { + protected Map call() throws ServerException { + ElectrumServer electrumServer = new ElectrumServer(); + return electrumServer.getReferencedTransactions(references); + } + }; + } + } + public enum Protocol { TCP { @Override diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 76a34f9f..8b1617b1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -1,15 +1,22 @@ package com.sparrowwallet.sparrow.transaction; +import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Transaction; +import com.sparrowwallet.drongo.protocol.TransactionInput; +import com.sparrowwallet.drongo.protocol.TransactionOutput; +import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.CoinLabel; import com.sparrowwallet.sparrow.control.IdLabel; import com.sparrowwallet.sparrow.control.CopyableLabel; +import com.sparrowwallet.sparrow.event.BlockTransactionFetchedEvent; import com.sparrowwallet.sparrow.event.TransactionChangedEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.*; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; import tornadofx.control.DateTimePicker; import tornadofx.control.Field; import tornadofx.control.Fieldset; @@ -17,11 +24,14 @@ import com.google.common.eventbus.Subscribe; import tornadofx.control.Form; import java.net.URL; +import java.text.SimpleDateFormat; import java.time.*; +import java.util.Map; import java.util.ResourceBundle; public class HeadersController extends TransactionFormController implements Initializable { public static final String LOCKTIME_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + public static final String BLOCK_TIMESTAMP_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss ZZZ"; private HeadersForm headersForm; @@ -88,6 +98,12 @@ public class HeadersController extends TransactionFormController implements Init @FXML private CopyableLabel blockHeight; + @FXML + private CopyableLabel blockTimestamp; + + @FXML + private IdLabel blockHash; + @Override public void initialize(URL location, ResourceBundle resources) { EventManager.get().register(this); @@ -194,26 +210,82 @@ public class HeadersController extends TransactionFormController implements Init Long feeAmt = null; if(headersForm.getPsbt() != null) { feeAmt = headersForm.getPsbt().getFee(); + } else if(headersForm.getInputTransactions() != null) { + feeAmt = calculateFee(headersForm.getInputTransactions()); } if(feeAmt != null) { - fee.setValue(feeAmt); - double feeRateAmt = feeAmt.doubleValue() / tx.getVirtualSize(); - feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vByte"); + updateFee(feeAmt); } blockchainForm.managedProperty().bind(blockchainForm.visibleProperty()); - blockchainForm.setVisible(headersForm.getBlockTransaction() != null); if(headersForm.getBlockTransaction() != null) { - Integer currentHeight = AppController.getCurrentBlockHeight(); - if(currentHeight == null) { - blockStatus.setText("Unknown"); - } else { - int confirmations = currentHeight - headersForm.getBlockTransaction().getHeight() + 1; - blockStatus.setText(confirmations + " Confirmations"); + updateBlockchainForm(headersForm.getBlockTransaction()); + } else { + blockchainForm.setVisible(false); + } + } + + private long calculateFee(Map inputTransactions) { + long feeAmt = 0L; + for(TransactionInput input : headersForm.getTransaction().getInputs()) { + BlockTransaction inputTx = inputTransactions.get(input.getOutpoint().getHash()); + if(inputTx == null) { + throw new IllegalStateException("Cannot find transaction for hash " + input.getOutpoint().getHash()); } - blockHeight.setText(Integer.toString(headersForm.getBlockTransaction().getHeight())); + feeAmt += inputTx.getTransaction().getOutputs().get((int)input.getOutpoint().getIndex()).getValue(); + } + + for(TransactionOutput output : headersForm.getTransaction().getOutputs()) { + feeAmt -= output.getValue(); + } + + return feeAmt; + } + + private void updateFee(Long feeAmt) { + fee.setValue(feeAmt); + double feeRateAmt = feeAmt.doubleValue() / headersForm.getTransaction().getVirtualSize(); + feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vByte"); + } + + private void updateBlockchainForm(BlockTransaction blockTransaction) { + blockchainForm.setVisible(true); + + Integer currentHeight = AppController.getCurrentBlockHeight(); + if(currentHeight == null) { + blockStatus.setText("Unknown"); + } else { + int confirmations = currentHeight - blockTransaction.getHeight() + 1; + blockStatus.setText(confirmations + " Confirmations"); + } + + blockHeight.setText(Integer.toString(blockTransaction.getHeight())); + + SimpleDateFormat dateFormat = new SimpleDateFormat(BLOCK_TIMESTAMP_DATE_FORMAT); + blockTimestamp.setText(dateFormat.format(blockTransaction.getDate())); + + blockHash.managedProperty().bind(blockHash.visibleProperty()); + if(blockTransaction.getBlockHash() != null) { + blockHash.setVisible(true); + blockHash.setText(blockTransaction.getBlockHash().toString()); + blockHash.setContextMenu(new BlockHeightContextMenu(blockTransaction.getBlockHash())); + } else { + blockHash.setVisible(false); + } + } + + private static class BlockHeightContextMenu extends ContextMenu { + public BlockHeightContextMenu(Sha256Hash blockHash) { + MenuItem copyBlockHash = new MenuItem("Copy Block Hash"); + copyBlockHash.setOnAction(AE -> { + hide(); + ClipboardContent content = new ClipboardContent(); + content.putString(blockHash.toString()); + Clipboard.getSystemClipboard().setContent(content); + }); + getItems().add(copyBlockHash); } } @@ -233,4 +305,15 @@ public class HeadersController extends TransactionFormController implements Init locktimeDate.setDisable(!locktimeEnabled); } } + + @Subscribe + public void blockTransactionFetched(BlockTransactionFetchedEvent event) { + if(event.getTxId().equals(headersForm.getTransaction().getTxId())) { + if(event.getBlockTransaction() != null) { + updateBlockchainForm(event.getBlockTransaction()); + } + + updateFee(calculateFee(event.getInputTransactions())); + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersForm.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersForm.java index a01fa823..a2c02626 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersForm.java @@ -21,6 +21,7 @@ public class HeadersForm extends TransactionForm { super(transaction); } + @Override public Node getContents() throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("headers.fxml")); Node node = loader.load(); @@ -29,6 +30,11 @@ public class HeadersForm extends TransactionForm { return node; } + @Override + public TransactionView getView() { + return TransactionView.HEADERS; + } + public String toString() { return "Tx [" + getTransaction().calculateTxId(false).toString().substring(0, 6) + "]"; } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/InputController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/InputController.java index fd26fab6..daabb9e0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/InputController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/InputController.java @@ -1,13 +1,17 @@ package com.sparrowwallet.sparrow.transaction; +import com.google.common.eventbus.Subscribe; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.psbt.PSBTInput; +import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.*; +import com.sparrowwallet.sparrow.event.BlockTransactionFetchedEvent; import com.sparrowwallet.sparrow.event.TransactionChangedEvent; +import com.sparrowwallet.sparrow.event.ViewTransactionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.geometry.Pos; @@ -28,6 +32,7 @@ import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.util.Map; import java.util.ResourceBundle; public class InputController extends TransactionFormController implements Initializable { @@ -39,6 +44,9 @@ public class InputController extends TransactionFormController implements Initia @FXML private IdLabel outpoint; + @FXML + private Hyperlink linkedOutpoint; + @FXML private Button outpointSelect; @@ -128,6 +136,10 @@ public class InputController extends TransactionFormController implements Initia private void initializeInputFields(TransactionInput txInput, PSBTInput psbtInput) { inputFieldset.setText("Input #" + txInput.getIndex()); + + outpoint.managedProperty().bind(outpoint.visibleProperty()); + linkedOutpoint.managedProperty().bind(linkedOutpoint.visibleProperty()); + if(txInput.isCoinBase()) { outpoint.setText("Coinbase"); outpointSelect.setVisible(false); @@ -136,7 +148,11 @@ public class InputController extends TransactionFormController implements Initia totalAmt += output.getValue(); } spends.setValue(totalAmt); + } else if(inputForm.getInputTransactions() != null) { + updateOutpoint(inputForm.getInputTransactions()); } else { + outpoint.setVisible(true); + linkedOutpoint.setVisible(false); outpoint.setText(txInput.getOutpoint().getHash().toString() + ":" + txInput.getOutpoint().getIndex()); } @@ -149,26 +165,51 @@ public class InputController extends TransactionFormController implements Initia output = psbtInput.getWitnessUtxo(); } - if(output != null) { - spends.setValue(output.getValue()); - try { - Address[] addresses = output.getScript().getToAddresses(); - from.setVisible(true); - if(addresses.length == 1) { - address.setAddress(addresses[0]); - } else { - address.setText("multiple addresses"); - } - } catch(NonStandardScriptException e) { - //ignore - } - } + updateSpends(output); + } else if(inputForm.getInputTransactions() != null) { + updateSpends(inputForm.getInputTransactions()); } //TODO: Enable select outpoint when wallet present outpointSelect.setDisable(true); } + private void updateOutpoint(Map inputTransactions) { + outpoint.setVisible(false); + linkedOutpoint.setVisible(true); + + TransactionInput txInput = inputForm.getTransactionInput(); + linkedOutpoint.setText(txInput.getOutpoint().getHash().toString() + ":" + txInput.getOutpoint().getIndex()); + linkedOutpoint.setOnAction(event -> { + BlockTransaction linkedTransaction = inputTransactions.get(txInput.getOutpoint().getHash()); + EventManager.get().post(new ViewTransactionEvent(linkedTransaction, TransactionView.OUTPUT, (int)txInput.getOutpoint().getIndex())); + }); + } + + private void updateSpends(Map inputTransactions) { + TransactionInput txInput = inputForm.getTransactionInput(); + BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash()); + TransactionOutput output = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex()); + updateSpends(output); + } + + private void updateSpends(TransactionOutput output) { + if (output != null) { + spends.setValue(output.getValue()); + try { + Address[] addresses = output.getScript().getToAddresses(); + from.setVisible(true); + if (addresses.length == 1) { + address.setAddress(addresses[0]); + } else { + address.setText("multiple addresses"); + } + } catch (NonStandardScriptException e) { + //ignore + } + } + } + private void initializeScriptFields(TransactionInput txInput, PSBTInput psbtInput) { //TODO: Is this safe? Script redeemScript = txInput.getScriptSig().getFirstNestedScript(); @@ -410,4 +451,14 @@ public class InputController extends TransactionFormController implements Initia return chunkString; } + + @Subscribe + public void blockTransactionFetched(BlockTransactionFetchedEvent event) { + if(event.getTxId().equals(inputForm.getTransaction().getTxId())) { + updateOutpoint(event.getInputTransactions()); + if(inputForm.getPsbt() == null) { + updateSpends(event.getInputTransactions()); + } + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/InputForm.java b/src/main/java/com/sparrowwallet/sparrow/transaction/InputForm.java index 9138642c..def1c4d6 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/InputForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/InputForm.java @@ -38,6 +38,7 @@ public class InputForm extends TransactionForm { return psbtInput; } + @Override public Node getContents() throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("input.fxml")); Node node = loader.load(); @@ -46,6 +47,11 @@ public class InputForm extends TransactionForm { return node; } + @Override + public TransactionView getView() { + return TransactionView.INPUT; + } + public String toString() { return "Input #" + transactionInput.getIndex(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/InputsController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/InputsController.java index ae3c0a5b..f561171b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/InputsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/InputsController.java @@ -1,9 +1,13 @@ package com.sparrowwallet.sparrow.transaction; +import com.google.common.eventbus.Subscribe; import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.psbt.PSBTInput; +import com.sparrowwallet.drongo.wallet.BlockTransaction; +import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.CoinLabel; import com.sparrowwallet.sparrow.control.CopyableLabel; +import com.sparrowwallet.sparrow.event.BlockTransactionFetchedEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.chart.PieChart; @@ -11,6 +15,7 @@ import javafx.scene.chart.PieChart; import java.net.URL; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.ResourceBundle; public class InputsController extends TransactionFormController implements Initializable { @@ -30,7 +35,7 @@ public class InputsController extends TransactionFormController implements Initi @Override public void initialize(URL location, ResourceBundle resources) { - + EventManager.get().register(this); } public void setModel(InputsForm form) { @@ -92,6 +97,46 @@ public class InputsController extends TransactionFormController implements Initi } addPieData(inputsPie, outputs); + } else if(inputsForm.getInputTransactions() != null) { + updateBlockTransactionInputs(inputsForm.getInputTransactions()); + } + } + + private void updateBlockTransactionInputs(Map inputTransactions) { + List outputs = new ArrayList<>(); + + int foundSigs = 0; + for(TransactionInput input : inputsForm.getTransaction().getInputs()) { + if(input.hasWitness()) { + foundSigs += input.getWitness().getSignatures().size(); + } else { + foundSigs += input.getScriptSig().getSignatures().size(); + } + + BlockTransaction inputTx = inputTransactions.get(input.getOutpoint().getHash()); + if(inputTx == null) { + throw new IllegalStateException("Cannot find transaction for hash " + input.getOutpoint().getHash()); + } + + TransactionOutput output = inputTx.getTransaction().getOutputs().get((int)input.getOutpoint().getIndex()); + outputs.add(output); + } + + long totalAmt = 0; + for(TransactionOutput output : outputs) { + totalAmt += output.getValue(); + } + total.setValue(totalAmt); + + //TODO: Find signing script and get required num sigs + signatures.setText(foundSigs + "/" + foundSigs); + addPieData(inputsPie, outputs); + } + + @Subscribe + public void blockTransactionFetched(BlockTransactionFetchedEvent event) { + if(event.getTxId().equals(inputsForm.getTransaction().getTxId()) && inputsForm.getPsbt() != null) { + updateBlockTransactionInputs(event.getInputTransactions()); } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/InputsForm.java b/src/main/java/com/sparrowwallet/sparrow/transaction/InputsForm.java index cfb31366..3590c98d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/InputsForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/InputsForm.java @@ -21,6 +21,7 @@ public class InputsForm extends TransactionForm { super(transaction); } + @Override public Node getContents() throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("inputs.fxml")); Node node = loader.load(); @@ -29,6 +30,11 @@ public class InputsForm extends TransactionForm { return node; } + @Override + public TransactionView getView() { + return TransactionView.INPUTS; + } + public String toString() { return "Inputs"; } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputForm.java b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputForm.java index c054dbf0..50bbf49d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputForm.java @@ -38,6 +38,7 @@ public class OutputForm extends TransactionForm { return psbtOutput; } + @Override public Node getContents() throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("output.fxml")); Node node = loader.load(); @@ -46,6 +47,11 @@ public class OutputForm extends TransactionForm { return node; } + @Override + public TransactionView getView() { + return TransactionView.OUTPUT; + } + public String toString() { return "Output #" + transactionOutput.getIndex(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputsForm.java b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputsForm.java index 6d0ba53f..83ca8960 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/OutputsForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/OutputsForm.java @@ -21,6 +21,7 @@ public class OutputsForm extends TransactionForm { super(transaction); } + @Override public Node getContents() throws IOException { FXMLLoader loader = new FXMLLoader(getClass().getResource("outputs.fxml")); Node node = loader.load(); @@ -29,6 +30,11 @@ public class OutputsForm extends TransactionForm { return node; } + @Override + public TransactionView getView() { + return TransactionView.OUTPUTS; + } + public String toString() { return "Outputs"; } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java index 05323ac6..c0c50be0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionController.java @@ -10,6 +10,8 @@ import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.sparrow.AppController; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.*; +import com.sparrowwallet.sparrow.io.ElectrumServer; +import javafx.application.Platform; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.Node; @@ -25,9 +27,7 @@ import org.fxmisc.richtext.CodeArea; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URL; -import java.util.List; -import java.util.Optional; -import java.util.ResourceBundle; +import java.util.*; public class TransactionController implements Initializable { @@ -49,6 +49,7 @@ public class TransactionController implements Initializable { private Transaction transaction; private PSBT psbt; private BlockTransaction blockTransaction; + private int selectedInputIndex = -1; private int selectedOutputIndex = -1; @@ -61,6 +62,7 @@ public class TransactionController implements Initializable { initializeTxTree(); transactionMasterDetail.setShowDetailNode(AppController.showTxHexProperty); refreshTxHex(); + fetchBlockTransactions(); } private void initializeTxTree() { @@ -142,6 +144,28 @@ public class TransactionController implements Initializable { txtree.getSelectionModel().select(txtree.getRoot()); } + public void setTreeSelection(TransactionView view, Integer index) { + select(txtree.getRoot(), view, index); + } + + private void select(TreeItem treeItem, TransactionView view, Integer index) { + if(treeItem.getValue().getView().equals(view)) { + if(view.equals(TransactionView.INPUT) || view.equals(TransactionView.OUTPUT)) { + if(treeItem.getParent().getChildren().indexOf(treeItem) == index) { + txtree.getSelectionModel().select(treeItem); + return; + } + } else { + txtree.getSelectionModel().select(treeItem); + return; + } + } + + for(TreeItem childItem : treeItem.getChildren()) { + select(childItem, view, index); + } + } + void refreshTxHex() { txhex.clear(); @@ -218,6 +242,49 @@ public class TransactionController implements Initializable { } } + private void fetchBlockTransactions() { + if(AppController.isOnline()) { + Set references = new HashSet<>(); + if(psbt == null) { + references.add(transaction.getTxId()); + } + for(TransactionInput input : transaction.getInputs()) { + references.add(input.getOutpoint().getHash()); + } + + ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(references); + transactionReferenceService.setOnSucceeded(successEvent -> { + Map transactionMap = transactionReferenceService.getValue(); + BlockTransaction thisBlockTx = null; + Map inputTransactions = new HashMap<>(); + for(Sha256Hash txid : transactionMap.keySet()) { + BlockTransaction blockTx = transactionMap.get(txid); + if(txid.equals(transaction.getTxId())) { + thisBlockTx = blockTx; + } else { + inputTransactions.put(txid, blockTx); + references.remove(txid); + } + } + + references.remove(transaction.getTxId()); + if(!references.isEmpty()) { + System.out.println("Failed to retrieve all referenced input transactions, aborting transaction fetch"); + return; + } + + final BlockTransaction blockTx = thisBlockTx; + Platform.runLater(() -> { + EventManager.get().post(new BlockTransactionFetchedEvent(transaction.getTxId(), blockTx, inputTransactions)); + }); + }); + transactionReferenceService.setOnFailed(failedEvent -> { + failedEvent.getSource().getException().printStackTrace(); + }); + transactionReferenceService.start(); + } + } + private String getIndexedStyleClass(int iterableIndex, int selectedIndex, String styleClass) { if (selectedIndex == -1 || selectedIndex == iterableIndex) { return styleClass; @@ -264,23 +331,19 @@ public class TransactionController implements Initializable { } @Subscribe - public void inputSelected(TransactionInputSelectedEvent event) { - Optional> optionalInputs = txtree.getRoot().getChildren().stream().filter(item -> item.getValue() instanceof InputsForm).findFirst(); - selectItem(optionalInputs, (int)event.getIndex()); + public void blockTransactionFetched(BlockTransactionFetchedEvent event) { + if(event.getTxId().equals(transaction.getTxId())) { + setBlockTransaction(txtree.getRoot(), event); + } } - @Subscribe - public void outputSelected(TransactionOutputSelectedEvent event) { - Optional> optionalOutputs = txtree.getRoot().getChildren().stream().filter(item -> item.getValue() instanceof OutputsForm).findFirst(); - selectItem(optionalOutputs, (int)event.getIndex()); - } + private void setBlockTransaction(TreeItem treeItem, BlockTransactionFetchedEvent event) { + TransactionForm form = treeItem.getValue(); + form.setBlockTransaction(event.getBlockTransaction()); + form.setInputTransactions(event.getInputTransactions()); - private void selectItem(Optional> optionalParent, int index) { - if(optionalParent.isPresent()) { - List> inputs = optionalParent.get().getChildren(); - if(inputs.size() > index) { - txtree.getSelectionModel().select(inputs.get(index)); - } + for(TreeItem childItem : treeItem.getChildren()) { + setBlockTransaction(childItem, event); } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java index 3fa79924..b8c88c00 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionForm.java @@ -1,16 +1,19 @@ package com.sparrowwallet.sparrow.transaction; +import com.sparrowwallet.drongo.protocol.Sha256Hash; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.wallet.BlockTransaction; import javafx.scene.Node; import java.io.IOException; +import java.util.Map; public abstract class TransactionForm { - private Transaction transaction; + private final Transaction transaction; private PSBT psbt; private BlockTransaction blockTransaction; + private Map inputTransactions; public TransactionForm(PSBT psbt) { this.transaction = psbt.getTransaction(); @@ -38,9 +41,23 @@ public abstract class TransactionForm { return blockTransaction; } + public void setBlockTransaction(BlockTransaction blockTransaction) { + this.blockTransaction = blockTransaction; + } + + public Map getInputTransactions() { + return inputTransactions; + } + + public void setInputTransactions(Map inputTransactions) { + this.inputTransactions = inputTransactions; + } + public boolean isEditable() { return blockTransaction == null; } public abstract Node getContents() throws IOException; + + public abstract TransactionView getView(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionView.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionView.java new file mode 100644 index 00000000..4fb902f2 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionView.java @@ -0,0 +1,5 @@ +package com.sparrowwallet.sparrow.transaction; + +public enum TransactionView { + HEADERS, INPUTS, INPUT, OUTPUTS, OUTPUT +} diff --git a/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.fxml b/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.fxml index a29a1803..15a0df89 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/transaction/headers.fxml @@ -99,7 +99,7 @@ -
+
@@ -107,6 +107,12 @@ + + + + + +
diff --git a/src/main/resources/com/sparrowwallet/sparrow/transaction/input.fxml b/src/main/resources/com/sparrowwallet/sparrow/transaction/input.fxml index 55090bdf..5afa796d 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/transaction/input.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/transaction/input.fxml @@ -33,6 +33,7 @@
+