mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-04 21:36:45 +00:00
transaction viewer block transaction fetch and display
This commit is contained in:
parent
a3a20a788d
commit
86eb8b8294
21 changed files with 508 additions and 96 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit 728b6ce5ef21233b5e1687d3fb4277b79eb2e4e1
|
Subproject commit 75701c725d00a11f7a3931190d029378a04be8f6
|
|
@ -18,12 +18,13 @@ import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.io.*;
|
import com.sparrowwallet.sparrow.io.*;
|
||||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||||
import com.sparrowwallet.sparrow.transaction.TransactionController;
|
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.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.SimpleBooleanProperty;
|
||||||
import javafx.concurrent.Worker;
|
import javafx.concurrent.Worker;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
@ -74,6 +75,8 @@ public class AppController implements Initializable {
|
||||||
//Determines if a change in serverToggle changes the offline/online mode
|
//Determines if a change in serverToggle changes the offline/online mode
|
||||||
private boolean changeMode = true;
|
private boolean changeMode = true;
|
||||||
|
|
||||||
|
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
private Timeline statusTimeline;
|
private Timeline statusTimeline;
|
||||||
|
|
||||||
private ElectrumServer.ConnectionService connectionService;
|
private ElectrumServer.ConnectionService connectionService;
|
||||||
|
@ -155,6 +158,8 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onlineProperty.bindBidirectional(serverToggle.selectedProperty());
|
||||||
|
|
||||||
connectionService = createConnectionService();
|
connectionService = createConnectionService();
|
||||||
Config config = Config.get();
|
Config config = Config.get();
|
||||||
if(config.getMode() == Mode.ONLINE && config.getElectrumServer() != null && !config.getElectrumServer().isEmpty()) {
|
if(config.getMode() == Mode.ONLINE && config.getElectrumServer() != null && !config.getElectrumServer().isEmpty()) {
|
||||||
|
@ -171,7 +176,7 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
connectionService.setOnSucceeded(successEvent -> {
|
connectionService.setOnSucceeded(successEvent -> {
|
||||||
changeMode = false;
|
changeMode = false;
|
||||||
serverToggle.setSelected(true);
|
onlineProperty.setValue(true);
|
||||||
changeMode = true;
|
changeMode = true;
|
||||||
|
|
||||||
if(connectionService.getValue() != null) {
|
if(connectionService.getValue() != null) {
|
||||||
|
@ -180,7 +185,7 @@ public class AppController implements Initializable {
|
||||||
});
|
});
|
||||||
connectionService.setOnFailed(failEvent -> {
|
connectionService.setOnFailed(failEvent -> {
|
||||||
changeMode = false;
|
changeMode = false;
|
||||||
serverToggle.setSelected(false);
|
onlineProperty.setValue(false);
|
||||||
changeMode = true;
|
changeMode = true;
|
||||||
|
|
||||||
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
|
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() {
|
public static Integer getCurrentBlockHeight() {
|
||||||
return currentBlockHeight;
|
return currentBlockHeight;
|
||||||
}
|
}
|
||||||
|
@ -539,18 +548,18 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Tab addTransactionTab(String name, Transaction transaction) {
|
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) {
|
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) {
|
private Tab addTransactionTab(BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
|
||||||
return addTransactionTab(null, blockTransaction.getTransaction(), null, blockTransaction);
|
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()) {
|
for(Tab tab : tabs.getTabs()) {
|
||||||
TabData tabData = (TabData)tab.getUserData();
|
TabData tabData = (TabData)tab.getUserData();
|
||||||
if(tabData instanceof TransactionTabData) {
|
if(tabData instanceof TransactionTabData) {
|
||||||
|
@ -581,6 +590,12 @@ public class AppController implements Initializable {
|
||||||
controller.setBlockTransaction(blockTransaction);
|
controller.setBlockTransaction(blockTransaction);
|
||||||
controller.setTransaction(transaction);
|
controller.setTransaction(transaction);
|
||||||
|
|
||||||
|
if(initialView != null) {
|
||||||
|
controller.setTreeSelection(initialView, initialIndex);
|
||||||
|
} else {
|
||||||
|
controller.setTreeSelection(TransactionView.HEADERS, null);
|
||||||
|
}
|
||||||
|
|
||||||
tabs.getTabs().add(tab);
|
tabs.getTabs().add(tab);
|
||||||
return tab;
|
return tab;
|
||||||
} catch(IOException e) {
|
} catch(IOException e) {
|
||||||
|
@ -715,14 +730,7 @@ public class AppController implements Initializable {
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void viewTransaction(ViewTransactionEvent event) {
|
public void viewTransaction(ViewTransactionEvent event) {
|
||||||
Tab tab = addTransactionTab(event.getBlockTransaction());
|
Tab tab = addTransactionTab(event.getBlockTransaction(), event.getInitialView(), event.getInitialIndex());
|
||||||
tabs.getSelectionModel().select(tab);
|
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()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,10 @@ public class AddressTreeTable extends TreeTableView<Entry> {
|
||||||
setEditable(true);
|
setEditable(true);
|
||||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||||
|
|
||||||
scrollTo(rootEntry.getNode().getHighestUsedIndex());
|
Integer highestUsedIndex = rootEntry.getNode().getHighestUsedIndex();
|
||||||
|
if(highestUsedIndex != null) {
|
||||||
|
scrollTo(highestUsedIndex);
|
||||||
|
}
|
||||||
|
|
||||||
setOnMouseClicked(mouseEvent -> {
|
setOnMouseClicked(mouseEvent -> {
|
||||||
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
|
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
|
||||||
|
|
|
@ -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<Sha256Hash, BlockTransaction> inputTransactions;
|
||||||
|
|
||||||
|
public BlockTransactionFetchedEvent(Sha256Hash txId, BlockTransaction blockTransaction, Map<Sha256Hash, BlockTransaction> inputTransactions) {
|
||||||
|
this.txId = txId;
|
||||||
|
this.blockTransaction = blockTransaction;
|
||||||
|
this.inputTransactions = inputTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Sha256Hash getTxId() {
|
||||||
|
return txId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockTransaction getBlockTransaction() {
|
||||||
|
return blockTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Sha256Hash, BlockTransaction> getInputTransactions() {
|
||||||
|
return inputTransactions;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +1,37 @@
|
||||||
package com.sparrowwallet.sparrow.event;
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||||
|
import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||||
|
|
||||||
public class ViewTransactionEvent {
|
public class ViewTransactionEvent {
|
||||||
public final BlockTransaction blockTransaction;
|
public final BlockTransaction blockTransaction;
|
||||||
public final HashIndexEntry hashIndexEntry;
|
public final TransactionView initialView;
|
||||||
|
public final Integer initialIndex;
|
||||||
|
|
||||||
public ViewTransactionEvent(BlockTransaction blockTransaction) {
|
public ViewTransactionEvent(BlockTransaction blockTransaction) {
|
||||||
this(blockTransaction, null);
|
this(blockTransaction, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ViewTransactionEvent(BlockTransaction blockTransaction, HashIndexEntry hashIndexEntry) {
|
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.blockTransaction = blockTransaction;
|
||||||
this.hashIndexEntry = hashIndexEntry;
|
this.initialView = initialView;
|
||||||
|
this.initialIndex = initialIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BlockTransaction getBlockTransaction() {
|
public BlockTransaction getBlockTransaction() {
|
||||||
return blockTransaction;
|
return blockTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HashIndexEntry getHashIndexEntry() {
|
public TransactionView getInitialView() {
|
||||||
return hashIndexEntry;
|
return initialView;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getInitialIndex() {
|
||||||
|
return initialIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package com.sparrowwallet.sparrow.io;
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.github.arteam.simplejsonrpc.client.*;
|
import com.github.arteam.simplejsonrpc.client.*;
|
||||||
import com.github.arteam.simplejsonrpc.client.builder.BatchRequestBuilder;
|
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.JsonRpcMethod;
|
||||||
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam;
|
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcParam;
|
||||||
import com.github.arteam.simplejsonrpc.core.annotation.JsonRpcService;
|
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.Utils;
|
||||||
import com.sparrowwallet.drongo.protocol.*;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
|
import com.sparrowwallet.sparrow.AppController;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.ConnectionEvent;
|
import com.sparrowwallet.sparrow.event.ConnectionEvent;
|
||||||
import com.sparrowwallet.sparrow.event.NewBlockEvent;
|
import com.sparrowwallet.sparrow.event.NewBlockEvent;
|
||||||
|
@ -365,6 +368,32 @@ public class ElectrumServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Map<Sha256Hash, BlockTransaction> getReferencedTransactions(Set<Sha256Hash> references) throws ServerException {
|
||||||
|
JsonRpcClient client = new JsonRpcClient(getTransport());
|
||||||
|
BatchRequestBuilder<String, VerboseTransaction> batchRequest = client.createBatchRequest().keysType(String.class).returnType(VerboseTransaction.class);
|
||||||
|
for(Sha256Hash reference : references) {
|
||||||
|
batchRequest.add(reference.toString(), "blockchain.transaction.get", reference.toString(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, VerboseTransaction> result;
|
||||||
|
try {
|
||||||
|
result = batchRequest.execute();
|
||||||
|
} catch (JsonRpcBatchException e) {
|
||||||
|
System.out.println("Some errors retrieving transactions: " + e.getErrors());
|
||||||
|
result = (Map<String, VerboseTransaction>)e.getSuccesses();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Sha256Hash, BlockTransaction> 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) {
|
private String getScriptHash(Wallet wallet, WalletNode node) {
|
||||||
byte[] hash = Sha256Hash.hash(wallet.getOutputScript(node).getProgram());
|
byte[] hash = Sha256Hash.hash(wallet.getOutputScript(node).getProgram());
|
||||||
byte[] reversed = Utils.reverseBytes(hash);
|
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
|
@JsonRpcService
|
||||||
public static class SubscriptionService {
|
public static class SubscriptionService {
|
||||||
@JsonRpcMethod("blockchain.headers.subscribe")
|
@JsonRpcMethod("blockchain.headers.subscribe")
|
||||||
|
@ -571,7 +630,7 @@ public class ElectrumServer {
|
||||||
public static class ProxyTcpOverTlsTransport extends TcpOverTlsTransport {
|
public static class ProxyTcpOverTlsTransport extends TcpOverTlsTransport {
|
||||||
public static final int DEFAULT_PROXY_PORT = 1080;
|
public static final int DEFAULT_PROXY_PORT = 1080;
|
||||||
|
|
||||||
private HostAndPort proxy;
|
private final HostAndPort proxy;
|
||||||
|
|
||||||
public ProxyTcpOverTlsTransport(HostAndPort server, HostAndPort proxy) throws KeyManagementException, NoSuchAlgorithmException {
|
public ProxyTcpOverTlsTransport(HostAndPort server, HostAndPort proxy) throws KeyManagementException, NoSuchAlgorithmException {
|
||||||
super(server);
|
super(server);
|
||||||
|
@ -715,6 +774,32 @@ public class ElectrumServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class TransactionReferenceService extends Service<Map<Sha256Hash, BlockTransaction>> {
|
||||||
|
private final Set<Sha256Hash> 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<Sha256Hash> references) {
|
||||||
|
this.references = references;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<Map<Sha256Hash, BlockTransaction>> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
protected Map<Sha256Hash, BlockTransaction> call() throws ServerException {
|
||||||
|
ElectrumServer electrumServer = new ElectrumServer();
|
||||||
|
return electrumServer.getReferencedTransactions(references);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public enum Protocol {
|
public enum Protocol {
|
||||||
TCP {
|
TCP {
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,15 +1,22 @@
|
||||||
package com.sparrowwallet.sparrow.transaction;
|
package com.sparrowwallet.sparrow.transaction;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
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.AppController;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.control.CoinLabel;
|
import com.sparrowwallet.sparrow.control.CoinLabel;
|
||||||
import com.sparrowwallet.sparrow.control.IdLabel;
|
import com.sparrowwallet.sparrow.control.IdLabel;
|
||||||
import com.sparrowwallet.sparrow.control.CopyableLabel;
|
import com.sparrowwallet.sparrow.control.CopyableLabel;
|
||||||
|
import com.sparrowwallet.sparrow.event.BlockTransactionFetchedEvent;
|
||||||
import com.sparrowwallet.sparrow.event.TransactionChangedEvent;
|
import com.sparrowwallet.sparrow.event.TransactionChangedEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.Initializable;
|
import javafx.fxml.Initializable;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.input.Clipboard;
|
||||||
|
import javafx.scene.input.ClipboardContent;
|
||||||
import tornadofx.control.DateTimePicker;
|
import tornadofx.control.DateTimePicker;
|
||||||
import tornadofx.control.Field;
|
import tornadofx.control.Field;
|
||||||
import tornadofx.control.Fieldset;
|
import tornadofx.control.Fieldset;
|
||||||
|
@ -17,11 +24,14 @@ import com.google.common.eventbus.Subscribe;
|
||||||
import tornadofx.control.Form;
|
import tornadofx.control.Form;
|
||||||
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.time.*;
|
import java.time.*;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
public class HeadersController extends TransactionFormController implements Initializable {
|
public class HeadersController extends TransactionFormController implements Initializable {
|
||||||
public static final String LOCKTIME_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
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;
|
private HeadersForm headersForm;
|
||||||
|
|
||||||
|
@ -88,6 +98,12 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
@FXML
|
@FXML
|
||||||
private CopyableLabel blockHeight;
|
private CopyableLabel blockHeight;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private CopyableLabel blockTimestamp;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private IdLabel blockHash;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
public void initialize(URL location, ResourceBundle resources) {
|
||||||
EventManager.get().register(this);
|
EventManager.get().register(this);
|
||||||
|
@ -194,26 +210,82 @@ 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.getInputTransactions() != null) {
|
||||||
|
feeAmt = calculateFee(headersForm.getInputTransactions());
|
||||||
}
|
}
|
||||||
|
|
||||||
if(feeAmt != null) {
|
if(feeAmt != null) {
|
||||||
fee.setValue(feeAmt);
|
updateFee(feeAmt);
|
||||||
double feeRateAmt = feeAmt.doubleValue() / tx.getVirtualSize();
|
|
||||||
feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vByte");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blockchainForm.managedProperty().bind(blockchainForm.visibleProperty());
|
blockchainForm.managedProperty().bind(blockchainForm.visibleProperty());
|
||||||
blockchainForm.setVisible(headersForm.getBlockTransaction() != null);
|
|
||||||
if(headersForm.getBlockTransaction() != null) {
|
if(headersForm.getBlockTransaction() != null) {
|
||||||
Integer currentHeight = AppController.getCurrentBlockHeight();
|
updateBlockchainForm(headersForm.getBlockTransaction());
|
||||||
if(currentHeight == null) {
|
} else {
|
||||||
blockStatus.setText("Unknown");
|
blockchainForm.setVisible(false);
|
||||||
} else {
|
}
|
||||||
int confirmations = currentHeight - headersForm.getBlockTransaction().getHeight() + 1;
|
}
|
||||||
blockStatus.setText(confirmations + " Confirmations");
|
|
||||||
|
private long calculateFee(Map<Sha256Hash, BlockTransaction> 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);
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ public class HeadersForm extends TransactionForm {
|
||||||
super(transaction);
|
super(transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public Node getContents() throws IOException {
|
public Node getContents() throws IOException {
|
||||||
FXMLLoader loader = new FXMLLoader(getClass().getResource("headers.fxml"));
|
FXMLLoader loader = new FXMLLoader(getClass().getResource("headers.fxml"));
|
||||||
Node node = loader.load();
|
Node node = loader.load();
|
||||||
|
@ -29,6 +30,11 @@ public class HeadersForm extends TransactionForm {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransactionView getView() {
|
||||||
|
return TransactionView.HEADERS;
|
||||||
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Tx [" + getTransaction().calculateTxId(false).toString().substring(0, 6) + "]";
|
return "Tx [" + getTransaction().calculateTxId(false).toString().substring(0, 6) + "]";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
package com.sparrowwallet.sparrow.transaction;
|
package com.sparrowwallet.sparrow.transaction;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
import com.sparrowwallet.drongo.KeyDerivation;
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
import com.sparrowwallet.drongo.protocol.*;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||||
|
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.control.*;
|
import com.sparrowwallet.sparrow.control.*;
|
||||||
|
import com.sparrowwallet.sparrow.event.BlockTransactionFetchedEvent;
|
||||||
import com.sparrowwallet.sparrow.event.TransactionChangedEvent;
|
import com.sparrowwallet.sparrow.event.TransactionChangedEvent;
|
||||||
|
import com.sparrowwallet.sparrow.event.ViewTransactionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.Initializable;
|
import javafx.fxml.Initializable;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
@ -28,6 +32,7 @@ import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
public class InputController extends TransactionFormController implements Initializable {
|
public class InputController extends TransactionFormController implements Initializable {
|
||||||
|
@ -39,6 +44,9 @@ public class InputController extends TransactionFormController implements Initia
|
||||||
@FXML
|
@FXML
|
||||||
private IdLabel outpoint;
|
private IdLabel outpoint;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Hyperlink linkedOutpoint;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Button outpointSelect;
|
private Button outpointSelect;
|
||||||
|
|
||||||
|
@ -128,6 +136,10 @@ public class InputController extends TransactionFormController implements Initia
|
||||||
|
|
||||||
private void initializeInputFields(TransactionInput txInput, PSBTInput psbtInput) {
|
private void initializeInputFields(TransactionInput txInput, PSBTInput psbtInput) {
|
||||||
inputFieldset.setText("Input #" + txInput.getIndex());
|
inputFieldset.setText("Input #" + txInput.getIndex());
|
||||||
|
|
||||||
|
outpoint.managedProperty().bind(outpoint.visibleProperty());
|
||||||
|
linkedOutpoint.managedProperty().bind(linkedOutpoint.visibleProperty());
|
||||||
|
|
||||||
if(txInput.isCoinBase()) {
|
if(txInput.isCoinBase()) {
|
||||||
outpoint.setText("Coinbase");
|
outpoint.setText("Coinbase");
|
||||||
outpointSelect.setVisible(false);
|
outpointSelect.setVisible(false);
|
||||||
|
@ -136,7 +148,11 @@ public class InputController extends TransactionFormController implements Initia
|
||||||
totalAmt += output.getValue();
|
totalAmt += output.getValue();
|
||||||
}
|
}
|
||||||
spends.setValue(totalAmt);
|
spends.setValue(totalAmt);
|
||||||
|
} else if(inputForm.getInputTransactions() != null) {
|
||||||
|
updateOutpoint(inputForm.getInputTransactions());
|
||||||
} else {
|
} else {
|
||||||
|
outpoint.setVisible(true);
|
||||||
|
linkedOutpoint.setVisible(false);
|
||||||
outpoint.setText(txInput.getOutpoint().getHash().toString() + ":" + txInput.getOutpoint().getIndex());
|
outpoint.setText(txInput.getOutpoint().getHash().toString() + ":" + txInput.getOutpoint().getIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,26 +165,51 @@ public class InputController extends TransactionFormController implements Initia
|
||||||
output = psbtInput.getWitnessUtxo();
|
output = psbtInput.getWitnessUtxo();
|
||||||
}
|
}
|
||||||
|
|
||||||
if(output != null) {
|
updateSpends(output);
|
||||||
spends.setValue(output.getValue());
|
} else if(inputForm.getInputTransactions() != null) {
|
||||||
try {
|
updateSpends(inputForm.getInputTransactions());
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Enable select outpoint when wallet present
|
//TODO: Enable select outpoint when wallet present
|
||||||
outpointSelect.setDisable(true);
|
outpointSelect.setDisable(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateOutpoint(Map<Sha256Hash, BlockTransaction> 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<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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
private void initializeScriptFields(TransactionInput txInput, PSBTInput psbtInput) {
|
||||||
//TODO: Is this safe?
|
//TODO: Is this safe?
|
||||||
Script redeemScript = txInput.getScriptSig().getFirstNestedScript();
|
Script redeemScript = txInput.getScriptSig().getFirstNestedScript();
|
||||||
|
@ -410,4 +451,14 @@ public class InputController extends TransactionFormController implements Initia
|
||||||
|
|
||||||
return chunkString;
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ public class InputForm extends TransactionForm {
|
||||||
return psbtInput;
|
return psbtInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public Node getContents() throws IOException {
|
public Node getContents() throws IOException {
|
||||||
FXMLLoader loader = new FXMLLoader(getClass().getResource("input.fxml"));
|
FXMLLoader loader = new FXMLLoader(getClass().getResource("input.fxml"));
|
||||||
Node node = loader.load();
|
Node node = loader.load();
|
||||||
|
@ -46,6 +47,11 @@ public class InputForm extends TransactionForm {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransactionView getView() {
|
||||||
|
return TransactionView.INPUT;
|
||||||
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Input #" + transactionInput.getIndex();
|
return "Input #" + transactionInput.getIndex();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
package com.sparrowwallet.sparrow.transaction;
|
package com.sparrowwallet.sparrow.transaction;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
import com.sparrowwallet.drongo.protocol.*;
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
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.CoinLabel;
|
||||||
import com.sparrowwallet.sparrow.control.CopyableLabel;
|
import com.sparrowwallet.sparrow.control.CopyableLabel;
|
||||||
|
import com.sparrowwallet.sparrow.event.BlockTransactionFetchedEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.Initializable;
|
import javafx.fxml.Initializable;
|
||||||
import javafx.scene.chart.PieChart;
|
import javafx.scene.chart.PieChart;
|
||||||
|
@ -11,6 +15,7 @@ import javafx.scene.chart.PieChart;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
public class InputsController extends TransactionFormController implements Initializable {
|
public class InputsController extends TransactionFormController implements Initializable {
|
||||||
|
@ -30,7 +35,7 @@ public class InputsController extends TransactionFormController implements Initi
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL location, ResourceBundle resources) {
|
public void initialize(URL location, ResourceBundle resources) {
|
||||||
|
EventManager.get().register(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setModel(InputsForm form) {
|
public void setModel(InputsForm form) {
|
||||||
|
@ -92,6 +97,46 @@ public class InputsController extends TransactionFormController implements Initi
|
||||||
}
|
}
|
||||||
|
|
||||||
addPieData(inputsPie, outputs);
|
addPieData(inputsPie, outputs);
|
||||||
|
} else if(inputsForm.getInputTransactions() != null) {
|
||||||
|
updateBlockTransactionInputs(inputsForm.getInputTransactions());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBlockTransactionInputs(Map<Sha256Hash, BlockTransaction> inputTransactions) {
|
||||||
|
List<TransactionOutput> 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ public class InputsForm extends TransactionForm {
|
||||||
super(transaction);
|
super(transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public Node getContents() throws IOException {
|
public Node getContents() throws IOException {
|
||||||
FXMLLoader loader = new FXMLLoader(getClass().getResource("inputs.fxml"));
|
FXMLLoader loader = new FXMLLoader(getClass().getResource("inputs.fxml"));
|
||||||
Node node = loader.load();
|
Node node = loader.load();
|
||||||
|
@ -29,6 +30,11 @@ public class InputsForm extends TransactionForm {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransactionView getView() {
|
||||||
|
return TransactionView.INPUTS;
|
||||||
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Inputs";
|
return "Inputs";
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,7 @@ public class OutputForm extends TransactionForm {
|
||||||
return psbtOutput;
|
return psbtOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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"));
|
||||||
Node node = loader.load();
|
Node node = loader.load();
|
||||||
|
@ -46,6 +47,11 @@ public class OutputForm extends TransactionForm {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransactionView getView() {
|
||||||
|
return TransactionView.OUTPUT;
|
||||||
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Output #" + transactionOutput.getIndex();
|
return "Output #" + transactionOutput.getIndex();
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ public class OutputsForm extends TransactionForm {
|
||||||
super(transaction);
|
super(transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public Node getContents() throws IOException {
|
public Node getContents() throws IOException {
|
||||||
FXMLLoader loader = new FXMLLoader(getClass().getResource("outputs.fxml"));
|
FXMLLoader loader = new FXMLLoader(getClass().getResource("outputs.fxml"));
|
||||||
Node node = loader.load();
|
Node node = loader.load();
|
||||||
|
@ -29,6 +30,11 @@ public class OutputsForm extends TransactionForm {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransactionView getView() {
|
||||||
|
return TransactionView.OUTPUTS;
|
||||||
|
}
|
||||||
|
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Outputs";
|
return "Outputs";
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||||
import com.sparrowwallet.sparrow.AppController;
|
import com.sparrowwallet.sparrow.AppController;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
|
import com.sparrowwallet.sparrow.io.ElectrumServer;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.Initializable;
|
import javafx.fxml.Initializable;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
@ -25,9 +27,7 @@ import org.fxmisc.richtext.CodeArea;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.List;
|
import java.util.*;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.ResourceBundle;
|
|
||||||
|
|
||||||
public class TransactionController implements Initializable {
|
public class TransactionController implements Initializable {
|
||||||
|
|
||||||
|
@ -49,6 +49,7 @@ public class TransactionController implements Initializable {
|
||||||
private Transaction transaction;
|
private Transaction transaction;
|
||||||
private PSBT psbt;
|
private PSBT psbt;
|
||||||
private BlockTransaction blockTransaction;
|
private BlockTransaction blockTransaction;
|
||||||
|
|
||||||
private int selectedInputIndex = -1;
|
private int selectedInputIndex = -1;
|
||||||
private int selectedOutputIndex = -1;
|
private int selectedOutputIndex = -1;
|
||||||
|
|
||||||
|
@ -61,6 +62,7 @@ public class TransactionController implements Initializable {
|
||||||
initializeTxTree();
|
initializeTxTree();
|
||||||
transactionMasterDetail.setShowDetailNode(AppController.showTxHexProperty);
|
transactionMasterDetail.setShowDetailNode(AppController.showTxHexProperty);
|
||||||
refreshTxHex();
|
refreshTxHex();
|
||||||
|
fetchBlockTransactions();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeTxTree() {
|
private void initializeTxTree() {
|
||||||
|
@ -142,6 +144,28 @@ public class TransactionController implements Initializable {
|
||||||
txtree.getSelectionModel().select(txtree.getRoot());
|
txtree.getSelectionModel().select(txtree.getRoot());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTreeSelection(TransactionView view, Integer index) {
|
||||||
|
select(txtree.getRoot(), view, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
txtree.getSelectionModel().select(treeItem);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
txtree.getSelectionModel().select(treeItem);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for(TreeItem<TransactionForm> childItem : treeItem.getChildren()) {
|
||||||
|
select(childItem, view, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void refreshTxHex() {
|
void refreshTxHex() {
|
||||||
txhex.clear();
|
txhex.clear();
|
||||||
|
|
||||||
|
@ -218,6 +242,49 @@ public class TransactionController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void fetchBlockTransactions() {
|
||||||
|
if(AppController.isOnline()) {
|
||||||
|
Set<Sha256Hash> 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<Sha256Hash, BlockTransaction> transactionMap = transactionReferenceService.getValue();
|
||||||
|
BlockTransaction thisBlockTx = null;
|
||||||
|
Map<Sha256Hash, BlockTransaction> 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) {
|
private String getIndexedStyleClass(int iterableIndex, int selectedIndex, String styleClass) {
|
||||||
if (selectedIndex == -1 || selectedIndex == iterableIndex) {
|
if (selectedIndex == -1 || selectedIndex == iterableIndex) {
|
||||||
return styleClass;
|
return styleClass;
|
||||||
|
@ -264,23 +331,19 @@ public class TransactionController implements Initializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void inputSelected(TransactionInputSelectedEvent event) {
|
public void blockTransactionFetched(BlockTransactionFetchedEvent event) {
|
||||||
Optional<TreeItem<TransactionForm>> optionalInputs = txtree.getRoot().getChildren().stream().filter(item -> item.getValue() instanceof InputsForm).findFirst();
|
if(event.getTxId().equals(transaction.getTxId())) {
|
||||||
selectItem(optionalInputs, (int)event.getIndex());
|
setBlockTransaction(txtree.getRoot(), event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
private void setBlockTransaction(TreeItem<TransactionForm> treeItem, BlockTransactionFetchedEvent event) {
|
||||||
public void outputSelected(TransactionOutputSelectedEvent event) {
|
TransactionForm form = treeItem.getValue();
|
||||||
Optional<TreeItem<TransactionForm>> optionalOutputs = txtree.getRoot().getChildren().stream().filter(item -> item.getValue() instanceof OutputsForm).findFirst();
|
form.setBlockTransaction(event.getBlockTransaction());
|
||||||
selectItem(optionalOutputs, (int)event.getIndex());
|
form.setInputTransactions(event.getInputTransactions());
|
||||||
}
|
|
||||||
|
|
||||||
private void selectItem(Optional<TreeItem<TransactionForm>> optionalParent, int index) {
|
for(TreeItem<TransactionForm> childItem : treeItem.getChildren()) {
|
||||||
if(optionalParent.isPresent()) {
|
setBlockTransaction(childItem, event);
|
||||||
List<TreeItem<TransactionForm>> inputs = optionalParent.get().getChildren();
|
|
||||||
if(inputs.size() > index) {
|
|
||||||
txtree.getSelectionModel().select(inputs.get(index));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
package com.sparrowwallet.sparrow.transaction;
|
package com.sparrowwallet.sparrow.transaction;
|
||||||
|
|
||||||
|
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.wallet.BlockTransaction;
|
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.Map;
|
||||||
|
|
||||||
public abstract class TransactionForm {
|
public abstract class TransactionForm {
|
||||||
private Transaction transaction;
|
private final Transaction transaction;
|
||||||
private PSBT psbt;
|
private PSBT psbt;
|
||||||
private BlockTransaction blockTransaction;
|
private BlockTransaction blockTransaction;
|
||||||
|
private Map<Sha256Hash, BlockTransaction> inputTransactions;
|
||||||
|
|
||||||
public TransactionForm(PSBT psbt) {
|
public TransactionForm(PSBT psbt) {
|
||||||
this.transaction = psbt.getTransaction();
|
this.transaction = psbt.getTransaction();
|
||||||
|
@ -38,9 +41,23 @@ public abstract class TransactionForm {
|
||||||
return blockTransaction;
|
return blockTransaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setBlockTransaction(BlockTransaction blockTransaction) {
|
||||||
|
this.blockTransaction = blockTransaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<Sha256Hash, BlockTransaction> getInputTransactions() {
|
||||||
|
return inputTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInputTransactions(Map<Sha256Hash, BlockTransaction> inputTransactions) {
|
||||||
|
this.inputTransactions = inputTransactions;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEditable() {
|
public boolean isEditable() {
|
||||||
return blockTransaction == null;
|
return blockTransaction == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Node getContents() throws IOException;
|
public abstract Node getContents() throws IOException;
|
||||||
|
|
||||||
|
public abstract TransactionView getView();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.sparrowwallet.sparrow.transaction;
|
||||||
|
|
||||||
|
public enum TransactionView {
|
||||||
|
HEADERS, INPUTS, INPUT, OUTPUTS, OUTPUT
|
||||||
|
}
|
|
@ -99,7 +99,7 @@
|
||||||
|
|
||||||
<Separator GridPane.columnIndex="0" GridPane.rowIndex="5" GridPane.columnSpan="2" styleClass="form-separator"/>
|
<Separator GridPane.columnIndex="0" GridPane.rowIndex="5" GridPane.columnSpan="2" styleClass="form-separator"/>
|
||||||
|
|
||||||
<Form fx:id="blockchainForm" GridPane.columnIndex="0" GridPane.rowIndex="6">
|
<Form fx:id="blockchainForm" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="6">
|
||||||
<Fieldset text="Blockchain" inputGrow="SOMETIMES">
|
<Fieldset text="Blockchain" inputGrow="SOMETIMES">
|
||||||
<Field text="Status:">
|
<Field text="Status:">
|
||||||
<CopyableLabel fx:id="blockStatus" />
|
<CopyableLabel fx:id="blockStatus" />
|
||||||
|
@ -107,6 +107,12 @@
|
||||||
<Field text="Block Height:">
|
<Field text="Block Height:">
|
||||||
<CopyableLabel fx:id="blockHeight" />
|
<CopyableLabel fx:id="blockHeight" />
|
||||||
</Field>
|
</Field>
|
||||||
|
<Field text="Timestamp:">
|
||||||
|
<CopyableLabel fx:id="blockTimestamp" />
|
||||||
|
</Field>
|
||||||
|
<Field text="Block Hash:">
|
||||||
|
<IdLabel fx:id="blockHash" />
|
||||||
|
</Field>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
|
|
@ -33,6 +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" />
|
||||||
<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" />
|
||||||
|
|
Loading…
Reference in a new issue