psbt combining, transaction viewer updating

This commit is contained in:
Craig Raw 2020-07-24 14:47:43 +02:00
parent b0f4d3b4c9
commit ef5124a9b9
23 changed files with 333 additions and 144 deletions

2
drongo

@ -1 +1 @@
Subproject commit 70d0b7afcd6f0575bf424a45f54dd6e039057dba Subproject commit 0466755883c19a9e679f5e937b2f7db55a73e05b

View file

@ -703,7 +703,15 @@ public class AppController implements Initializable {
TabData tabData = (TabData)tab.getUserData(); TabData tabData = (TabData)tab.getUserData();
if(tabData instanceof TransactionTabData) { if(tabData instanceof TransactionTabData) {
TransactionTabData transactionTabData = (TransactionTabData)tabData; TransactionTabData transactionTabData = (TransactionTabData)tabData;
if(transactionTabData.getTransaction().getTxId().equals(transaction.getTxId())) {
//If an exact match bytewise of an existing tab, return that tab
if(Arrays.equals(transactionTabData.getTransaction().bitcoinSerialize(), transaction.bitcoinSerialize())) {
//As per BIP174, combine PSBTs with matching transactions
if(transactionTabData.getPsbt() != null && psbt != null) {
transactionTabData.getPsbt().combine(psbt);
EventManager.get().post(new PSBTCombinedEvent(transactionTabData.getPsbt()));
}
return tab; return tab;
} }
} }
@ -717,7 +725,7 @@ public class AppController implements Initializable {
} }
Tab tab = new Tab(tabName); Tab tab = new Tab(tabName);
TabData tabData = new TransactionTabData(TabData.TabType.TRANSACTION, transaction); TabData tabData = new TransactionTabData(TabData.TabType.TRANSACTION, transaction, psbt);
tab.setUserData(tabData); tab.setUserData(tabData);
tab.setContextMenu(getTabContextMenu(tab)); tab.setContextMenu(getTabContextMenu(tab));
tab.setClosable(true); tab.setClosable(true);
@ -733,6 +741,8 @@ public class AppController implements Initializable {
controller.setTransaction(transaction); controller.setTransaction(transaction);
} }
controller.setName(name);
if(initialView != null) { if(initialView != null) {
controller.setInitialView(initialView, initialIndex); controller.setInitialView(initialView, initialIndex);
} }
@ -919,4 +929,9 @@ public class AppController implements Initializable {
public void requestWalletOpen(RequestWalletOpenEvent event) { public void requestWalletOpen(RequestWalletOpenEvent event) {
openWallet(null); openWallet(null);
} }
@Subscribe
public void requestTransactionOpen(RequestTransactionOpenEvent event) {
openTransactionFromFile(null);
}
} }

View file

@ -1,108 +1,37 @@
package com.sparrowwallet.sparrow; package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptChunk; import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.sparrow.transaction.ScriptContextMenu; import com.sparrowwallet.sparrow.control.ScriptArea;
import javafx.geometry.Point2D; import javafx.geometry.Point2D;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.stage.Popup; import javafx.stage.Popup;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.event.MouseOverTextEvent; import org.fxmisc.richtext.event.MouseOverTextEvent;
import org.fxmisc.richtext.model.TwoDimensional; import org.fxmisc.richtext.model.TwoDimensional;
import java.time.Duration; import java.time.Duration;
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
import static com.sparrowwallet.drongo.protocol.ScriptType.P2WSH;
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward; import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward;
public abstract class BaseController { public abstract class BaseController {
protected void appendScript(CodeArea codeArea, Script script) { protected void initializeScriptField(ScriptArea scriptArea) {
appendScript(codeArea, script, null, null);
}
protected void appendScript(CodeArea codeArea, Script script, Script redeemScript, Script witnessScript) {
if(P2PKH.isScriptType(script)) {
codeArea.append(script.getChunks().get(0).toString(), "script-opcode");
codeArea.append(" ", "");
codeArea.append(script.getChunks().get(1).toString(), "script-opcode");
codeArea.append(" ", "");
codeArea.append("<pkh>", "script-hash");
codeArea.append(" ", "");
codeArea.append(script.getChunks().get(3).toString(), "script-opcode");
codeArea.append(" ", "");
codeArea.append(script.getChunks().get(4).toString(), "script-opcode");
} else if(P2SH.isScriptType(script)) {
codeArea.append(script.getChunks().get(0).toString(), "script-opcode");
codeArea.append(" ", "");
codeArea.append("<sh>", "script-hash");
codeArea.append(" ", "");
codeArea.append(script.getChunks().get(2).toString(), "script-opcode");
} else if(P2WPKH.isScriptType(script)) {
codeArea.append(script.getChunks().get(0).toString(), "script-opcode");
codeArea.append(" ", "");
codeArea.append("<wpkh>", "script-hash");
} else if(P2WSH.isScriptType(script)) {
codeArea.append(script.getChunks().get(0).toString(), "script-opcode");
codeArea.append(" ", "");
codeArea.append("<wsh>", "script-hash");
} else {
int signatureCount = 1;
int pubKeyCount = 1;
for (int i = 0; i < script.getChunks().size(); i++) {
ScriptChunk chunk = script.getChunks().get(i);
if(chunk.isOpCode()) {
codeArea.append(chunk.toString(), "script-opcode");
} else if(chunk.isSignature()) {
codeArea.append("<signature" + signatureCount++ + ">", "script-signature");
} else if(chunk.isPubKey()) {
codeArea.append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
} else if(chunk.isScript()) {
Script nestedScript = chunk.getScript();
if (nestedScript.equals(redeemScript)) {
codeArea.append("<RedeemScript>", "script-redeem");
} else if (nestedScript.equals(witnessScript)) {
codeArea.append("<WitnessScript>", "script-redeem");
} else {
codeArea.append("(", "script-nest");
appendScript(codeArea, nestedScript);
codeArea.append(")", "script-nest");
}
} else {
codeArea.append(chunk.toString(), "script-other");
}
if(i < script.getChunks().size() - 1) {
codeArea.append(" ", "");
}
}
}
addScriptPopup(codeArea, script);
}
protected void addScriptPopup(CodeArea area, Script script) {
ScriptContextMenu contextMenu = new ScriptContextMenu(area, script);
area.setContextMenu(contextMenu);
Popup popup = new Popup(); Popup popup = new Popup();
Label popupMsg = new Label(); Label popupMsg = new Label();
popupMsg.getStyleClass().add("tooltip"); popupMsg.getStyleClass().add("tooltip");
popup.getContent().add(popupMsg); popup.getContent().add(popupMsg);
area.setMouseOverTextDelay(Duration.ofMillis(150)); scriptArea.setMouseOverTextDelay(Duration.ofMillis(150));
area.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> { scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
TwoDimensional.Position position = area.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward); TwoDimensional.Position position = scriptArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
if(position.getMajor() % 2 == 0) { if(position.getMajor() % 2 == 0) {
ScriptChunk hoverChunk = script.getChunks().get(position.getMajor()/2); ScriptChunk hoverChunk = scriptArea.getScript().getChunks().get(position.getMajor()/2);
if(!hoverChunk.isOpCode()) { if(!hoverChunk.isOpCode()) {
Point2D pos = e.getScreenPosition(); Point2D pos = e.getScreenPosition();
popupMsg.setText(describeScriptChunk(hoverChunk)); popupMsg.setText(describeScriptChunk(hoverChunk));
popup.show(area, pos.getX(), pos.getY() + 10); popup.show(scriptArea, pos.getX(), pos.getY() + 10);
} }
} }
}); });
area.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> { scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> {
popup.hide(); popup.hide();
}); });
} }

View file

@ -1,16 +1,27 @@
package com.sparrowwallet.sparrow; package com.sparrowwallet.sparrow;
import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
public class TransactionTabData extends TabData { public class TransactionTabData extends TabData {
private Transaction transaction; private final Transaction transaction;
private final PSBT psbt;
public TransactionTabData(TabType type, Transaction transaction) { public TransactionTabData(TabType type, Transaction transaction) {
this(type, transaction, null);
}
public TransactionTabData(TabType type, Transaction transaction, PSBT psbt) {
super(type); super(type);
this.transaction = transaction; this.transaction = transaction;
this.psbt = psbt;
} }
public Transaction getTransaction() { public Transaction getTransaction() {
return transaction; return transaction;
} }
public PSBT getPsbt() {
return psbt;
}
} }

View file

@ -0,0 +1,99 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptChunk;
import com.sparrowwallet.sparrow.transaction.ScriptContextMenu;
import javafx.geometry.Pos;
import org.controlsfx.control.decoration.Decorator;
import org.controlsfx.control.decoration.GraphicDecoration;
import org.fxmisc.richtext.CodeArea;
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
public class ScriptArea extends CodeArea {
private Script script;
public void appendScript(Script script) {
appendScript(script, null, null);
}
public void appendScript(Script script, Script redeemScript, Script witnessScript) {
if(this.script == null) {
this.script = script;
ScriptContextMenu contextMenu = new ScriptContextMenu(this, script);
setContextMenu(contextMenu);
}
if(P2PKH.isScriptType(script)) {
append(script.getChunks().get(0).toString(), "script-opcode");
append(" ", "");
append(script.getChunks().get(1).toString(), "script-opcode");
append(" ", "");
append("<pkh>", "script-hash");
append(" ", "");
append(script.getChunks().get(3).toString(), "script-opcode");
append(" ", "");
append(script.getChunks().get(4).toString(), "script-opcode");
} else if(P2SH.isScriptType(script)) {
append(script.getChunks().get(0).toString(), "script-opcode");
append(" ", "");
append("<sh>", "script-hash");
append(" ", "");
append(script.getChunks().get(2).toString(), "script-opcode");
} else if(P2WPKH.isScriptType(script)) {
append(script.getChunks().get(0).toString(), "script-opcode");
append(" ", "");
append("<wpkh>", "script-hash");
} else if(P2WSH.isScriptType(script)) {
append(script.getChunks().get(0).toString(), "script-opcode");
append(" ", "");
append("<wsh>", "script-hash");
} else {
int signatureCount = 1;
int pubKeyCount = 1;
for (int i = 0; i < script.getChunks().size(); i++) {
ScriptChunk chunk = script.getChunks().get(i);
if(chunk.isOpCode()) {
append(chunk.toString(), "script-opcode");
} else if(chunk.isSignature()) {
append("<signature" + signatureCount++ + ">", "script-signature");
} else if(chunk.isPubKey()) {
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
} else if(chunk.isScript()) {
Script nestedScript = chunk.getScript();
if (nestedScript.equals(redeemScript)) {
append("<RedeemScript>", "script-redeem");
} else if(nestedScript.equals(witnessScript)) {
append("<WitnessScript>", "script-redeem");
} else {
append("(", "script-nest");
appendScript(nestedScript);
append(")", "script-nest");
}
} else {
append(chunk.toString(), "script-other");
}
if(i < script.getChunks().size() - 1) {
append(" ", "");
}
}
}
}
public void clear() {
super.clear();
this.script = null;
setDisable(false);
setContextMenu(null);
Decorator.removeAllDecorations(this);
}
public void addPSBTDecoration(String description, String styleClass) {
Decorator.addDecoration(this, new GraphicDecoration(new TextDecoration("PSBT", description, styleClass), Pos.TOP_RIGHT));
}
public Script getScript() {
return script;
}
}

View file

@ -0,0 +1,9 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.psbt.PSBT;
public class PSBTCombinedEvent extends PSBTEvent {
public PSBTCombinedEvent(PSBT psbt) {
super(psbt);
}
}

View file

@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.psbt.PSBT;
public class PSBTEvent {
private final PSBT psbt;
public PSBTEvent(PSBT psbt) {
this.psbt = psbt;
}
public PSBT getPsbt() {
return psbt;
}
}

View file

@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.event;
public class RequestTransactionOpenEvent {
//Empty event class used to request the transaction open dialog
}

View file

@ -15,6 +15,8 @@ public class FontAwesome5 extends GlyphFont {
* The individual glyphs offered by the FontAwesome5 font. * The individual glyphs offered by the FontAwesome5 font.
*/ */
public static enum Glyph implements INamedCharacter { public static enum Glyph implements INamedCharacter {
ARROW_UP('\uf062'),
CAMERA('\uf030'),
CHECK_CIRCLE('\uf058'), CHECK_CIRCLE('\uf058'),
CIRCLE('\uf111'), CIRCLE('\uf111'),
COINS('\uf51e'), COINS('\uf51e'),

View file

@ -21,6 +21,8 @@ import javafx.fxml.Initializable;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.input.Clipboard; import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent; import javafx.scene.input.ClipboardContent;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.glyphfont.Glyph; import org.controlsfx.glyphfont.Glyph;
import tornadofx.control.DateTimePicker; import tornadofx.control.DateTimePicker;
import tornadofx.control.Field; import tornadofx.control.Field;
@ -29,7 +31,10 @@ import com.google.common.eventbus.Subscribe;
import tornadofx.control.Form; import tornadofx.control.Form;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.*; import java.time.*;
import java.util.*; import java.util.*;
@ -434,11 +439,47 @@ public class HeadersController extends TransactionFormController implements Init
} }
public void showPSBT(ActionEvent event) { public void showPSBT(ActionEvent event) {
ToggleButton toggleButton = (ToggleButton)event.getSource();
toggleButton.setSelected(false);
headersForm.getSignedKeystores().add(headersForm.getSigningWallet().getKeystores().get(0)); headersForm.getSignedKeystores().add(headersForm.getSigningWallet().getKeystores().get(0));
} }
public void savePSBT(ActionEvent event) { public void scanPSBT(ActionEvent event) {
ToggleButton toggleButton = (ToggleButton)event.getSource();
toggleButton.setSelected(false);
}
public void savePSBT(ActionEvent event) {
ToggleButton toggleButton = (ToggleButton)event.getSource();
toggleButton.setSelected(false);
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save PSBT");
if(headersForm.getName() != null && !headersForm.getName().isEmpty()) {
fileChooser.setInitialFileName(headersForm.getName() + ".psbt");
}
File file = fileChooser.showSaveDialog(window);
if(file != null) {
try {
try(PrintWriter writer = new PrintWriter(file, StandardCharsets.UTF_8)) {
writer.print(headersForm.getPsbt().toBase64String());
}
} catch(IOException e) {
AppController.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath());
}
}
}
public void loadPSBT(ActionEvent event) {
ToggleButton toggleButton = (ToggleButton)event.getSource();
toggleButton.setSelected(false);
EventManager.get().post(new RequestTransactionOpenEvent());
} }
public void signPSBT(ActionEvent event) { public void signPSBT(ActionEvent event) {
@ -570,4 +611,11 @@ public class HeadersController extends TransactionFormController implements Init
signaturesForm.setVisible(true); signaturesForm.setVisible(true);
} }
} }
@Subscribe
public void psbtCombined(PSBTCombinedEvent event) {
if(event.getPsbt().equals(headersForm.getPsbt()) && headersForm.getSigningWallet() != null) {
updateSignedKeystores(headersForm.getSigningWallet());
}
}
} }

View file

@ -14,17 +14,12 @@ import com.sparrowwallet.sparrow.event.*;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.Button;
import org.controlsfx.control.ToggleSwitch; import org.controlsfx.control.ToggleSwitch;
import org.controlsfx.control.decoration.Decorator; import org.fxmisc.flowless.VirtualizedScrollPane;
import org.controlsfx.control.decoration.GraphicDecoration;
import org.fxmisc.richtext.CodeArea; import org.fxmisc.richtext.CodeArea;
import tornadofx.control.Field; import tornadofx.control.Field;
import tornadofx.control.Fieldset; import tornadofx.control.Fieldset;
import org.fxmisc.flowless.VirtualizedScrollPane;
import java.net.URL; import java.net.URL;
import java.time.Duration; import java.time.Duration;
@ -60,25 +55,25 @@ public class InputController extends TransactionFormController implements Initia
private AddressLabel address; private AddressLabel address;
@FXML @FXML
private CodeArea scriptSigArea; private ScriptArea scriptSigArea;
@FXML @FXML
private VirtualizedScrollPane<CodeArea> redeemScriptScroll; private VirtualizedScrollPane<CodeArea> redeemScriptScroll;
@FXML @FXML
private CodeArea redeemScriptArea; private ScriptArea redeemScriptArea;
@FXML @FXML
private VirtualizedScrollPane<CodeArea> witnessScriptScroll; private VirtualizedScrollPane<CodeArea> witnessScriptScroll;
@FXML @FXML
private CodeArea witnessScriptArea; private ScriptArea witnessScriptArea;
@FXML @FXML
private VirtualizedScrollPane<CodeArea> witnessesScroll; private VirtualizedScrollPane<CodeArea> witnessesScroll;
@FXML @FXML
private CodeArea witnessesArea; private ScriptArea witnessesArea;
@FXML @FXML
private CopyableLabel signatures; private CopyableLabel signatures;
@ -130,12 +125,12 @@ public class InputController extends TransactionFormController implements Initia
initializeInputFields(txInput, psbtInput); initializeInputFields(txInput, psbtInput);
initializeScriptFields(txInput, psbtInput); initializeScriptFields(txInput, psbtInput);
initializeStatusFields(txInput); initializeStatusFields(txInput, psbtInput);
initializeLocktimeFields(txInput); initializeLocktimeFields(txInput);
if(psbtInput != null) { if(psbtInput != null) {
inputForm.getSignedKeystores().addListener((ListChangeListener<Keystore>) c -> { inputForm.getSignedKeystores().addListener((ListChangeListener<Keystore>) c -> {
updateSignatures(); updateSignatures(inputForm.getPsbtInput());
}); });
} }
} }
@ -223,6 +218,20 @@ public class InputController extends TransactionFormController implements Initia
} }
private void initializeScriptFields(TransactionInput txInput, PSBTInput psbtInput) { private void initializeScriptFields(TransactionInput txInput, PSBTInput psbtInput) {
initializeScriptField(scriptSigArea);
initializeScriptField(redeemScriptArea);
initializeScriptField(witnessesArea);
initializeScriptField(witnessScriptArea);
updateScriptFields(txInput, psbtInput);
}
private void updateScriptFields(TransactionInput txInput, PSBTInput psbtInput) {
scriptSigArea.clear();
redeemScriptArea.clear();
witnessesArea.clear();
witnessScriptArea.clear();
//TODO: While we immediately check if the referenced transaction output is P2SH, where this is not present getting the first nested script is not safe //TODO: While we immediately check if the referenced transaction output is P2SH, where this is not present getting the first nested script is not safe
Script redeemScript = txInput.getScriptSig().getFirstNestedScript(); Script redeemScript = txInput.getScriptSig().getFirstNestedScript();
if(redeemScript != null && inputForm.getReferencedTransactionOutput() != null) { if(redeemScript != null && inputForm.getReferencedTransactionOutput() != null) {
@ -233,31 +242,27 @@ public class InputController extends TransactionFormController implements Initia
} }
if(redeemScript == null && psbtInput != null && psbtInput.getRedeemScript() != null) { if(redeemScript == null && psbtInput != null && psbtInput.getRedeemScript() != null) {
addPSBTDecoration(redeemScriptArea, "PSBT Redeem Script", "non-final"); redeemScriptArea.addPSBTDecoration("PSBT Redeem Script", "non-final");
redeemScript = psbtInput.getRedeemScript(); redeemScript = psbtInput.getRedeemScript();
} }
if(redeemScript == null && psbtInput != null && psbtInput.getFinalScriptSig() != null) { if(redeemScript == null && psbtInput != null && psbtInput.getFinalScriptSig() != null) {
addPSBTDecoration(redeemScriptArea, "PSBT Final ScriptSig", "final"); redeemScriptArea.addPSBTDecoration("PSBT Final ScriptSig", "final");
redeemScript = psbtInput.getFinalScriptSig().getFirstNestedScript(); redeemScript = psbtInput.getFinalScriptSig().getFirstNestedScript();
} }
scriptSigArea.clear();
if(txInput.getScriptSig().isEmpty() && psbtInput != null && psbtInput.getFinalScriptSig() != null) { if(txInput.getScriptSig().isEmpty() && psbtInput != null && psbtInput.getFinalScriptSig() != null) {
appendScript(scriptSigArea, psbtInput.getFinalScriptSig(), redeemScript, null); scriptSigArea.appendScript(psbtInput.getFinalScriptSig(), redeemScript, null);
addPSBTDecoration(scriptSigArea, "PSBT Final ScriptSig", "final"); scriptSigArea.addPSBTDecoration("PSBT Final ScriptSig", "final");
} else { } else {
appendScript(scriptSigArea, txInput.getScriptSig(), redeemScript, null); scriptSigArea.appendScript(txInput.getScriptSig(), redeemScript, null);
} }
redeemScriptArea.clear();
if(redeemScript != null) { if(redeemScript != null) {
appendScript(redeemScriptArea, redeemScript); redeemScriptArea.appendScript(redeemScript);
} else { } else {
redeemScriptScroll.setDisable(true); redeemScriptScroll.setDisable(true);
} }
witnessesArea.clear();
witnessScriptArea.clear();
Script witnesses = null; Script witnesses = null;
Script witnessScript = null; Script witnessScript = null;
@ -268,36 +273,31 @@ public class InputController extends TransactionFormController implements Initia
if(psbtInput.getFinalScriptWitness() != null) { if(psbtInput.getFinalScriptWitness() != null) {
witnesses = new Script(psbtInput.getFinalScriptWitness().asScriptChunks()); witnesses = new Script(psbtInput.getFinalScriptWitness().asScriptChunks());
witnessScript = psbtInput.getFinalScriptWitness().getWitnessScript(); witnessScript = psbtInput.getFinalScriptWitness().getWitnessScript();
addPSBTDecoration(witnessesArea, "PSBT Final ScriptWitness", "final"); witnessesArea.addPSBTDecoration("PSBT Final ScriptWitness", "final");
addPSBTDecoration(witnessScriptArea, "PSBT Final ScriptWitness", "final"); witnessScriptArea.addPSBTDecoration("PSBT Final ScriptWitness", "final");
} else if(psbtInput.getWitnessScript() != null) { } else if(psbtInput.getWitnessScript() != null) {
witnessScript = psbtInput.getWitnessScript(); witnessScript = psbtInput.getWitnessScript();
addPSBTDecoration(witnessScriptArea, "PSBT Witness Script", "non-final"); witnessScriptArea.addPSBTDecoration("PSBT Witness Script", "non-final");
} }
} }
if(witnesses != null) { if(witnesses != null) {
appendScript(witnessesArea, witnesses, null, witnessScript); witnessesArea.appendScript(witnesses, null, witnessScript);
} else { } else {
witnessesScroll.setDisable(true); witnessesScroll.setDisable(true);
} }
if(witnessScript != null) { if(witnessScript != null) {
appendScript(witnessScriptArea, witnessScript); witnessScriptArea.appendScript(witnessScript);
} else { } else {
witnessScriptScroll.setDisable(true); witnessScriptScroll.setDisable(true);
} }
} }
private void addPSBTDecoration(Node target, String description, String styleClass) { private void initializeStatusFields(TransactionInput txInput, PSBTInput psbtInput) {
Decorator.addDecoration(target, new GraphicDecoration(new TextDecoration("PSBT", description, styleClass), Pos.TOP_RIGHT)); updateSignatures(psbtInput);
}
private void initializeStatusFields(TransactionInput txInput) {
Transaction transaction = inputForm.getTransaction(); Transaction transaction = inputForm.getTransaction();
updateSignatures();
rbf.setSelected(txInput.isReplaceByFeeEnabled()); rbf.setSelected(txInput.isReplaceByFeeEnabled());
rbf.selectedProperty().addListener((observable, oldValue, newValue) -> { rbf.selectedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue) { if(newValue) {
@ -323,11 +323,9 @@ public class InputController extends TransactionFormController implements Initia
rbf.setDisable(!inputForm.isEditable()); rbf.setDisable(!inputForm.isEditable());
} }
private void updateSignatures() { private void updateSignatures(PSBTInput psbtInput) {
signatures.setText("Unknown"); signatures.setText("Unknown");
if(inputForm.getPsbtInput() != null) { if(inputForm.getPsbtInput() != null) {
PSBTInput psbtInput = inputForm.getPsbtInput();
int reqSigs = -1; int reqSigs = -1;
if(psbtInput.getUtxo() != null && psbtInput.getSigningScript() != null) { if(psbtInput.getUtxo() != null && psbtInput.getSigningScript() != null) {
try { try {
@ -519,4 +517,13 @@ public class InputController extends TransactionFormController implements Initia
locktimeRelativeCombo.setDisable(true); locktimeRelativeCombo.setDisable(true);
} }
} }
@Subscribe
public void psbtCombined(PSBTCombinedEvent event) {
if(event.getPsbt().equals(inputForm.getPsbt())) {
updateSpends(inputForm.getPsbtInput().getUtxo());
updateScriptFields(inputForm.getTransactionInput(), inputForm.getPsbtInput());
updateSignatures(inputForm.getPsbtInput());
}
}
} }

View file

@ -11,6 +11,7 @@ import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.control.CopyableLabel; import com.sparrowwallet.sparrow.control.CopyableLabel;
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent; import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
import com.sparrowwallet.sparrow.event.BlockTransactionFetchedEvent; import com.sparrowwallet.sparrow.event.BlockTransactionFetchedEvent;
import com.sparrowwallet.sparrow.event.PSBTCombinedEvent;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
@ -106,7 +107,6 @@ public class InputsController extends TransactionFormController implements Initi
signatures.setText(foundSigs + "/?"); signatures.setText(foundSigs + "/?");
} }
addPieData(inputsPie, outputs); addPieData(inputsPie, outputs);
} }
@ -173,4 +173,11 @@ public class InputsController extends TransactionFormController implements Initi
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) { public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
total.refresh(event.getBitcoinUnit()); total.refresh(event.getBitcoinUnit());
} }
@Subscribe
public void psbtCombined(PSBTCombinedEvent event) {
if(event.getPsbt().equals(inputsForm.getPsbt())) {
updatePSBTInputs(inputsForm.getPsbt());
}
}
} }

View file

@ -10,6 +10,7 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.AddressLabel; import com.sparrowwallet.sparrow.control.AddressLabel;
import com.sparrowwallet.sparrow.control.CoinLabel; import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.control.CopyableLabel; import com.sparrowwallet.sparrow.control.CopyableLabel;
import com.sparrowwallet.sparrow.control.ScriptArea;
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent; import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
import com.sparrowwallet.sparrow.event.BlockTransactionOutputsFetchedEvent; import com.sparrowwallet.sparrow.event.BlockTransactionOutputsFetchedEvent;
import com.sparrowwallet.sparrow.event.ViewTransactionEvent; import com.sparrowwallet.sparrow.event.ViewTransactionEvent;
@ -54,7 +55,7 @@ public class OutputController extends TransactionFormController implements Initi
private Hyperlink spentBy; private Hyperlink spentBy;
@FXML @FXML
private CodeArea scriptPubKeyArea; private ScriptArea scriptPubKeyArea;
@Override @Override
public void initialize(URL location, ResourceBundle resources) { public void initialize(URL location, ResourceBundle resources) {
@ -92,8 +93,9 @@ public class OutputController extends TransactionFormController implements Initi
spent.setText("Unknown"); spent.setText("Unknown");
} }
initializeScriptField(scriptPubKeyArea);
scriptPubKeyArea.clear(); scriptPubKeyArea.clear();
appendScript(scriptPubKeyArea, txOutput.getScript(), null, null); scriptPubKeyArea.appendScript(txOutput.getScript(), null, null);
} }
private void updateSpent(List<BlockTransaction> outputTransactions) { private void updateSpent(List<BlockTransaction> outputTransactions) {

View file

@ -368,6 +368,10 @@ public class TransactionController implements Initializable {
this.txdata = new TransactionData(transaction); this.txdata = new TransactionData(transaction);
} }
public void setName(String name) {
this.txdata.setName(name);
}
public PSBT getPSBT() { public PSBT getPSBT() {
return txdata.getPsbt(); return txdata.getPsbt();
} }

View file

@ -17,6 +17,7 @@ import java.util.Map;
public class TransactionData { public class TransactionData {
private final Transaction transaction; private final Transaction transaction;
private String name;
private PSBT psbt; private PSBT psbt;
private BlockTransaction blockTransaction; private BlockTransaction blockTransaction;
private Map<Sha256Hash, BlockTransaction> inputTransactions; private Map<Sha256Hash, BlockTransaction> inputTransactions;
@ -49,6 +50,14 @@ public class TransactionData {
return transaction; return transaction;
} }
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public PSBT getPsbt() { public PSBT getPsbt() {
return psbt; return psbt;
} }

View file

@ -27,6 +27,10 @@ public abstract class TransactionForm {
return txdata.getTransaction(); return txdata.getTransaction();
} }
public String getName() {
return txdata.getName();
}
public PSBT getPsbt() { public PSBT getPsbt() {
return txdata.getPsbt(); return txdata.getPsbt();
} }

View file

@ -12,6 +12,7 @@ import com.sparrowwallet.sparrow.AppController;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.CopyableLabel; import com.sparrowwallet.sparrow.control.CopyableLabel;
import com.sparrowwallet.sparrow.control.CopyableTextField; import com.sparrowwallet.sparrow.control.CopyableTextField;
import com.sparrowwallet.sparrow.control.ScriptArea;
import com.sparrowwallet.sparrow.event.ReceiveToEvent; import com.sparrowwallet.sparrow.event.ReceiveToEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent; import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
import com.sparrowwallet.sparrow.event.WalletNodesChangedEvent; import com.sparrowwallet.sparrow.event.WalletNodesChangedEvent;
@ -53,7 +54,7 @@ public class ReceiveController extends WalletFormController implements Initializ
private ImageView qrCode; private ImageView qrCode;
@FXML @FXML
private CodeArea scriptPubKeyArea; private ScriptArea scriptPubKeyArea;
@FXML @FXML
private CodeArea outputDescriptor; private CodeArea outputDescriptor;
@ -67,7 +68,7 @@ public class ReceiveController extends WalletFormController implements Initializ
@Override @Override
public void initializeView() { public void initializeView() {
initializeScriptField(scriptPubKeyArea);
} }
public void setNodeEntry(NodeEntry nodeEntry) { public void setNodeEntry(NodeEntry nodeEntry) {
@ -88,7 +89,7 @@ public class ReceiveController extends WalletFormController implements Initializ
} }
scriptPubKeyArea.clear(); scriptPubKeyArea.clear();
appendScript(scriptPubKeyArea, nodeEntry.getOutputScript(), null, null); scriptPubKeyArea.appendScript(nodeEntry.getOutputScript(), null, null);
outputDescriptor.clear(); outputDescriptor.clear();
outputDescriptor.appendText(nodeEntry.getOutputDescriptor()); outputDescriptor.appendText(nodeEntry.getOutputDescriptor());

View file

@ -137,6 +137,7 @@ public class SendController extends WalletFormController implements Initializabl
targetBlocks.setTooltip(tooltip); targetBlocks.setTooltip(tooltip);
userFeeSet.set(false); userFeeSet.set(false);
revalidate(amount, amountListener);
updateTransaction(); updateTransaction();
} }
}; };

View file

@ -41,7 +41,7 @@
-fx-padding: 10 20 10 20; -fx-padding: 10 20 10 20;
} }
.signatures-buttons .button { .signatures-buttons .segmented-button, .signatures-buttons .button, .signatures-buttons .toggle-button {
-fx-pref-height: 75px; -fx-pref-height: 75px;
-fx-max-width: Infinity; -fx-max-width: Infinity;
} }

View file

@ -171,16 +171,34 @@
</VBox> </VBox>
<VBox> <VBox>
<HBox styleClass="signatures-buttons" spacing="20"> <HBox styleClass="signatures-buttons" spacing="20">
<Button HBox.hgrow="ALWAYS" text="Show QR" contentDisplay="TOP" wrapText="true" textAlignment="CENTER" onAction="#showPSBT"> <SegmentedButton HBox.hgrow="ALWAYS">
<buttons>
<ToggleButton HBox.hgrow="ALWAYS" text="Show QR" contentDisplay="TOP" wrapText="true" textAlignment="CENTER" onAction="#showPSBT">
<graphic> <graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="QRCODE" /> <Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="QRCODE" />
</graphic> </graphic>
</Button> </ToggleButton>
<Button HBox.hgrow="ALWAYS" text="Save PSBT" contentDisplay="TOP" wrapText="true" textAlignment="CENTER" onAction="#savePSBT"> <ToggleButton HBox.hgrow="ALWAYS" text="Scan QR" contentDisplay="TOP" wrapText="true" textAlignment="CENTER" onAction="#scanPSBT">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="CAMERA" />
</graphic>
</ToggleButton>
</buttons>
</SegmentedButton>
<SegmentedButton HBox.hgrow="ALWAYS">
<buttons>
<ToggleButton HBox.hgrow="ALWAYS" text="Save PSBT" contentDisplay="TOP" wrapText="true" textAlignment="CENTER" onAction="#savePSBT">
<graphic> <graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="SD_CARD" /> <Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="SD_CARD" />
</graphic> </graphic>
</Button> </ToggleButton>
<ToggleButton HBox.hgrow="ALWAYS" text="Load PSBT" contentDisplay="TOP" wrapText="true" textAlignment="CENTER" onAction="#loadPSBT">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="ARROW_UP" />
</graphic>
</ToggleButton>
</buttons>
</SegmentedButton>
<Button fx:id="signButton" defaultButton="true" HBox.hgrow="ALWAYS" text="Sign" contentDisplay="TOP" wrapText="true" textAlignment="CENTER" onAction="#signPSBT"> <Button fx:id="signButton" defaultButton="true" HBox.hgrow="ALWAYS" text="Sign" contentDisplay="TOP" wrapText="true" textAlignment="CENTER" onAction="#signPSBT">
<graphic> <graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="PEN_FANCY" /> <Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="PEN_FANCY" />

View file

@ -16,6 +16,7 @@
<?import com.sparrowwallet.sparrow.control.CoinLabel?> <?import com.sparrowwallet.sparrow.control.CoinLabel?>
<?import com.sparrowwallet.sparrow.control.AddressLabel?> <?import com.sparrowwallet.sparrow.control.AddressLabel?>
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?> <?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
<?import com.sparrowwallet.sparrow.control.ScriptArea?>
<GridPane hgap="10.0" vgap="10.0" styleClass="tx-pane" stylesheets="@input.css, @transaction.css, @../script.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.InputController"> <GridPane hgap="10.0" vgap="10.0" styleClass="tx-pane" stylesheets="@input.css, @transaction.css, @../script.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.InputController">
<padding> <padding>
@ -55,28 +56,28 @@
<Field text="ScriptSig:"> <Field text="ScriptSig:">
<VirtualizedScrollPane> <VirtualizedScrollPane>
<content> <content>
<CodeArea fx:id="scriptSigArea" editable="false" wrapText="true" prefHeight="42" maxHeight="42" styleClass="uneditable-codearea" /> <ScriptArea fx:id="scriptSigArea" editable="false" wrapText="true" prefHeight="42" maxHeight="42" styleClass="uneditable-codearea" />
</content> </content>
</VirtualizedScrollPane> </VirtualizedScrollPane>
</Field> </Field>
<Field text="RedeemScript:"> <Field text="RedeemScript:">
<VirtualizedScrollPane fx:id="redeemScriptScroll"> <VirtualizedScrollPane fx:id="redeemScriptScroll">
<content> <content>
<CodeArea fx:id="redeemScriptArea" editable="false" wrapText="true" prefHeight="42" maxHeight="42" styleClass="uneditable-codearea" /> <ScriptArea fx:id="redeemScriptArea" editable="false" wrapText="true" prefHeight="42" maxHeight="42" styleClass="uneditable-codearea" />
</content> </content>
</VirtualizedScrollPane> </VirtualizedScrollPane>
</Field> </Field>
<Field text="Witnesses:"> <Field text="Witnesses:">
<VirtualizedScrollPane fx:id="witnessesScroll"> <VirtualizedScrollPane fx:id="witnessesScroll">
<content> <content>
<CodeArea fx:id="witnessesArea" editable="false" wrapText="true" prefHeight="42" maxHeight="42" styleClass="uneditable-codearea" /> <ScriptArea fx:id="witnessesArea" editable="false" wrapText="true" prefHeight="42" maxHeight="42" styleClass="uneditable-codearea" />
</content> </content>
</VirtualizedScrollPane> </VirtualizedScrollPane>
</Field> </Field>
<Field text="WitnessScript:"> <Field text="WitnessScript:">
<VirtualizedScrollPane fx:id="witnessScriptScroll"> <VirtualizedScrollPane fx:id="witnessScriptScroll">
<content> <content>
<CodeArea fx:id="witnessScriptArea" editable="false" wrapText="true" prefHeight="42" maxHeight="42" styleClass="uneditable-codearea" /> <ScriptArea fx:id="witnessScriptArea" editable="false" wrapText="true" prefHeight="42" maxHeight="42" styleClass="uneditable-codearea" />
</content> </content>
</VirtualizedScrollPane> </VirtualizedScrollPane>
</Field> </Field>

View file

@ -14,6 +14,7 @@
<?import com.sparrowwallet.sparrow.control.CopyableLabel?> <?import com.sparrowwallet.sparrow.control.CopyableLabel?>
<?import com.sparrowwallet.sparrow.control.CoinLabel?> <?import com.sparrowwallet.sparrow.control.CoinLabel?>
<?import com.sparrowwallet.sparrow.control.AddressLabel?> <?import com.sparrowwallet.sparrow.control.AddressLabel?>
<?import com.sparrowwallet.sparrow.control.ScriptArea?>
<GridPane hgap="10.0" vgap="10.0" styleClass="tx-pane" stylesheets="@output.css, @transaction.css, @../script.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.OutputController"> <GridPane hgap="10.0" vgap="10.0" styleClass="tx-pane" stylesheets="@output.css, @transaction.css, @../script.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.OutputController">
<padding> <padding>
@ -50,7 +51,7 @@
<Field text="ScriptPubKey:"> <Field text="ScriptPubKey:">
<VirtualizedScrollPane> <VirtualizedScrollPane>
<content> <content>
<CodeArea fx:id="scriptPubKeyArea" editable="false" wrapText="true" prefHeight="42" maxHeight="42" styleClass="uneditable-codearea" /> <ScriptArea fx:id="scriptPubKeyArea" editable="false" wrapText="true" prefHeight="42" maxHeight="42" styleClass="uneditable-codearea" />
</content> </content>
</VirtualizedScrollPane> </VirtualizedScrollPane>
</Field> </Field>

View file

@ -16,6 +16,7 @@
<?import com.sparrowwallet.sparrow.control.CopyableLabel?> <?import com.sparrowwallet.sparrow.control.CopyableLabel?>
<?import org.controlsfx.glyphfont.Glyph?> <?import org.controlsfx.glyphfont.Glyph?>
<?import com.sparrowwallet.sparrow.control.CopyableTextField?> <?import com.sparrowwallet.sparrow.control.CopyableTextField?>
<?import com.sparrowwallet.sparrow.control.ScriptArea?>
<BorderPane stylesheets="@receive.css, @wallet.css, @../script.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.ReceiveController"> <BorderPane stylesheets="@receive.css, @wallet.css, @../script.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.ReceiveController">
<center> <center>
@ -59,7 +60,7 @@
<Field text="ScriptPubKey"> <Field text="ScriptPubKey">
<VirtualizedScrollPane> <VirtualizedScrollPane>
<content> <content>
<CodeArea fx:id="scriptPubKeyArea" editable="false" wrapText="true" prefHeight="42" maxHeight="42" styleClass="uneditable-codearea" /> <ScriptArea fx:id="scriptPubKeyArea" editable="false" wrapText="true" prefHeight="42" maxHeight="42" styleClass="uneditable-codearea" />
</content> </content>
</VirtualizedScrollPane> </VirtualizedScrollPane>
</Field> </Field>