From b6b18a2a7cd8562b3c5fdf86dacce650a020c75b Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Sat, 28 Mar 2020 10:45:39 +0200 Subject: [PATCH] transaction headers pane first draft --- README.md | 5 + build.gradle | 2 + drongo | 2 +- .../com/craigraw/sparrow/AppController.java | 37 +++- .../com/craigraw/sparrow/EventManager.java | 31 +++ .../sparrow/TransactionController.java | 185 ++++++++++++------ .../craigraw/sparrow/TransactionListener.java | 7 + .../java/com/craigraw/sparrow/form/Form.java | 9 + .../sparrow/form/HeadersController.java | 157 +++++++++++++++ .../craigraw/sparrow/form/HeadersForm.java | 38 ++++ .../sparrow/form/InputController.java | 19 ++ .../com/craigraw/sparrow/form/InputForm.java | 27 +++ .../sparrow/form/InputsController.java | 19 ++ .../com/craigraw/sparrow/form/InputsForm.java | 27 +++ .../sparrow/form/OutputController.java | 19 ++ .../com/craigraw/sparrow/form/OutputForm.java | 27 +++ .../sparrow/form/OutputsController.java | 19 ++ .../craigraw/sparrow/form/OutputsForm.java | 27 +++ .../sparrow/form/PartialInputController.java | 19 ++ .../sparrow/form/PartialInputForm.java | 23 +++ .../sparrow/form/PartialOutputController.java | 19 ++ .../sparrow/form/PartialOutputForm.java | 23 +++ .../resources/com/craigraw/sparrow/app.fxml | 2 +- .../com/craigraw/sparrow/form/headers.fxml | 82 ++++++++ .../com/craigraw/sparrow/form/input.fxml | 14 ++ .../com/craigraw/sparrow/form/inputs.fxml | 14 ++ .../com/craigraw/sparrow/form/output.fxml | 14 ++ .../com/craigraw/sparrow/form/outputs.fxml | 14 ++ .../craigraw/sparrow/form/partialinput.fxml | 14 ++ .../craigraw/sparrow/form/partialoutput.fxml | 14 ++ .../com/craigraw/sparrow/general.css | 6 + .../com/craigraw/sparrow/transaction.css | 27 +++ .../com/craigraw/sparrow/transaction.fxml | 19 +- 33 files changed, 885 insertions(+), 76 deletions(-) create mode 100644 src/main/java/com/craigraw/sparrow/EventManager.java create mode 100644 src/main/java/com/craigraw/sparrow/TransactionListener.java create mode 100644 src/main/java/com/craigraw/sparrow/form/Form.java create mode 100644 src/main/java/com/craigraw/sparrow/form/HeadersController.java create mode 100644 src/main/java/com/craigraw/sparrow/form/HeadersForm.java create mode 100644 src/main/java/com/craigraw/sparrow/form/InputController.java create mode 100644 src/main/java/com/craigraw/sparrow/form/InputForm.java create mode 100644 src/main/java/com/craigraw/sparrow/form/InputsController.java create mode 100644 src/main/java/com/craigraw/sparrow/form/InputsForm.java create mode 100644 src/main/java/com/craigraw/sparrow/form/OutputController.java create mode 100644 src/main/java/com/craigraw/sparrow/form/OutputForm.java create mode 100644 src/main/java/com/craigraw/sparrow/form/OutputsController.java create mode 100644 src/main/java/com/craigraw/sparrow/form/OutputsForm.java create mode 100644 src/main/java/com/craigraw/sparrow/form/PartialInputController.java create mode 100644 src/main/java/com/craigraw/sparrow/form/PartialInputForm.java create mode 100644 src/main/java/com/craigraw/sparrow/form/PartialOutputController.java create mode 100644 src/main/java/com/craigraw/sparrow/form/PartialOutputForm.java create mode 100644 src/main/resources/com/craigraw/sparrow/form/headers.fxml create mode 100644 src/main/resources/com/craigraw/sparrow/form/input.fxml create mode 100644 src/main/resources/com/craigraw/sparrow/form/inputs.fxml create mode 100644 src/main/resources/com/craigraw/sparrow/form/output.fxml create mode 100644 src/main/resources/com/craigraw/sparrow/form/outputs.fxml create mode 100644 src/main/resources/com/craigraw/sparrow/form/partialinput.fxml create mode 100644 src/main/resources/com/craigraw/sparrow/form/partialoutput.fxml create mode 100644 src/main/resources/com/craigraw/sparrow/general.css create mode 100644 src/main/resources/com/craigraw/sparrow/transaction.css diff --git a/README.md b/README.md index 75da039b..36b96fc2 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,8 @@ Bitcoin Transaction Editor To clone this project, use `git clone --recursive git@github.com:craigraw/sparrow.git` + +## Various ways to hex dump a file without spaces: +xxd -p file | tr -d '\n' +hexdump -ve '1/1 "%.2x"' +od -t x1 -An file | tr -d '\n ' \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0d47fda4..8ae8dd8b 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,8 @@ javafx { dependencies { implementation project(':drongo') + compile group: 'org.fxmisc.richtext', name: 'richtextfx', version: '0.10.4' + compile group: 'no.tornado', name: 'tornadofx-controls', version: '1.0.4' testCompile group: 'junit', name: 'junit', version: '4.12' } diff --git a/drongo b/drongo index 566aa2e9..7fb5601d 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit 566aa2e95315291dd7c2092dfc1378bf2fa0d484 +Subproject commit 7fb5601de38e79d5c0547ebc684ca137aa895134 diff --git a/src/main/java/com/craigraw/sparrow/AppController.java b/src/main/java/com/craigraw/sparrow/AppController.java index 3c59aa88..70050439 100644 --- a/src/main/java/com/craigraw/sparrow/AppController.java +++ b/src/main/java/com/craigraw/sparrow/AppController.java @@ -1,11 +1,12 @@ package com.craigraw.sparrow; +import com.craigraw.drongo.Utils; +import com.craigraw.drongo.protocol.Transaction; import com.craigraw.drongo.psbt.PSBT; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; -import javafx.scene.Parent; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.stage.FileChooser; @@ -25,13 +26,20 @@ public class AppController implements Initializable { @Override public void initialize(URL location, ResourceBundle resources) { - + addExampleTxTabs(); } public void open(ActionEvent event) { Stage window = new Stage(); FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Open Transaction"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("All Files", "*.*"), + new FileChooser.ExtensionFilter("PSBT", "*.psbt"), + new FileChooser.ExtensionFilter("TXN", "*.txn") + ); + File file = fileChooser.showOpenDialog(window); if (file != null) { System.out.println(file); @@ -61,4 +69,29 @@ public class AppController implements Initializable { } } } + + private void addExampleTxTabs() { + addTransactionTab("p2pkh", "01000000019c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48010000006a47304402203da9d487be5302a6d69e02a861acff1da472885e43d7528ed9b1b537a8e2cac9022002d1bca03a1e9715a99971bafe3b1852b7a4f0168281cbd27a220380a01b3307012102c9950c622494c2e9ff5a003e33b690fe4832477d32c2d256c67eab8bf613b34effffffff02b6f50500000000001976a914bdf63990d6dc33d705b756e13dd135466c06b3b588ac845e0201000000001976a9145fb0e9755a3424efd2ba0587d20b1e98ee29814a88ac06241559"); + addTransactionTab("p2sh-p2wpkh", "01000000000101db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477010000001716001479091972186c449eb1ded22b78e40d009bdf0089feffffff02b8b4eb0b000000001976a914a457b684d7f0d539a46a45bbc043f35b59d0d96388ac0008af2f000000001976a914fd270b1ee6abcaea97fea7ad0402e8bd8ad6d77c88ac02473044022047ac8e878352d3ebbde1c94ce3a10d057c24175747116f8288e5d794d12d482f0220217f36a485cae903c713331d877c1f64677e3622ad4010726870540656fe9dcb012103ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a2687392040000"); + addTransactionTab("p2wpkh", "01000000000102fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f00000000494830450221008b9d1dc26ba6a9cb62127b02742fa9d754cd3bebf337f7a55d114c8e5cdd30be022040529b194ba3f9281a99f2b1c0a19c0489bc22ede944ccf4ecbab4cc618ef3ed01eeffffffef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff02202cb206000000001976a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac9093510d000000001976a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac000247304402203609e17b84f6a7d30c80bfa610b5b4542f32a8a0d5447a12fb1366d7f01cc44a0220573a954c4518331561406f90300e8f3358f51928d43c212a8caed02de67eebee0121025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee635711000000"); + addTransactionTab("p2wsh", "01000000000102fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e000000004847304402200af4e47c9b9629dbecc21f73af989bdaa911f7e6f6c2e9394588a3aa68f81e9902204f3fcf6ade7e5abb1295b6774c8e0abd94ae62217367096bc02ee5e435b67da201ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac000347304402200de66acf4527789bfda55fc5459e214fa6083f936b430a762c629656216805ac0220396f550692cd347171cbc1ef1f51e15282e837bb2b30860dc77c8f78bc8501e503473044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e27034721026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac00000000"); + addTransactionTab("test1", "02000000000102ba4dc5a4a14bfaa941b7d115b379b5e15f960635cf694c178b9116763cbd63b11600000017160014fc164cbcac023f5eacfcead2d17d8768c41949affeffffff074d44d2856beb68ba52e8832da60a1682768c2421c2d9a8109ef4e66babd1fd1e000000171600148c3098be6b430859115f5ee99c84c368afecd048feffffff02305310000000000017a914ffaf369c2212b178c7a2c21c9ccdd5d126e74c4187327f0300000000001976a914a7cda2e06b102a143ab606937a01d152e300cd3e88ac02473044022006da0ca227f765179219e08a33026b94e7cacff77f87b8cd8eb1b46d6dda11d6022064faa7912924fd23406b6ed3328f1bbbc3760dc51109a49c1b38bf57029d304f012103c6a2fcd030270427d4abe1041c8af929a9e2dbab07b243673453847ab842ee1f024730440220786316a16095105a0af28dccac5cf80f449dea2ea810a9559a89ecb989c2cb3d02205cbd9913d1217ffec144ae4f2bd895f16d778c2ec49ae9c929fdc8bcc2a2b1db0121024d4985241609d072a59be6418d700e87688f6c4d99a51ad68e66078211f076ee38820900"); + } + + private void addTransactionTab(String name, String hex) { + try { + byte[] txbytes = Utils.hexToBytes(hex); + Transaction transaction = new Transaction(txbytes); + + Tab tab = new Tab(name); + FXMLLoader transactionLoader = new FXMLLoader(getClass().getResource("transaction.fxml")); + tab.setContent(transactionLoader.load()); + TransactionController controller = transactionLoader.getController(); + controller.setTransaction(transaction); + + tabs.getTabs().add(tab); + } catch(IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/src/main/java/com/craigraw/sparrow/EventManager.java b/src/main/java/com/craigraw/sparrow/EventManager.java new file mode 100644 index 00000000..d8764f77 --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/EventManager.java @@ -0,0 +1,31 @@ +package com.craigraw.sparrow; + +import com.craigraw.drongo.protocol.Transaction; + +import java.util.ArrayList; +import java.util.List; + +public class EventManager { + private List listenerList = new ArrayList<>(); + private static EventManager SINGLETON = new EventManager(); + + private EventManager() {} + + public void subscribe(TransactionListener listener) { + listenerList.add(listener); + } + + public void unsubscribe(TransactionListener listener) { + listenerList.remove(listener); + } + + public void notify(Transaction transaction) { + for (TransactionListener listener : listenerList) { + listener.updated(transaction); + } + } + + public static EventManager get() { + return SINGLETON; + } +} diff --git a/src/main/java/com/craigraw/sparrow/TransactionController.java b/src/main/java/com/craigraw/sparrow/TransactionController.java index c249621a..fb382424 100644 --- a/src/main/java/com/craigraw/sparrow/TransactionController.java +++ b/src/main/java/com/craigraw/sparrow/TransactionController.java @@ -2,46 +2,66 @@ package com.craigraw.sparrow; import com.craigraw.drongo.protocol.*; import com.craigraw.drongo.psbt.PSBT; +import com.craigraw.sparrow.form.*; import javafx.fxml.FXML; import javafx.fxml.Initializable; -import javafx.scene.control.TreeCell; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; import javafx.scene.control.cell.TextFieldTreeCell; -import javafx.util.Callback; +import javafx.scene.layout.Pane; import javafx.util.StringConverter; +import org.bouncycastle.util.encoders.Hex; +import org.fxmisc.richtext.CodeArea; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.net.URL; import java.util.ResourceBundle; -public class TransactionController implements Initializable { +public class TransactionController implements Initializable, TransactionListener { @FXML - private TreeView txtree; + private TreeView
txtree; + + @FXML + private Pane txpane; + + @FXML + private CodeArea txhex; private Transaction transaction; private PSBT psbt; @Override public void initialize(URL location, ResourceBundle resources) { + EventManager.get().subscribe(this); + } + private void initializeView() { + initializeTxTree(); + refreshTxHex(); } - private void initialiseTxTree() { - TreeItem rootItem = new TreeItem<>(transaction); + private void initializeTxTree() { + HeadersForm headersForm = new HeadersForm(transaction, psbt); + TreeItem rootItem = new TreeItem<>(headersForm); rootItem.setExpanded(true); - InputsPart inputsPart = new InputsPart(); - TreeItem inputsItem = new TreeItem(inputsPart); - for(TransactionInput input : transaction.getInputs()) { - TreeItem inputItem = new TreeItem<>(input); + InputsForm inputsForm = new InputsForm(transaction); + TreeItem inputsItem = new TreeItem<>(inputsForm); + inputsItem.setExpanded(true); + for(TransactionInput txInput : transaction.getInputs()) { + InputForm inputForm = new InputForm(txInput); + TreeItem inputItem = new TreeItem<>(inputForm); inputsItem.getChildren().add(inputItem); } - OutputsPart outputsPart = new OutputsPart(); - TreeItem outputsItem = new TreeItem(outputsPart); - for(TransactionOutput output : transaction.getOutputs()) { - TreeItem outputItem = new TreeItem<>(output); + OutputsForm outputsForm = new OutputsForm(transaction); + TreeItem outputsItem = new TreeItem<>(outputsForm); + outputsItem.setExpanded(true); + for(TransactionOutput txOutput : transaction.getOutputs()) { + OutputForm outputForm = new OutputForm(txOutput); + TreeItem outputItem = new TreeItem<>(outputForm); outputsItem.getChildren().add(outputItem); } @@ -49,66 +69,113 @@ public class TransactionController implements Initializable { rootItem.getChildren().add(outputsItem); txtree.setRoot(rootItem); - txtree.setCellFactory(new Callback, TreeCell>() { + txtree.setCellFactory(p -> new TextFieldTreeCell<>(new StringConverter(){ @Override - public TreeCell call(TreeView p) { - return new TextFieldTreeCell(new StringConverter(){ - - @Override - public String toString(TransactionPart part) { - if(part instanceof Transaction) { - Transaction transaction = (Transaction)part; - return "Tx " + transaction.getTxId().toString().substring(0, 6) + "..."; - } else if(part instanceof InputsPart) { - return "Inputs"; - } else if(part instanceof OutputsPart) { - return "Outputs"; - } else if(part instanceof TransactionInput) { - TransactionInput input = (TransactionInput)part; - return "Input #" + input.getIndex(); - } else if(part instanceof TransactionOutput) { - TransactionOutput output = (TransactionOutput)part; - return "Output #" + output.getIndex(); - } - - return part.toString(); - } - - @Override - public TransactionPart fromString(String string) { - throw new IllegalStateException("No fromString"); - } - }); + public String toString(Form form) { + return form.toString(); + } + + @Override + public Form fromString(String string) { + throw new IllegalStateException("No editing"); + } + })); + + txtree.getSelectionModel().selectedItemProperty().addListener((observable, old_val, new_val) -> { + Form form = new_val.getValue(); + + try { + txpane.getChildren().removeAll(); + txpane.getChildren().add(form.getContents()); + } catch (IOException e) { + throw new IllegalStateException("Can't find pane", e); } }); + + txtree.getSelectionModel().select(txtree.getRoot()); } - public void setPSBT(PSBT psbt) { - this.psbt = psbt; - this.transaction = psbt.getTransaction(); + void refreshTxHex() { + txhex.clear(); - initialiseTxTree(); - } + String hex = ""; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + transaction.bitcoinSerializeToStream(baos); + hex = Hex.toHexString(baos.toByteArray()); + } catch(IOException e) { + throw new IllegalStateException("Can't happen"); + } + + int cursor = 0; + + //Version + cursor = addText(hex, cursor, 8, "version"); - private static class InputsPart extends TransactionPart { - public InputsPart() { - super(new byte[0], 0); + if(transaction.hasWitnesses()) { + //Segwit marker + cursor = addText(hex, cursor, 2, "segwit-marker"); + //Segwit flag + cursor = addText(hex, cursor, 2, "segwit-flag"); } - @Override - protected void parse() throws ProtocolException { + //Number of inputs + cursor = addText(hex, cursor, 2, "num-inputs"); + //Inputs + int totalInputLength = 0; + for(TransactionInput input : transaction.getInputs()) { + totalInputLength += input.getLength(); } - } + cursor = addText(hex, cursor, totalInputLength*2, "inputs"); + + //Number of outputs + cursor = addText(hex, cursor, 2, "num-outputs"); - private static class OutputsPart extends TransactionPart { - public OutputsPart() { - super(new byte[0], 0); + //Outputs + int totalOutputLength = 0; + for(TransactionOutput output : transaction.getOutputs()) { + totalOutputLength += output.getLength(); } + cursor = addText(hex, cursor, totalOutputLength*2, "outputs"); - @Override - protected void parse() throws ProtocolException { + if(transaction.hasWitnesses()) { + int totalWitnessLength = 0; + for(TransactionInput input : transaction.getInputs()) { + totalWitnessLength += input.getWitness().getLength(); + } + cursor = addText(hex, cursor, totalWitnessLength*2, "witnesses"); + } + //Locktime + cursor = addText(hex, cursor, 8, "locktime"); + + if(cursor != hex.length()) { + throw new IllegalStateException("Cursor position does not match transaction serialisation " + cursor + ": " + hex.length()); } } + + private int addText(String hex, int cursor, int length, String description) { + txhex.append(hex.substring(cursor, cursor+=length), description + "-color"); + return cursor; + } + + public void setPSBT(PSBT psbt) { + this.psbt = psbt; + this.transaction = psbt.getTransaction(); + + initializeView(); + } + + public void setTransaction(Transaction transaction) { + this.transaction = transaction; + + initializeView(); + } + + @Override + public void updated(Transaction transaction) { + refreshTxHex(); + txtree.refresh(); + } } diff --git a/src/main/java/com/craigraw/sparrow/TransactionListener.java b/src/main/java/com/craigraw/sparrow/TransactionListener.java new file mode 100644 index 00000000..486bb545 --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/TransactionListener.java @@ -0,0 +1,7 @@ +package com.craigraw.sparrow; + +import com.craigraw.drongo.protocol.Transaction; + +public interface TransactionListener { + void updated(Transaction transaction); +} diff --git a/src/main/java/com/craigraw/sparrow/form/Form.java b/src/main/java/com/craigraw/sparrow/form/Form.java new file mode 100644 index 00000000..0d1f9705 --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/form/Form.java @@ -0,0 +1,9 @@ +package com.craigraw.sparrow.form; + +import javafx.scene.Node; + +import java.io.IOException; + +public abstract class Form { + public abstract Node getContents() throws IOException; +} diff --git a/src/main/java/com/craigraw/sparrow/form/HeadersController.java b/src/main/java/com/craigraw/sparrow/form/HeadersController.java new file mode 100644 index 00000000..abb18264 --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/form/HeadersController.java @@ -0,0 +1,157 @@ +package com.craigraw.sparrow.form; + +import com.craigraw.drongo.protocol.Transaction; +import com.craigraw.sparrow.EventManager; +import com.craigraw.sparrow.TransactionListener; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.*; +import javafx.scene.layout.GridPane; +import tornadofx.control.DateTimePicker; + +import java.net.URL; +import java.time.*; +import java.util.ResourceBundle; + +public class HeadersController implements Initializable, TransactionListener { + private HeadersForm headersForm; + + private static final long MAX_BLOCK_LOCKTIME = 500000000L; + + @FXML + private GridPane layout; + + @FXML + private TextField idField; + + @FXML + private Spinner versionField; + + @FXML + private Label segwitField; + + @FXML + private Label segwitVersionLabel; + + @FXML + private Spinner segwitVersionField; + + @FXML + private ToggleGroup locktimeTypeField; + + @FXML + private ToggleButton locktimeBlockTypeField; + + @FXML + private ToggleButton locktimeDateTypeField; + + @FXML + private Spinner locktimeBlockField; + + @FXML + private DateTimePicker locktimeDateField; + + @FXML + private Label feeField; + + @FXML + private Label sizeField; + + @FXML + private Label virtualSizeField; + + @Override + public void initialize(URL location, ResourceBundle resources) { + EventManager.get().subscribe(this); + } + + void setModel(HeadersForm form) { + this.headersForm = form; + Transaction tx = headersForm.getTransaction(); + + updateTxId(); + + versionField.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 2, (int)tx.getVersion())); + versionField.valueProperty().addListener((obs, oldValue, newValue) -> { + tx.setVersion(newValue); + EventManager.get().notify(tx); + }); + + segwitField.setText(tx.isSegwit() ? "Segwit" : "Legacy" ); + if(tx.isSegwit()) { + segwitVersionField.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 2, tx.getSegwitVersion())); + segwitVersionField.valueProperty().addListener((obs, oldValue, newValue) -> { + tx.setSegwitVersion(newValue); + EventManager.get().notify(tx); + }); + } else { + layout.getChildren().removeAll(segwitVersionLabel, segwitVersionField); + } + + locktimeTypeField.selectedToggleProperty().addListener((ov, old_toggle, new_toggle) -> { + if(locktimeTypeField.getSelectedToggle() != null) { + String selection = locktimeTypeField.getSelectedToggle().getUserData().toString(); + if(selection.equals("block")) { + locktimeBlockField.setVisible(true); + locktimeDateField.setVisible(false); + Integer block = locktimeBlockField.getValue(); + if(block != null) { + tx.setLockTime(block); + EventManager.get().notify(tx); + } + } else { + locktimeBlockField.setVisible(false); + locktimeDateField.setVisible(true); + LocalDateTime date = locktimeDateField.getDateTimeValue(); + if(date != null) { + tx.setLockTime(date.toEpochSecond(OffsetDateTime.now(ZoneId.systemDefault()).getOffset())); + EventManager.get().notify(tx); + } + } + } + }); + + if(tx.getLockTime() < MAX_BLOCK_LOCKTIME) { + locktimeBlockField.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)MAX_BLOCK_LOCKTIME-1, (int)tx.getLockTime())); + locktimeBlockField.setVisible(true); + locktimeDateField.setVisible(false); + locktimeTypeField.selectToggle(locktimeBlockTypeField); + } else { + LocalDateTime date = Instant.ofEpochSecond(tx.getLockTime()).atZone(ZoneId.systemDefault()).toLocalDateTime(); + locktimeDateField.setDateTimeValue(date); + locktimeBlockField.setVisible(false); + locktimeDateField.setVisible(true); + locktimeTypeField.selectToggle(locktimeDateTypeField); + } + + locktimeBlockField.valueProperty().addListener((obs, oldValue, newValue) -> { + tx.setLockTime(newValue); + EventManager.get().notify(tx); + }); + + locktimeDateField.setFormat("yyyy-MM-dd HH:mm:ss"); + locktimeDateField.dateTimeValueProperty().addListener((obs, oldValue, newValue) -> { + tx.setLockTime(newValue.toEpochSecond(OffsetDateTime.now(ZoneId.systemDefault()).getOffset())); + EventManager.get().notify(tx); + }); + + if(form.getPsbt() != null) { + feeField.setText(form.getPsbt().getFee().toString() + " sats"); + } else { + feeField.setText("Unknown"); + } + + sizeField.setText(tx.getSize() + " B"); + + virtualSizeField.setText(tx.getVirtualSize() + " vB"); + } + + private void updateTxId() { + idField.setText(headersForm.getTransaction().calculateTxId(false).toString()); + } + + @Override + public void updated(Transaction transaction) { + updateTxId(); + } +} diff --git a/src/main/java/com/craigraw/sparrow/form/HeadersForm.java b/src/main/java/com/craigraw/sparrow/form/HeadersForm.java new file mode 100644 index 00000000..37cbad4e --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/form/HeadersForm.java @@ -0,0 +1,38 @@ +package com.craigraw.sparrow.form; + +import com.craigraw.drongo.protocol.Transaction; +import com.craigraw.drongo.psbt.PSBT; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; + +import java.io.IOException; + +public class HeadersForm extends Form { + private Transaction transaction; + private PSBT psbt; + + public HeadersForm(Transaction transaction, PSBT psbt) { + this.transaction = transaction; + this.psbt = psbt; + } + + public Transaction getTransaction() { + return transaction; + } + + public PSBT getPsbt() { + return psbt; + } + + public Node getContents() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("headers.fxml")); + Node node = loader.load(); + HeadersController controller = loader.getController(); + controller.setModel(this); + return node; + } + + public String toString() { + return "Tx [" + transaction.calculateTxId(false).toString().substring(0, 6) + "]"; + } +} diff --git a/src/main/java/com/craigraw/sparrow/form/InputController.java b/src/main/java/com/craigraw/sparrow/form/InputController.java new file mode 100644 index 00000000..8f9c1f2a --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/form/InputController.java @@ -0,0 +1,19 @@ +package com.craigraw.sparrow.form; + +import javafx.fxml.Initializable; + +import java.net.URL; +import java.util.ResourceBundle; + +public class InputController implements Initializable { + private InputForm inputForm; + + @Override + public void initialize(URL location, ResourceBundle resources) { + + } + + public void setModel(InputForm form) { + this.inputForm = form; + } +} diff --git a/src/main/java/com/craigraw/sparrow/form/InputForm.java b/src/main/java/com/craigraw/sparrow/form/InputForm.java new file mode 100644 index 00000000..eb9a2f27 --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/form/InputForm.java @@ -0,0 +1,27 @@ +package com.craigraw.sparrow.form; + +import com.craigraw.drongo.protocol.TransactionInput; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; + +import java.io.IOException; + +public class InputForm extends Form { + private TransactionInput transactionInput; + + public InputForm(TransactionInput transactionInput) { + this.transactionInput = transactionInput; + } + + public Node getContents() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("input.fxml")); + Node node = loader.load(); + InputController controller = loader.getController(); + controller.setModel(this); + return node; + } + + public String toString() { + return "Input #" + transactionInput.getIndex(); + } +} diff --git a/src/main/java/com/craigraw/sparrow/form/InputsController.java b/src/main/java/com/craigraw/sparrow/form/InputsController.java new file mode 100644 index 00000000..a3a7b809 --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/form/InputsController.java @@ -0,0 +1,19 @@ +package com.craigraw.sparrow.form; + +import javafx.fxml.Initializable; + +import java.net.URL; +import java.util.ResourceBundle; + +public class InputsController implements Initializable { + private InputsForm inputsForm; + + @Override + public void initialize(URL location, ResourceBundle resources) { + + } + + public void setModel(InputsForm form) { + this.inputsForm = form; + } +} diff --git a/src/main/java/com/craigraw/sparrow/form/InputsForm.java b/src/main/java/com/craigraw/sparrow/form/InputsForm.java new file mode 100644 index 00000000..90c660eb --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/form/InputsForm.java @@ -0,0 +1,27 @@ +package com.craigraw.sparrow.form; + +import com.craigraw.drongo.protocol.Transaction; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; + +import java.io.IOException; + +public class InputsForm extends Form { + private Transaction transaction; + + public InputsForm(Transaction transaction) { + this.transaction = transaction; + } + + public Node getContents() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("inputs.fxml")); + Node node = loader.load(); + InputsController controller = loader.getController(); + controller.setModel(this); + return node; + } + + public String toString() { + return "Inputs"; + } +} diff --git a/src/main/java/com/craigraw/sparrow/form/OutputController.java b/src/main/java/com/craigraw/sparrow/form/OutputController.java new file mode 100644 index 00000000..177e4165 --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/form/OutputController.java @@ -0,0 +1,19 @@ +package com.craigraw.sparrow.form; + +import javafx.fxml.Initializable; + +import java.net.URL; +import java.util.ResourceBundle; + +public class OutputController implements Initializable { + private OutputForm outputForm; + + @Override + public void initialize(URL location, ResourceBundle resources) { + + } + + public void setModel(OutputForm form) { + this.outputForm = form; + } +} diff --git a/src/main/java/com/craigraw/sparrow/form/OutputForm.java b/src/main/java/com/craigraw/sparrow/form/OutputForm.java new file mode 100644 index 00000000..cd367eac --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/form/OutputForm.java @@ -0,0 +1,27 @@ +package com.craigraw.sparrow.form; + +import com.craigraw.drongo.protocol.TransactionOutput; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; + +import java.io.IOException; + +public class OutputForm extends Form { + private TransactionOutput transactionOutput; + + public OutputForm(TransactionOutput transactionOutput) { + this.transactionOutput = transactionOutput; + } + + public Node getContents() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("output.fxml")); + Node node = loader.load(); + OutputController controller = loader.getController(); + controller.setModel(this); + return node; + } + + public String toString() { + return "Output #" + transactionOutput.getIndex(); + } +} diff --git a/src/main/java/com/craigraw/sparrow/form/OutputsController.java b/src/main/java/com/craigraw/sparrow/form/OutputsController.java new file mode 100644 index 00000000..a3ff71ee --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/form/OutputsController.java @@ -0,0 +1,19 @@ +package com.craigraw.sparrow.form; + +import javafx.fxml.Initializable; + +import java.net.URL; +import java.util.ResourceBundle; + +public class OutputsController implements Initializable { + private OutputsForm outputsForm; + + @Override + public void initialize(URL location, ResourceBundle resources) { + + } + + public void setModel(OutputsForm form) { + this.outputsForm = form; + } +} diff --git a/src/main/java/com/craigraw/sparrow/form/OutputsForm.java b/src/main/java/com/craigraw/sparrow/form/OutputsForm.java new file mode 100644 index 00000000..fe85b0d1 --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/form/OutputsForm.java @@ -0,0 +1,27 @@ +package com.craigraw.sparrow.form; + +import com.craigraw.drongo.protocol.Transaction; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; + +import java.io.IOException; + +public class OutputsForm extends Form { + private Transaction transaction; + + public OutputsForm(Transaction transaction) { + this.transaction = transaction; + } + + public Node getContents() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("outputs.fxml")); + Node node = loader.load(); + OutputsController controller = loader.getController(); + controller.setModel(this); + return node; + } + + public String toString() { + return "Outputs"; + } +} diff --git a/src/main/java/com/craigraw/sparrow/form/PartialInputController.java b/src/main/java/com/craigraw/sparrow/form/PartialInputController.java new file mode 100644 index 00000000..f04eb32a --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/form/PartialInputController.java @@ -0,0 +1,19 @@ +package com.craigraw.sparrow.form; + +import javafx.fxml.Initializable; + +import java.net.URL; +import java.util.ResourceBundle; + +public class PartialInputController implements Initializable { + private PartialInputForm partialInputForm; + + @Override + public void initialize(URL location, ResourceBundle resources) { + + } + + public void setModel(PartialInputForm form) { + this.partialInputForm = form; + } +} diff --git a/src/main/java/com/craigraw/sparrow/form/PartialInputForm.java b/src/main/java/com/craigraw/sparrow/form/PartialInputForm.java new file mode 100644 index 00000000..b3ff03f8 --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/form/PartialInputForm.java @@ -0,0 +1,23 @@ +package com.craigraw.sparrow.form; + +import com.craigraw.drongo.psbt.PSBTInput; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; + +import java.io.IOException; + +public class PartialInputForm extends Form { + private PSBTInput psbtInput; + + public PartialInputForm(PSBTInput psbtInput) { + this.psbtInput = psbtInput; + } + + public Node getContents() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("partialinput.fxml")); + Node node = loader.load(); + PartialInputController controller = loader.getController(); + controller.setModel(this); + return node; + } +} diff --git a/src/main/java/com/craigraw/sparrow/form/PartialOutputController.java b/src/main/java/com/craigraw/sparrow/form/PartialOutputController.java new file mode 100644 index 00000000..8410e550 --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/form/PartialOutputController.java @@ -0,0 +1,19 @@ +package com.craigraw.sparrow.form; + +import javafx.fxml.Initializable; + +import java.net.URL; +import java.util.ResourceBundle; + +public class PartialOutputController implements Initializable { + private PartialOutputForm partialOutputForm; + + @Override + public void initialize(URL location, ResourceBundle resources) { + + } + + public void setModel(PartialOutputForm form) { + this.partialOutputForm = form; + } +} diff --git a/src/main/java/com/craigraw/sparrow/form/PartialOutputForm.java b/src/main/java/com/craigraw/sparrow/form/PartialOutputForm.java new file mode 100644 index 00000000..6d013915 --- /dev/null +++ b/src/main/java/com/craigraw/sparrow/form/PartialOutputForm.java @@ -0,0 +1,23 @@ +package com.craigraw.sparrow.form; + +import com.craigraw.drongo.psbt.PSBTOutput; +import javafx.fxml.FXMLLoader; +import javafx.scene.Node; + +import java.io.IOException; + +public class PartialOutputForm extends Form { + private PSBTOutput psbtOutput; + + public PartialOutputForm(PSBTOutput psbtOutput) { + this.psbtOutput = psbtOutput; + } + + public Node getContents() throws IOException { + FXMLLoader loader = new FXMLLoader(getClass().getResource("partialoutput.fxml")); + Node node = loader.load(); + PartialOutputController controller = loader.getController(); + controller.setModel(this); + return node; + } +} diff --git a/src/main/resources/com/craigraw/sparrow/app.fxml b/src/main/resources/com/craigraw/sparrow/app.fxml index f8367886..3ba429ed 100644 --- a/src/main/resources/com/craigraw/sparrow/app.fxml +++ b/src/main/resources/com/craigraw/sparrow/app.fxml @@ -3,7 +3,7 @@ - + diff --git a/src/main/resources/com/craigraw/sparrow/form/headers.fxml b/src/main/resources/com/craigraw/sparrow/form/headers.fxml new file mode 100644 index 00000000..de1e33ca --- /dev/null +++ b/src/main/resources/com/craigraw/sparrow/form/headers.fxml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/com/craigraw/sparrow/form/input.fxml b/src/main/resources/com/craigraw/sparrow/form/input.fxml new file mode 100644 index 00000000..95474a90 --- /dev/null +++ b/src/main/resources/com/craigraw/sparrow/form/input.fxml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/src/main/resources/com/craigraw/sparrow/form/inputs.fxml b/src/main/resources/com/craigraw/sparrow/form/inputs.fxml new file mode 100644 index 00000000..3b6038a2 --- /dev/null +++ b/src/main/resources/com/craigraw/sparrow/form/inputs.fxml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/src/main/resources/com/craigraw/sparrow/form/output.fxml b/src/main/resources/com/craigraw/sparrow/form/output.fxml new file mode 100644 index 00000000..bc32f4be --- /dev/null +++ b/src/main/resources/com/craigraw/sparrow/form/output.fxml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/src/main/resources/com/craigraw/sparrow/form/outputs.fxml b/src/main/resources/com/craigraw/sparrow/form/outputs.fxml new file mode 100644 index 00000000..38e6c281 --- /dev/null +++ b/src/main/resources/com/craigraw/sparrow/form/outputs.fxml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/src/main/resources/com/craigraw/sparrow/form/partialinput.fxml b/src/main/resources/com/craigraw/sparrow/form/partialinput.fxml new file mode 100644 index 00000000..49e122c9 --- /dev/null +++ b/src/main/resources/com/craigraw/sparrow/form/partialinput.fxml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/src/main/resources/com/craigraw/sparrow/form/partialoutput.fxml b/src/main/resources/com/craigraw/sparrow/form/partialoutput.fxml new file mode 100644 index 00000000..e9fe2753 --- /dev/null +++ b/src/main/resources/com/craigraw/sparrow/form/partialoutput.fxml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/src/main/resources/com/craigraw/sparrow/general.css b/src/main/resources/com/craigraw/sparrow/general.css new file mode 100644 index 00000000..89d0be84 --- /dev/null +++ b/src/main/resources/com/craigraw/sparrow/general.css @@ -0,0 +1,6 @@ +.copyable-label, .copyable-label:focused { + -fx-background-color: transparent ; + -fx-background-insets: 0px ; + -fx-border :none; + -fx-padding: 0; +} \ No newline at end of file diff --git a/src/main/resources/com/craigraw/sparrow/transaction.css b/src/main/resources/com/craigraw/sparrow/transaction.css new file mode 100644 index 00000000..b85f39f1 --- /dev/null +++ b/src/main/resources/com/craigraw/sparrow/transaction.css @@ -0,0 +1,27 @@ +#txhex { + -fx-background-color: #ffffff; + -fx-font: 14px Courier; + -fx-padding: 2; +} + +.version-color { -fx-fill: #986801 } +.segwit-marker-color { -fx-fill: #000000 } +.segwit-flag-color { -fx-fill: #4078f2 } +.num-inputs-color { -fx-fill: #ca1243 } +.inputs-color { -fx-fill: #0184bc } +.num-outputs-color { -fx-fill: #ca1243 } +.outputs-color { -fx-fill: #50a14f } +.witnesses-color { -fx-fill: #a626a4 } +.locktime-color { -fx-fill: #986801 } + +.color-0 { -fx-fill: #ca1243 } +.color-1 { -fx-fill: #d75f00 } +.color-2 { -fx-fill: #c18401 } +.color-3 { -fx-fill: #50a14f } +.color-4 { -fx-fill: #0184bc } +.color-5 { -fx-fill: #4078f2 } +.color-6 { -fx-fill: #a626a4 } +.color-7 { -fx-fill: #986801 } +.color-8 { -fx-fill: #000000 } + +.color-grey { -fx-fill: #e5e5e6 } \ No newline at end of file diff --git a/src/main/resources/com/craigraw/sparrow/transaction.fxml b/src/main/resources/com/craigraw/sparrow/transaction.fxml index 03b39d42..5d4d606b 100644 --- a/src/main/resources/com/craigraw/sparrow/transaction.fxml +++ b/src/main/resources/com/craigraw/sparrow/transaction.fxml @@ -6,23 +6,16 @@ - + + + - + - - - - - - - - - - + -