Craig Raw
5 years ago
22 changed files with 557 additions and 193 deletions
@ -1 +1 @@ |
|||||
Subproject commit eabcf4e8f48ae18ff8d21436a2ab5e5153719944 |
Subproject commit 7871413573e67ed7539cf03d6deadd1a2c4abafa |
@ -0,0 +1,113 @@ |
|||||
|
package com.sparrowwallet.sparrow; |
||||
|
|
||||
|
import com.sparrowwallet.drongo.protocol.Script; |
||||
|
import com.sparrowwallet.drongo.protocol.ScriptChunk; |
||||
|
import com.sparrowwallet.sparrow.transaction.ScriptContextMenu; |
||||
|
import javafx.geometry.Point2D; |
||||
|
import javafx.scene.control.Label; |
||||
|
import javafx.stage.Popup; |
||||
|
import org.fxmisc.richtext.CodeArea; |
||||
|
import org.fxmisc.richtext.event.MouseOverTextEvent; |
||||
|
import org.fxmisc.richtext.model.TwoDimensional; |
||||
|
|
||||
|
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; |
||||
|
|
||||
|
public abstract class BaseController { |
||||
|
protected void appendScript(CodeArea codeArea, Script script) { |
||||
|
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(); |
||||
|
Label popupMsg = new Label(); |
||||
|
popupMsg.getStyleClass().add("tooltip"); |
||||
|
popup.getContent().add(popupMsg); |
||||
|
|
||||
|
area.setMouseOverTextDelay(Duration.ofMillis(150)); |
||||
|
area.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> { |
||||
|
TwoDimensional.Position position = area.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward); |
||||
|
if(position.getMajor() % 2 == 0) { |
||||
|
ScriptChunk hoverChunk = script.getChunks().get(position.getMajor()/2); |
||||
|
if(!hoverChunk.isOpCode()) { |
||||
|
Point2D pos = e.getScreenPosition(); |
||||
|
popupMsg.setText(describeScriptChunk(hoverChunk)); |
||||
|
popup.show(area, pos.getX(), pos.getY() + 10); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
area.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> { |
||||
|
popup.hide(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
protected String describeScriptChunk(ScriptChunk chunk) { |
||||
|
return chunk.toString(); |
||||
|
} |
||||
|
} |
@ -0,0 +1,64 @@ |
|||||
|
package com.sparrowwallet.sparrow.control; |
||||
|
|
||||
|
import javafx.animation.FadeTransition; |
||||
|
import javafx.beans.InvalidationListener; |
||||
|
import javafx.beans.Observable; |
||||
|
import javafx.beans.property.ObjectProperty; |
||||
|
import javafx.scene.Cursor; |
||||
|
import javafx.scene.Node; |
||||
|
import javafx.scene.input.Clipboard; |
||||
|
import javafx.scene.input.ClipboardContent; |
||||
|
import javafx.scene.layout.Region; |
||||
|
import javafx.scene.layout.StackPane; |
||||
|
import javafx.util.Duration; |
||||
|
import org.controlsfx.control.textfield.CustomTextField; |
||||
|
|
||||
|
public class CopyableTextField extends CustomTextField { |
||||
|
private static final Duration FADE_DURATION = Duration.millis(350); |
||||
|
|
||||
|
public CopyableTextField() { |
||||
|
super(); |
||||
|
getStyleClass().add("copyable-text-field"); |
||||
|
setupCopyButtonField(super.rightProperty()); |
||||
|
} |
||||
|
|
||||
|
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) { |
||||
|
Region copyButton = new Region(); |
||||
|
copyButton.getStyleClass().addAll("graphic"); //$NON-NLS-1$
|
||||
|
StackPane copyButtonPane = new StackPane(copyButton); |
||||
|
copyButtonPane.getStyleClass().addAll("copy-button"); //$NON-NLS-1$
|
||||
|
copyButtonPane.setOpacity(0.0); |
||||
|
copyButtonPane.setCursor(Cursor.DEFAULT); |
||||
|
copyButtonPane.setOnMouseReleased(e -> { |
||||
|
ClipboardContent content = new ClipboardContent(); |
||||
|
content.putString(getText()); |
||||
|
Clipboard.getSystemClipboard().setContent(content); |
||||
|
}); |
||||
|
|
||||
|
rightProperty.set(copyButtonPane); |
||||
|
|
||||
|
final FadeTransition fader = new FadeTransition(FADE_DURATION, copyButtonPane); |
||||
|
fader.setCycleCount(1); |
||||
|
|
||||
|
textProperty().addListener(new InvalidationListener() { |
||||
|
@Override |
||||
|
public void invalidated(Observable arg0) { |
||||
|
String text = getText(); |
||||
|
boolean isTextEmpty = text == null || text.isEmpty(); |
||||
|
boolean isButtonVisible = fader.getNode().getOpacity() > 0; |
||||
|
|
||||
|
if (isTextEmpty && isButtonVisible) { |
||||
|
setButtonVisible(false); |
||||
|
} else if (!isTextEmpty && !isButtonVisible) { |
||||
|
setButtonVisible(true); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void setButtonVisible( boolean visible ) { |
||||
|
fader.setFromValue(visible? 0.0: 1.0); |
||||
|
fader.setToValue(visible? 1.0: 0.0); |
||||
|
fader.play(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -1,15 +1,15 @@ |
|||||
package com.sparrowwallet.sparrow.event; |
package com.sparrowwallet.sparrow.event; |
||||
|
|
||||
import com.sparrowwallet.drongo.wallet.Wallet; |
import com.sparrowwallet.sparrow.wallet.NodeEntry; |
||||
|
|
||||
public class ReceiveActionEvent { |
public class ReceiveActionEvent { |
||||
private Wallet.Node receiveNode; |
private NodeEntry receiveEntry; |
||||
|
|
||||
public ReceiveActionEvent(Wallet.Node receiveNode) { |
public ReceiveActionEvent(NodeEntry receiveEntry) { |
||||
this.receiveNode = receiveNode; |
this.receiveEntry = receiveEntry; |
||||
} |
} |
||||
|
|
||||
public Wallet.Node getReceiveNode() { |
public NodeEntry getReceiveEntry() { |
||||
return receiveNode; |
return receiveEntry; |
||||
} |
} |
||||
} |
} |
||||
|
@ -0,0 +1,33 @@ |
|||||
|
package com.sparrowwallet.sparrow.wallet; |
||||
|
|
||||
|
import javafx.beans.property.SimpleStringProperty; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
|
||||
|
public abstract class Entry { |
||||
|
private final SimpleStringProperty labelProperty; |
||||
|
private final List<Entry> children = new ArrayList<>(); |
||||
|
|
||||
|
public Entry(String label) { |
||||
|
this.labelProperty = new SimpleStringProperty(label); |
||||
|
} |
||||
|
|
||||
|
public Entry(SimpleStringProperty labelProperty) { |
||||
|
this.labelProperty = labelProperty; |
||||
|
} |
||||
|
|
||||
|
public String getLabel() { |
||||
|
return labelProperty.get(); |
||||
|
} |
||||
|
|
||||
|
public SimpleStringProperty labelProperty() { |
||||
|
return labelProperty; |
||||
|
} |
||||
|
|
||||
|
public List<Entry> getChildren() { |
||||
|
return children; |
||||
|
} |
||||
|
|
||||
|
public abstract Long getAmount(); |
||||
|
} |
@ -0,0 +1,25 @@ |
|||||
|
package com.sparrowwallet.sparrow.wallet; |
||||
|
|
||||
|
import com.sparrowwallet.drongo.wallet.Wallet; |
||||
|
|
||||
|
public class NodeEntry extends Entry { |
||||
|
private final Wallet.Node node; |
||||
|
|
||||
|
public NodeEntry(Wallet.Node node) { |
||||
|
super(node.getLabel()); |
||||
|
this.node = node; |
||||
|
|
||||
|
labelProperty().addListener((observable, oldValue, newValue) -> node.setLabel(newValue)); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public Long getAmount() { |
||||
|
//TODO: Iterate through TransactionEntries to calculate amount
|
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public Wallet.Node getNode() { |
||||
|
return node; |
||||
|
} |
||||
|
} |
@ -0,0 +1,104 @@ |
|||||
|
package com.sparrowwallet.sparrow.wallet; |
||||
|
|
||||
|
import com.google.zxing.BarcodeFormat; |
||||
|
import com.google.zxing.client.j2se.MatrixToImageConfig; |
||||
|
import com.google.zxing.client.j2se.MatrixToImageWriter; |
||||
|
import com.google.zxing.common.BitMatrix; |
||||
|
import com.google.zxing.qrcode.QRCodeWriter; |
||||
|
import com.sparrowwallet.drongo.KeyPurpose; |
||||
|
import com.sparrowwallet.sparrow.EventManager; |
||||
|
import com.sparrowwallet.sparrow.control.CopyableLabel; |
||||
|
import com.sparrowwallet.sparrow.control.CopyableTextField; |
||||
|
import javafx.beans.value.ChangeListener; |
||||
|
import javafx.beans.value.ObservableValue; |
||||
|
import javafx.event.ActionEvent; |
||||
|
import javafx.fxml.FXML; |
||||
|
import javafx.fxml.Initializable; |
||||
|
import javafx.scene.control.TextField; |
||||
|
import javafx.scene.image.Image; |
||||
|
import javafx.scene.image.ImageView; |
||||
|
import org.fxmisc.richtext.CodeArea; |
||||
|
|
||||
|
import java.io.ByteArrayInputStream; |
||||
|
import java.io.ByteArrayOutputStream; |
||||
|
import java.net.URL; |
||||
|
import java.util.ResourceBundle; |
||||
|
|
||||
|
public class ReceiveController extends WalletFormController implements Initializable { |
||||
|
@FXML |
||||
|
private CopyableTextField address; |
||||
|
|
||||
|
@FXML |
||||
|
private TextField label; |
||||
|
|
||||
|
@FXML |
||||
|
private CopyableLabel derivationPath; |
||||
|
|
||||
|
@FXML |
||||
|
private CopyableLabel lastUsed; |
||||
|
|
||||
|
@FXML |
||||
|
private ImageView qrCode; |
||||
|
|
||||
|
@FXML |
||||
|
private CodeArea scriptPubKeyArea; |
||||
|
|
||||
|
private NodeEntry currentEntry; |
||||
|
|
||||
|
@Override |
||||
|
public void initialize(URL location, ResourceBundle resources) { |
||||
|
EventManager.get().register(this); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void initializeView() { |
||||
|
|
||||
|
} |
||||
|
|
||||
|
public void setNodeEntry(NodeEntry nodeEntry) { |
||||
|
if(currentEntry != null) { |
||||
|
label.textProperty().unbindBidirectional(currentEntry.labelProperty()); |
||||
|
} |
||||
|
|
||||
|
this.currentEntry = nodeEntry; |
||||
|
|
||||
|
address.setText(nodeEntry.getNode().getAddress().toString()); |
||||
|
|
||||
|
label.textProperty().bindBidirectional(nodeEntry.labelProperty()); |
||||
|
|
||||
|
derivationPath.setText(nodeEntry.getNode().getDerivationPath()); |
||||
|
|
||||
|
//TODO: Find last used block height if available (red flag?)
|
||||
|
lastUsed.setText("Unknown"); |
||||
|
|
||||
|
Image qrImage = getQrCode(nodeEntry.getNode().getAddress().toString()); |
||||
|
if(qrImage != null) { |
||||
|
qrCode.setImage(qrImage); |
||||
|
} |
||||
|
|
||||
|
scriptPubKeyArea.clear(); |
||||
|
appendScript(scriptPubKeyArea, nodeEntry.getNode().getOutputScript(), null, null); |
||||
|
} |
||||
|
|
||||
|
private Image getQrCode(String address) { |
||||
|
try { |
||||
|
QRCodeWriter qrCodeWriter = new QRCodeWriter(); |
||||
|
BitMatrix qrMatrix = qrCodeWriter.encode(address, BarcodeFormat.QR_CODE, 150, 150); |
||||
|
|
||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
||||
|
MatrixToImageWriter.writeToStream(qrMatrix, "PNG", baos, new MatrixToImageConfig()); |
||||
|
|
||||
|
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); |
||||
|
return new Image(bais); |
||||
|
} catch(Exception e) { |
||||
|
e.printStackTrace(); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
public void getNewAddress(ActionEvent event) { |
||||
|
NodeEntry freshEntry = getWalletForm().getFreshNodeEntry(KeyPurpose.RECEIVE, currentEntry); |
||||
|
setNodeEntry(freshEntry); |
||||
|
} |
||||
|
} |
@ -0,0 +1,8 @@ |
|||||
|
.address-text-field { |
||||
|
-fx-font-family: Courier; |
||||
|
} |
||||
|
|
||||
|
.qr-code { |
||||
|
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 10, 0, 0, 0); |
||||
|
-fx-padding: 20; |
||||
|
} |
@ -0,0 +1,83 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
|
||||
|
<?import java.lang.*?> |
||||
|
<?import java.util.*?> |
||||
|
<?import javafx.scene.*?> |
||||
|
<?import javafx.scene.control.*?> |
||||
|
<?import javafx.scene.layout.*?> |
||||
|
|
||||
|
<?import javafx.geometry.Insets?> |
||||
|
<?import tornadofx.control.Form?> |
||||
|
<?import tornadofx.control.Fieldset?> |
||||
|
<?import tornadofx.control.Field?> |
||||
|
<?import javafx.scene.image.ImageView?> |
||||
|
<?import org.fxmisc.flowless.VirtualizedScrollPane?> |
||||
|
<?import org.fxmisc.richtext.CodeArea?> |
||||
|
<?import com.sparrowwallet.sparrow.control.CopyableLabel?> |
||||
|
<?import org.controlsfx.glyphfont.Glyph?> |
||||
|
<?import com.sparrowwallet.sparrow.control.CopyableTextField?> |
||||
|
|
||||
|
<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> |
||||
|
|
||||
|
<GridPane hgap="10.0" vgap="10.0"> |
||||
|
<padding> |
||||
|
<Insets left="25.0" right="25.0" top="25.0" /> |
||||
|
</padding> |
||||
|
<columnConstraints> |
||||
|
<ColumnConstraints percentWidth="70" /> |
||||
|
<ColumnConstraints percentWidth="30" /> |
||||
|
</columnConstraints> |
||||
|
<rowConstraints> |
||||
|
<RowConstraints /> |
||||
|
</rowConstraints> |
||||
|
<Form GridPane.columnIndex="0" GridPane.rowIndex="0"> |
||||
|
<Fieldset inputGrow="SOMETIMES" text="Receive"> |
||||
|
<Field text="Address:"> |
||||
|
<CopyableTextField fx:id="address" styleClass="address-text-field" editable="false" prefWidth="350"/> |
||||
|
</Field> |
||||
|
<Field text="Label:"> |
||||
|
<TextField fx:id="label" /> |
||||
|
</Field> |
||||
|
<Field text="Derivation:"> |
||||
|
<CopyableLabel fx:id="derivationPath" /> |
||||
|
</Field> |
||||
|
<Field text="Last Used:"> |
||||
|
<CopyableLabel fx:id="lastUsed" /> |
||||
|
</Field> |
||||
|
</Fieldset> |
||||
|
</Form> |
||||
|
|
||||
|
<AnchorPane GridPane.columnIndex="1" GridPane.rowIndex="0"> |
||||
|
<ImageView fx:id="qrCode" styleClass="qr-code" AnchorPane.rightAnchor="5"/> |
||||
|
</AnchorPane> |
||||
|
|
||||
|
<Separator styleClass="form-separator" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="1" /> |
||||
|
|
||||
|
<Form GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.rowIndex="2"> |
||||
|
<Fieldset inputGrow="SOMETIMES" text="Required Script"> |
||||
|
<Field text="ScriptPubKey:"> |
||||
|
<VirtualizedScrollPane> |
||||
|
<content> |
||||
|
<CodeArea fx:id="scriptPubKeyArea" editable="false" wrapText="true" prefHeight="42" maxHeight="42" styleClass="uneditable-codearea" /> |
||||
|
</content> |
||||
|
</VirtualizedScrollPane> |
||||
|
</Field> |
||||
|
</Fieldset> |
||||
|
</Form> |
||||
|
|
||||
|
</GridPane> |
||||
|
</center> |
||||
|
<bottom> |
||||
|
<AnchorPane> |
||||
|
<padding> |
||||
|
<Insets left="25.0" right="25.0" bottom="25.0" /> |
||||
|
</padding> |
||||
|
<Button fx:id="nextAddress" text="Get Next Address" defaultButton="true" AnchorPane.rightAnchor="10" onAction="#getNewAddress"> |
||||
|
<graphic> |
||||
|
<Glyph fontFamily="FontAwesome" icon="ARROW_DOWN" fontSize="12" /> |
||||
|
</graphic> |
||||
|
</Button> |
||||
|
</AnchorPane> |
||||
|
</bottom> |
||||
|
</BorderPane> |
Loading…
Reference in new issue