fetch transaction output transactions, handle coinbase input

This commit is contained in:
Craig Raw 2020-06-14 10:36:24 +02:00
parent 86eb8b8294
commit b1b4734094
17 changed files with 491 additions and 48 deletions

View file

@ -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;
} }
} }

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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

View file

@ -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());

View file

@ -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) {

View file

@ -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) {

View file

@ -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());
}
}
} }

View file

@ -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"));

View file

@ -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);
}
}
} }

View file

@ -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;
} }

View file

@ -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);
}
}
} }

View file

@ -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>

View file

@ -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" />

View file

@ -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;
}

View file

@ -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>