mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-02 20:36:44 +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.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
|
@ -23,6 +24,7 @@ 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;
|
||||
|
@ -57,6 +59,9 @@ public class AppController implements Initializable {
|
|||
@FXML
|
||||
private Menu fileMenu;
|
||||
|
||||
@FXML
|
||||
private MenuItem openTransactionIdItem;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem showTxHex;
|
||||
|
||||
|
@ -166,6 +171,8 @@ public class AppController implements Initializable {
|
|||
connectionService.start();
|
||||
}
|
||||
|
||||
openTransactionIdItem.disableProperty().bind(onlineProperty.not());
|
||||
|
||||
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() {
|
||||
return onlineProperty.get();
|
||||
}
|
||||
|
@ -564,7 +597,7 @@ public class AppController implements Initializable {
|
|||
TabData tabData = (TabData)tab.getUserData();
|
||||
if(tabData instanceof TransactionTabData) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
if(transactionTabData.getTransaction() == transaction) {
|
||||
if(transactionTabData.getTransaction().getTxId().equals(transaction.getTxId())) {
|
||||
return tab;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,9 +18,9 @@ public class CoinLabel extends CopyableLabel {
|
|||
|
||||
public static final DecimalFormat BTC_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
|
||||
private final LongProperty value = new SimpleLongProperty();
|
||||
private Tooltip tooltip;
|
||||
private CoinContextMenu contextMenu;
|
||||
private final LongProperty value = new SimpleLongProperty(-1);
|
||||
private final Tooltip tooltip;
|
||||
private final CoinContextMenu contextMenu;
|
||||
|
||||
public CoinLabel() {
|
||||
this("Unknown");
|
||||
|
@ -62,11 +62,8 @@ public class CoinLabel extends CopyableLabel {
|
|||
}
|
||||
|
||||
private class CoinContextMenu extends ContextMenu {
|
||||
private MenuItem copySatsValue;
|
||||
private MenuItem copyBtcValue;
|
||||
|
||||
public CoinContextMenu() {
|
||||
copySatsValue = new MenuItem("Copy Value in Satoshis");
|
||||
MenuItem copySatsValue = new MenuItem("Copy Value in Satoshis");
|
||||
copySatsValue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
|
@ -74,7 +71,7 @@ public class CoinLabel extends CopyableLabel {
|
|||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
copyBtcValue = new MenuItem("Copy Value in BTC");
|
||||
MenuItem copyBtcValue = new MenuItem("Copy Value in BTC");
|
||||
copyBtcValue.setOnAction(AE -> {
|
||||
hide();
|
||||
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 {
|
||||
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 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 {
|
||||
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 {
|
||||
|
@ -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 {
|
||||
Set<BlockTransactionHash> references = new TreeSet<>();
|
||||
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 {
|
||||
try {
|
||||
Set<Integer> blockHeights = new TreeSet<>();
|
||||
|
@ -221,7 +266,13 @@ public class ElectrumServer {
|
|||
for(Integer height : blockHeights) {
|
||||
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<>();
|
||||
for(Integer height : result.keySet()) {
|
||||
|
@ -232,7 +283,7 @@ public class ElectrumServer {
|
|||
}
|
||||
|
||||
if(!blockHeights.isEmpty()) {
|
||||
throw new IllegalStateException("Could not retrieve blocks " + blockHeights);
|
||||
System.out.println("Could not retrieve " + blockHeights.size() + " blocks");
|
||||
}
|
||||
|
||||
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 {
|
||||
try {
|
||||
Set<BlockTransactionHash> checkReferences = new TreeSet<>(references);
|
||||
|
@ -252,11 +304,28 @@ public class ElectrumServer {
|
|||
for(BlockTransactionHash reference : references) {
|
||||
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<>();
|
||||
for(String txid : result.keySet()) {
|
||||
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));
|
||||
Transaction transaction = new Transaction(rawtx);
|
||||
|
||||
|
@ -268,7 +337,9 @@ public class ElectrumServer {
|
|||
|
||||
BlockHeader blockHeader = blockHeaderMap.get(reference.getHeight());
|
||||
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);
|
||||
|
@ -400,7 +471,20 @@ public class ElectrumServer {
|
|||
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 {
|
||||
public static final ScriptHashTx ERROR_TX = new ScriptHashTx() {
|
||||
@Override
|
||||
public BlockTransactionHash getBlockchainTransactionHash() {
|
||||
return UNFETCHABLE_BLOCK_TRANSACTION;
|
||||
}
|
||||
};
|
||||
|
||||
public int height;
|
||||
public String tx_hash;
|
||||
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 {
|
||||
TCP {
|
||||
@Override
|
||||
|
|
|
@ -210,6 +210,8 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
Long feeAmt = null;
|
||||
if(headersForm.getPsbt() != null) {
|
||||
feeAmt = headersForm.getPsbt().getFee();
|
||||
} else if(headersForm.getTransaction().getInputs().size() == 1 && headersForm.getTransaction().getInputs().get(0).isCoinBase()) {
|
||||
feeAmt = 0L;
|
||||
} else if(headersForm.getInputTransactions() != null) {
|
||||
feeAmt = calculateFee(headersForm.getInputTransactions());
|
||||
}
|
||||
|
@ -229,6 +231,10 @@ public class HeadersController extends TransactionFormController implements Init
|
|||
private long calculateFee(Map<Sha256Hash, BlockTransaction> inputTransactions) {
|
||||
long feeAmt = 0L;
|
||||
for(TransactionInput input : headersForm.getTransaction().getInputs()) {
|
||||
if(input.isCoinBase()) {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
BlockTransaction inputTx = inputTransactions.get(input.getOutpoint().getHash());
|
||||
if(inputTx == null) {
|
||||
throw new IllegalStateException("Cannot find transaction for hash " + input.getOutpoint().getHash());
|
||||
|
|
|
@ -121,7 +121,7 @@ public class InputController extends TransactionFormController implements Initia
|
|||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
|
||||
EventManager.get().register(this);
|
||||
}
|
||||
|
||||
public void initializeView() {
|
||||
|
@ -184,13 +184,16 @@ public class InputController extends TransactionFormController implements Initia
|
|||
BlockTransaction linkedTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
|
||||
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) {
|
||||
TransactionInput txInput = inputForm.getTransactionInput();
|
||||
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
|
||||
TransactionOutput output = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
||||
updateSpends(output);
|
||||
if(!txInput.isCoinBase()) {
|
||||
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
|
||||
TransactionOutput output = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
||||
updateSpends(output);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSpends(TransactionOutput output) {
|
||||
|
|
|
@ -113,13 +113,24 @@ public class InputsController extends TransactionFormController implements Initi
|
|||
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());
|
||||
}
|
||||
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());
|
||||
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);
|
||||
TransactionOutput output = inputTx.getTransaction().getOutputs().get((int)input.getOutpoint().getIndex());
|
||||
outputs.add(output);
|
||||
}
|
||||
}
|
||||
|
||||
long totalAmt = 0;
|
||||
|
|
|
@ -1,17 +1,28 @@
|
|||
package com.sparrowwallet.sparrow.transaction;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
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.CoinLabel;
|
||||
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.Initializable;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import org.fxmisc.richtext.CodeArea;
|
||||
import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
public class OutputController extends TransactionFormController implements Initializable {
|
||||
|
@ -29,12 +40,24 @@ public class OutputController extends TransactionFormController implements Initi
|
|||
@FXML
|
||||
private AddressLabel address;
|
||||
|
||||
@FXML
|
||||
private Field spentField;
|
||||
|
||||
@FXML
|
||||
private Label spent;
|
||||
|
||||
@FXML
|
||||
private Field spentByField;
|
||||
|
||||
@FXML
|
||||
private Hyperlink spentBy;
|
||||
|
||||
@FXML
|
||||
private CodeArea scriptPubKeyArea;
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
|
||||
EventManager.get().register(this);
|
||||
}
|
||||
|
||||
public void initializeView() {
|
||||
|
@ -56,12 +79,63 @@ public class OutputController extends TransactionFormController implements Initi
|
|||
//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();
|
||||
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) {
|
||||
this.outputForm = form;
|
||||
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;
|
||||
|
||||
public class OutputForm extends TransactionForm {
|
||||
private TransactionOutput transactionOutput;
|
||||
private final TransactionOutput transactionOutput;
|
||||
private PSBTOutput psbtOutput;
|
||||
|
||||
public OutputForm(PSBT psbt, PSBTOutput psbtOutput) {
|
||||
|
@ -38,6 +38,10 @@ public class OutputForm extends TransactionForm {
|
|||
return psbtOutput;
|
||||
}
|
||||
|
||||
public int getTransactionOutputIndex() {
|
||||
return getTransaction().getOutputs().indexOf(transactionOutput);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getContents() throws IOException {
|
||||
FXMLLoader loader = new FXMLLoader(getClass().getResource("output.fxml"));
|
||||
|
|
|
@ -62,7 +62,8 @@ public class TransactionController implements Initializable {
|
|||
initializeTxTree();
|
||||
transactionMasterDetail.setShowDetailNode(AppController.showTxHexProperty);
|
||||
refreshTxHex();
|
||||
fetchBlockTransactions();
|
||||
fetchThisAndInputBlockTransactions();
|
||||
fetchOutputBlockTransactions();
|
||||
}
|
||||
|
||||
private void initializeTxTree() {
|
||||
|
@ -134,7 +135,7 @@ public class TransactionController implements Initializable {
|
|||
selectedOutputIndex = outputForm.getTransactionOutput().getIndex();
|
||||
}
|
||||
|
||||
refreshTxHex();
|
||||
Platform.runLater(this::refreshTxHex);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Can't find pane", e);
|
||||
|
@ -149,9 +150,9 @@ public class TransactionController implements Initializable {
|
|||
}
|
||||
|
||||
private void select(TreeItem<TransactionForm> 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) {
|
||||
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;
|
||||
}
|
||||
|
@ -161,12 +162,13 @@ public class TransactionController implements Initializable {
|
|||
}
|
||||
}
|
||||
|
||||
for(TreeItem<TransactionForm> childItem : treeItem.getChildren()) {
|
||||
for (TreeItem<TransactionForm> childItem : treeItem.getChildren()) {
|
||||
select(childItem, view, index);
|
||||
}
|
||||
}
|
||||
|
||||
void refreshTxHex() {
|
||||
//TODO: Handle large transactions like efd513fffbbc2977c2d3933dfaab590b5cab5841ee791b3116e531ac9f8034ed better by not replacing text
|
||||
txhex.clear();
|
||||
|
||||
String hex = "";
|
||||
|
@ -242,14 +244,16 @@ public class TransactionController implements Initializable {
|
|||
}
|
||||
}
|
||||
|
||||
private void fetchBlockTransactions() {
|
||||
if(AppController.isOnline()) {
|
||||
private void fetchThisAndInputBlockTransactions() {
|
||||
if (AppController.isOnline()) {
|
||||
Set<Sha256Hash> references = new HashSet<>();
|
||||
if(psbt == null) {
|
||||
if (psbt == null) {
|
||||
references.add(transaction.getTxId());
|
||||
}
|
||||
for(TransactionInput input : transaction.getInputs()) {
|
||||
references.add(input.getOutpoint().getHash());
|
||||
for (TransactionInput input : transaction.getInputs()) {
|
||||
if(!input.isCoinBase()) {
|
||||
references.add(input.getOutpoint().getHash());
|
||||
}
|
||||
}
|
||||
|
||||
ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(references);
|
||||
|
@ -257,9 +261,9 @@ public class TransactionController implements Initializable {
|
|||
Map<Sha256Hash, BlockTransaction> transactionMap = transactionReferenceService.getValue();
|
||||
BlockTransaction thisBlockTx = null;
|
||||
Map<Sha256Hash, BlockTransaction> inputTransactions = new HashMap<>();
|
||||
for(Sha256Hash txid : transactionMap.keySet()) {
|
||||
for (Sha256Hash txid : transactionMap.keySet()) {
|
||||
BlockTransaction blockTx = transactionMap.get(txid);
|
||||
if(txid.equals(transaction.getTxId())) {
|
||||
if (txid.equals(transaction.getTxId())) {
|
||||
thisBlockTx = blockTx;
|
||||
} else {
|
||||
inputTransactions.put(txid, blockTx);
|
||||
|
@ -268,7 +272,7 @@ public class TransactionController implements Initializable {
|
|||
}
|
||||
|
||||
references.remove(transaction.getTxId());
|
||||
if(!references.isEmpty()) {
|
||||
if (!references.isEmpty()) {
|
||||
System.out.println("Failed to retrieve all referenced input transactions, aborting transaction fetch");
|
||||
return;
|
||||
}
|
||||
|
@ -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) {
|
||||
if (selectedIndex == -1 || selectedIndex == iterableIndex) {
|
||||
return styleClass;
|
||||
|
@ -314,7 +334,7 @@ public class TransactionController implements Initializable {
|
|||
|
||||
@Subscribe
|
||||
public void transactionChanged(TransactionChangedEvent event) {
|
||||
if(event.getTransaction().equals(transaction)) {
|
||||
if (event.getTransaction().equals(transaction)) {
|
||||
refreshTxHex();
|
||||
txtree.refresh();
|
||||
}
|
||||
|
@ -332,7 +352,7 @@ public class TransactionController implements Initializable {
|
|||
|
||||
@Subscribe
|
||||
public void blockTransactionFetched(BlockTransactionFetchedEvent event) {
|
||||
if(event.getTxId().equals(transaction.getTxId())) {
|
||||
if (event.getTxId().equals(transaction.getTxId())) {
|
||||
setBlockTransaction(txtree.getRoot(), event);
|
||||
}
|
||||
}
|
||||
|
@ -342,8 +362,24 @@ public class TransactionController implements Initializable {
|
|||
form.setBlockTransaction(event.getBlockTransaction());
|
||||
form.setInputTransactions(event.getInputTransactions());
|
||||
|
||||
for(TreeItem<TransactionForm> childItem : treeItem.getChildren()) {
|
||||
for (TreeItem<TransactionForm> childItem : treeItem.getChildren()) {
|
||||
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 java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class TransactionForm {
|
||||
|
@ -14,6 +15,7 @@ public abstract class TransactionForm {
|
|||
private PSBT psbt;
|
||||
private BlockTransaction blockTransaction;
|
||||
private Map<Sha256Hash, BlockTransaction> inputTransactions;
|
||||
private List<BlockTransaction> outputTransactions;
|
||||
|
||||
public TransactionForm(PSBT psbt) {
|
||||
this.transaction = psbt.getTransaction();
|
||||
|
@ -53,6 +55,14 @@ public abstract class TransactionForm {
|
|||
this.inputTransactions = inputTransactions;
|
||||
}
|
||||
|
||||
public List<BlockTransaction> getOutputTransactions() {
|
||||
return outputTransactions;
|
||||
}
|
||||
|
||||
public void setOutputTransactions(List<BlockTransaction> outputTransactions) {
|
||||
this.outputTransactions = outputTransactions;
|
||||
}
|
||||
|
||||
public boolean isEditable() {
|
||||
return blockTransaction == null;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,11 @@ import com.sparrowwallet.sparrow.BaseController;
|
|||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.chart.PieChart;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -33,9 +37,17 @@ public abstract class TransactionFormController extends BaseController {
|
|||
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 -> {
|
||||
Tooltip tooltip = new Tooltip();
|
||||
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 + "%"));
|
||||
});
|
||||
}
|
||||
|
||||
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">
|
||||
<items>
|
||||
<MenuItem text="File..." onAction="#openTransactionFromFile"/>
|
||||
<MenuItem fx:id="openTransactionIdItem" text="From ID..." onAction="#openTransactionFromId"/>
|
||||
<MenuItem text="From Text..." onAction="#openTransactionFromText"/>
|
||||
<MenuItem text="Examples" onAction="#openExamples"/>
|
||||
</items>
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<Fieldset fx:id="inputFieldset" inputGrow="SOMETIMES" text="Input" wrapWidth="680">
|
||||
<Field text="Outpoint:" styleClass="label-button">
|
||||
<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">
|
||||
<graphic>
|
||||
<Glyph fontFamily="FontAwesome" icon="EDIT" prefWidth="15" />
|
||||
|
|
|
@ -20,4 +20,12 @@
|
|||
.witness-length { -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" />
|
||||
<AddressLabel fx:id="address" />
|
||||
</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>
|
||||
</Form>
|
||||
|
||||
|
|
Loading…
Reference in a new issue