Added DLC GUI stuff to a new package in the existing GUI and made a new tab for DLCs (#1590)

Co-authored-by: Ben Carman <benthecarman@live.com>
This commit is contained in:
Nadav Kohen 2020-06-18 15:23:53 -05:00 committed by GitHub
parent 9237074510
commit a1d17ab662
11 changed files with 605 additions and 1 deletions

View File

@ -2,6 +2,7 @@ package org.bitcoins.gui
import javafx.event.{ActionEvent, EventHandler}
import javafx.scene.image.Image
import org.bitcoins.gui.dlc.DLCPane
import org.bitcoins.gui.settings.SettingsPane
import scalafx.application.{JFXApp, Platform}
import scalafx.beans.property.StringProperty
@ -80,6 +81,8 @@ object WalletGUI extends JFXApp {
bottom = statusLabel
}
private val dlcPane = new DLCPane(glassPane)
private val settingsPane = new SettingsPane
private val tabPane: TabPane = new TabPane {
@ -89,12 +92,17 @@ object WalletGUI extends JFXApp {
content = borderPane
}
val dlcTab: Tab = new Tab {
text = "DLC"
content = dlcPane.borderPane
}
val settingsTab: Tab = new Tab {
text = "Settings"
content = settingsPane.view
}
tabs = Seq(walletTab, settingsTab)
tabs = Seq(walletTab, dlcTab, settingsTab)
tabClosingPolicy = TabClosingPolicy.Unavailable
}

View File

@ -0,0 +1,178 @@
package org.bitcoins.gui.dlc
import javafx.event.{ActionEvent, EventHandler}
import org.bitcoins.gui.{GlobalData, TaskRunner}
import scalafx.geometry.{Insets, Pos}
import scalafx.scene.control._
import scalafx.scene.layout._
class DLCPane(glassPane: VBox) {
private val statusLabel = new Label {
maxWidth = Double.MaxValue
padding = Insets(0, 10, 10, 10)
text <== GlobalData.statusText
}
private val resultArea = new TextArea {
prefHeight = 750
prefWidth = 800
editable = false
text = "Click on Offer or Accept to begin."
wrapText = true
}
private val demoOracleArea = new TextArea {
prefHeight = 700
prefWidth = 400
editable = false
text =
"Click on Init Demo Oracle to generate example oracle and contract information"
wrapText = true
}
private val numOutcomesTF = new TextField {
promptText = "Number of Outcomes"
}
private val model =
new DLCPaneModel(resultArea, demoOracleArea, numOutcomesTF)
private val demoOracleButton = new Button {
text = "Init Demo Oracle"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onInitOracle()
}
}
private val oracleButtonHBox = new HBox {
children = Seq(numOutcomesTF, demoOracleButton)
spacing = 15
}
private val demoOracleVBox = new VBox {
children = Seq(demoOracleArea, oracleButtonHBox)
spacing = 15
}
private val offerButton = new Button {
text = "Offer"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onOffer()
}
}
private val acceptButton = new Button {
text = "Accept"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onAccept()
}
}
private val signButton = new Button {
text = "Sign"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onSign()
}
}
private val addSigsButton = new Button {
text = "Add Sigs"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onAddSigs()
}
}
private val getFundingButton = new Button {
text = "Get Funding Tx"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onGetFunding()
}
}
private val initCloseButton = new Button {
text = "Init Close"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onInitClose()
}
}
private val acceptCloseButton = new Button {
text = "Accept Close"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onAcceptClose()
}
}
private val refundButton = new Button {
text = "Refund"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onRefund()
}
}
private val forceCloseButton = new Button {
text = "Force Close"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onForceClose()
}
}
private val punishButton = new Button {
text = "Punish"
onAction = new EventHandler[ActionEvent] {
override def handle(event: ActionEvent): Unit = model.onPunish()
}
}
private val initButtonBar = new ButtonBar {
buttons = Seq(offerButton, signButton)
}
private val acceptButtonBar = new ButtonBar {
buttons = Seq(acceptButton, addSigsButton, getFundingButton)
}
private val execButtonBar = new ButtonBar {
buttons = Seq(initCloseButton,
acceptCloseButton,
refundButton,
forceCloseButton,
punishButton)
}
private val spaceRegion = new Region()
private val spaceRegion2 = new Region()
private val buttonSpacer = new GridPane {
hgap = 10
prefHeight = 50
alignment = Pos.Center
add(initButtonBar, 0, 0)
add(spaceRegion, 1, 0)
add(acceptButtonBar, 2, 0)
add(spaceRegion2, 3, 0)
add(execButtonBar, 4, 0)
}
private val textAreaHBox = new HBox {
children = Seq(resultArea, demoOracleVBox)
spacing = 10
}
val borderPane: BorderPane = new BorderPane {
top = buttonSpacer
center = textAreaHBox
bottom = statusLabel
}
resultArea.prefWidth <== (borderPane.width * 2) / 3
demoOracleVBox.prefWidth <== (borderPane.width / 3)
spaceRegion.prefWidth <== (borderPane.width - initButtonBar.width - acceptButtonBar.width - execButtonBar.width - 100) / 2
spaceRegion2.prefWidth <== spaceRegion.prefWidth
private val taskRunner = new TaskRunner(buttonSpacer, glassPane)
model.taskRunner = taskRunner
}

View File

@ -0,0 +1,127 @@
package org.bitcoins.gui.dlc
import org.bitcoins.cli.{CliCommand, Config, ConsoleCli}
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.OracleInfo
import org.bitcoins.crypto.ECPrivateKey
import org.bitcoins.gui.TaskRunner
import org.bitcoins.gui.dlc.dialog._
import scalafx.beans.property.ObjectProperty
import scalafx.scene.control.{TextArea, TextField}
import scalafx.stage.Window
import scala.util.{Failure, Success}
class DLCPaneModel(
resultArea: TextArea,
oracleInfoArea: TextArea,
numOutcomesTF: TextField) {
var taskRunner: TaskRunner = _
// Sadly, it is a Java "pattern" to pass null into
// constructors to signal that you want some default
val parentWindow: ObjectProperty[Window] =
ObjectProperty[Window](null.asInstanceOf[Window])
def printDLCDialogResult[T <: CliCommand](
caption: String,
dialog: DLCDialog[T]): Unit = {
val result = dialog.showAndWait(parentWindow.value)
result match {
case Some(command) =>
taskRunner.run(
caption = caption,
op = {
ConsoleCli.exec(command, Config.empty) match {
case Success(commandReturn) => resultArea.text = commandReturn
case Failure(err) =>
err.printStackTrace()
resultArea.text = s"Error executing command:\n${err.getMessage}"
}
}
)
case None => ()
}
}
def onInitOracle(): Unit = {
val numOutcomes = BigInt(numOutcomesTF.text()).toInt
require(numOutcomes <= 10, "More than 10 outcomes not supported.")
val result = InitOracleDialog.showAndWait(parentWindow.value, numOutcomes)
result match {
case Some((outcomes, contractInfo)) =>
val builder = new StringBuilder()
val privKey = ECPrivateKey.freshPrivateKey
val pubKey = privKey.schnorrPublicKey
val kValue = ECPrivateKey.freshPrivateKey
val rValue = kValue.schnorrNonce
val oracleInfo = OracleInfo(pubKey, rValue)
builder.append(
s"Oracle Public Key: ${pubKey.hex}\nEvent R value: ${rValue.hex}\n")
builder.append(s"Serialized Oracle Info: ${oracleInfo.hex}\n\n")
builder.append("Outcome hashes and amounts in order of entry:\n")
contractInfo.foreach {
case (hash, amt) => builder.append(s"${hash.hex} - ${amt.toLong}\n")
}
builder.append(s"\nSerialized Contract Info:\n${contractInfo.hex}\n\n")
builder.append("Outcomes and oracle sigs in order of entry:\n")
outcomes.zip(contractInfo.keys).foreach {
case (outcome, hash) =>
val sig = privKey.schnorrSignWithNonce(hash.bytes, kValue)
builder.append(s"$outcome - ${sig.hex}\n")
}
GlobalDLCData.lastOracleInfo = oracleInfo.hex
GlobalDLCData.lastContractInfo = contractInfo.hex
oracleInfoArea.text = builder.result()
case None => ()
}
}
def onOffer(): Unit = {
printDLCDialogResult("CreateDLCOffer", OfferDLCDialog)
}
def onAccept(): Unit = {
printDLCDialogResult("AcceptDLCOffer", AcceptDLCDialog)
}
def onSign(): Unit = {
printDLCDialogResult("SignDLC", SignDLCDialog)
}
def onAddSigs(): Unit = {
printDLCDialogResult("AddDLCSigs", AddSigsDLCDialog)
}
def onGetFunding(): Unit = {
printDLCDialogResult("GetDLCFundingTx", GetFundingDLCDialog)
}
def onInitClose(): Unit = {
printDLCDialogResult("InitDLCMutualClose", InitCloseDLCDialog)
}
def onAcceptClose(): Unit = {
printDLCDialogResult("AcceptDLCMutualClose", AcceptCloseDLCDialog)
}
def onForceClose(): Unit = {
printDLCDialogResult("ExecuteUnilateralDLC", ForceCloseDLCDialog)
}
def onPunish(): Unit = {
printDLCDialogResult("ExecuteDLCPunishment", PunishDLCDialog)
}
def onRefund(): Unit = {
printDLCDialogResult("ExecuteDLCRefund", RefundDLCDialog)
}
}

View File

@ -0,0 +1,8 @@
package org.bitcoins.gui.dlc
object GlobalDLCData {
var lastEventId: String = ""
var lastOracleSig: String = ""
var lastOracleInfo: String = ""
var lastContractInfo: String = ""
}

View File

@ -0,0 +1,18 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.AcceptDLCOffer
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.DLCOffer
object AcceptDLCDialog
extends DLCDialog[AcceptDLCOffer](
"Accept DLC Offer",
"Enter DLC offer to accept",
Vector(DLCDialog.dlcOfferStr -> DLCDialog.textArea())) {
import DLCDialog._
override def constructFromInput(
inputs: Map[String, String]): AcceptDLCOffer = {
val offer = DLCOffer.fromJson(ujson.read(inputs(dlcOfferStr)))
AcceptDLCOffer(offer, escaped = false)
}
}

View File

@ -0,0 +1,18 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.AddDLCSigs
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.DLCSign
object AddSigsDLCDialog
extends DLCDialog[AddDLCSigs](
"Sign DLC",
"Enter DLC signatures message",
Vector(DLCDialog.dlcSigStr -> DLCDialog.textArea())) {
import DLCDialog._
override def constructFromInput(inputs: Map[String, String]): AddDLCSigs = {
val sign = DLCSign.fromJson(ujson.read(inputs(dlcSigStr)))
AddDLCSigs(sign)
}
}

View File

@ -0,0 +1,152 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand
import org.bitcoins.gui.dlc.GlobalDLCData
import scalafx.Includes._
import scalafx.application.Platform
import scalafx.geometry.Insets
import scalafx.scene.control._
import scalafx.scene.layout.GridPane
import scalafx.stage.Window
abstract class DLCDialog[T <: CliCommand](
dialogTitle: String,
header: String,
fields: Vector[(String, TextInputControl)], // Vector instead of Map to keep order
optionalFields: Vector[String] = Vector.empty) {
private def readCachedValue(key: String, value: String): Unit = {
fields
.find(_._1 == key)
.foreach(_._2.text = value)
}
readCachedValue(DLCDialog.dlcEventIdStr, GlobalDLCData.lastEventId)
readCachedValue(DLCDialog.dlcOracleSigStr, GlobalDLCData.lastOracleSig)
readCachedValue(DLCDialog.oracleInfoStr, GlobalDLCData.lastOracleInfo)
readCachedValue(DLCDialog.contractInfoStr, GlobalDLCData.lastContractInfo)
private def writeCachedValue(
key: String,
inputs: Vector[(String, String)],
setter: String => Unit): Unit = {
inputs
.find(_._1 == key)
.foreach(pair => setter(pair._2))
}
def constructFromInput(inputs: Map[String, String]): T
def showAndWait(parentWindow: Window): Option[T] = {
val dialog = new Dialog[Option[T]]() {
initOwner(parentWindow)
title = dialogTitle
headerText = header
}
dialog.dialogPane().buttonTypes = Seq(ButtonType.OK, ButtonType.Cancel)
dialog.dialogPane().content = new GridPane {
hgap = 10
vgap = 10
padding = Insets(20, 100, 10, 10)
var nextRow: Int = 0
def addRow(label: String, textField: TextInputControl): Unit = {
add(new Label(label), 0, nextRow)
add(textField, 1, nextRow)
nextRow += 1
}
fields.foreach {
case (fieldStr, filedInput) => addRow(fieldStr, filedInput)
}
}
// Enable/Disable OK button depending on whether all data was entered.
val okButton = dialog.dialogPane().lookupButton(ButtonType.OK)
val requiredFields =
fields.filterNot(field => optionalFields.contains(field._1))
// Simple validation that sufficient data was entered
okButton.disable <== requiredFields
.map(_._2.text.isEmpty)
.reduce(_ || _)
// Request focus on the first field by default.
Platform.runLater(fields.head._2.requestFocus())
// When the OK button is clicked, convert the result to a T.
dialog.resultConverter = dialogButton =>
if (dialogButton == ButtonType.OK) {
val inputs = fields.map { case (key, input) => (key, input.text()) }
writeCachedValue(DLCDialog.dlcEventIdStr,
inputs,
GlobalDLCData.lastEventId = _)
writeCachedValue(DLCDialog.dlcOracleSigStr,
inputs,
GlobalDLCData.lastOracleSig = _)
writeCachedValue(DLCDialog.oracleInfoStr,
inputs,
GlobalDLCData.lastOracleInfo = _)
writeCachedValue(DLCDialog.contractInfoStr,
inputs,
GlobalDLCData.lastContractInfo = _)
Some(constructFromInput(inputs.toMap))
} else None
val result = dialog.showAndWait()
result match {
case Some(someT: Some[T]) => someT
case Some(_) | None => None
}
}
}
object DLCDialog {
def textArea(): TextArea = {
new TextArea {
wrapText = true
}
}
val oracleInfoStr = "Oracle Info"
val contractInfoStr = "Contract Info"
val collateralStr = "Collateral"
val feeRateStr = "Fee Rate"
val locktimeStr = "Locktime"
val refundLocktimeStr = "Refund Locktime"
val allOfferFields: Map[String, String] = Map[String, String](
oracleInfoStr -> "",
contractInfoStr -> "",
collateralStr -> "Satoshis",
feeRateStr -> "sats/vbyte (optional)",
locktimeStr -> "Block or unix time",
refundLocktimeStr -> "Block or unix time"
)
def constructOfferFields(): Vector[(String, TextField)] =
allOfferFields.map {
case (label, hint) =>
(label, new TextField() {
promptText = hint
})
}.toVector
val dlcOfferStr = "DLC Offer"
val dlcAcceptStr = "DLC Accept Message"
val dlcSigStr = "DLC Signatures"
val dlcEventIdStr = "Event ID"
val dlcOracleSigStr = "Oracle Signature"
val dlcMutualCloseOfferStr = "Mutual Close Offer"
val dlcForceCloseTxStr = "Force Close Transaction"
}

View File

@ -0,0 +1,19 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.GetDLCFundingTx
import org.bitcoins.crypto.Sha256DigestBE
import scalafx.scene.control.TextField
object GetFundingDLCDialog
extends DLCDialog[GetDLCFundingTx](
"DLC Funding Transaction",
"Enter DLC event ID",
Vector(DLCDialog.dlcEventIdStr -> new TextField())) {
import DLCDialog._
override def constructFromInput(
inputs: Map[String, String]): GetDLCFundingTx = {
val eventId = Sha256DigestBE(inputs(dlcEventIdStr))
GetDLCFundingTx(eventId)
}
}

View File

@ -0,0 +1,38 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.CreateDLCOffer
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage
import org.bitcoins.core.currency.Satoshis
import org.bitcoins.core.number.UInt32
import org.bitcoins.core.wallet.fee.SatoshisPerVirtualByte
object OfferDLCDialog
extends DLCDialog[CreateDLCOffer]("Create DLC Offer",
"Enter DLC details",
DLCDialog.constructOfferFields(),
Vector(DLCDialog.feeRateStr)) {
import DLCDialog._
override def constructFromInput(
inputs: Map[String, String]): CreateDLCOffer = {
val feeRate = if (inputs(feeRateStr).isEmpty) {
None
} else {
Some(
SatoshisPerVirtualByte(
Satoshis(BigInt(inputs(feeRateStr)))
)
)
}
CreateDLCOffer(
oracleInfo = DLCMessage.OracleInfo.fromHex(inputs(oracleInfoStr)),
contractInfo = DLCMessage.ContractInfo.fromHex(inputs(contractInfoStr)),
collateral = Satoshis(BigInt(inputs(collateralStr))),
feeRateOpt = feeRate,
locktime = UInt32(BigInt(inputs(locktimeStr))),
refundLT = UInt32(BigInt(inputs(refundLocktimeStr))),
escaped = false
)
}
}

View File

@ -0,0 +1,19 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.ExecuteDLCRefund
import org.bitcoins.crypto.Sha256DigestBE
import scalafx.scene.control.TextField
object RefundDLCDialog
extends DLCDialog[ExecuteDLCRefund](
"DLC Refund",
"Enter DLC event ID",
Vector(DLCDialog.dlcEventIdStr -> new TextField())) {
import DLCDialog._
override def constructFromInput(
inputs: Map[String, String]): ExecuteDLCRefund = {
val eventId = Sha256DigestBE(inputs(dlcEventIdStr))
ExecuteDLCRefund(eventId, noBroadcast = false)
}
}

View File

@ -0,0 +1,19 @@
package org.bitcoins.gui.dlc.dialog
import org.bitcoins.cli.CliCommand.SignDLC
import org.bitcoins.commons.jsonmodels.dlc.DLCMessage.DLCAccept
import org.bitcoins.gui.dlc.GlobalDLCData
object SignDLCDialog
extends DLCDialog[SignDLC](
"Sign DLC",
"Enter DLC accept message",
Vector(DLCDialog.dlcAcceptStr -> DLCDialog.textArea())) {
import DLCDialog._
override def constructFromInput(inputs: Map[String, String]): SignDLC = {
val accept = DLCAccept.fromJson(ujson.read(inputs(dlcAcceptStr)))
GlobalDLCData.lastEventId = accept.eventId.hex
SignDLC(accept, escaped = false)
}
}