From 3458251289660dde756a8dc55b3e2fcf7491ea65 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Tue, 6 Apr 2021 12:32:37 -0500 Subject: [PATCH] Add clone event ability (#47) --- .../gui/dialog/CreateEnumEventDialog.scala | 77 ++++++++++++---- .../gui/dialog/CreateNumericEventDialog.scala | 75 +++++++++++++--- .../com/krystal/bull/gui/home/HomePane.scala | 41 +++------ .../krystal/bull/gui/home/HomePaneModel.scala | 89 +++++++++++++++++-- 4 files changed, 213 insertions(+), 69 deletions(-) diff --git a/src/main/scala/com/krystal/bull/gui/dialog/CreateEnumEventDialog.scala b/src/main/scala/com/krystal/bull/gui/dialog/CreateEnumEventDialog.scala index 72ee755..dbb5623 100644 --- a/src/main/scala/com/krystal/bull/gui/dialog/CreateEnumEventDialog.scala +++ b/src/main/scala/com/krystal/bull/gui/dialog/CreateEnumEventDialog.scala @@ -3,6 +3,7 @@ package com.krystal.bull.gui.dialog import com.krystal.bull.gui.home.InitEventParams import com.krystal.bull.gui.{GlobalData, KrystalBullUtil} import org.bitcoins.core.protocol.tlv.EnumEventDescriptorV0TLV +import org.bitcoins.core.util.TimeUtil import scalafx.Includes._ import scalafx.geometry.{Insets, Pos} import scalafx.scene.Node @@ -11,39 +12,64 @@ import scalafx.scene.layout.{GridPane, HBox, VBox} import scalafx.stage.Window import scalafx.util.StringConverter +import java.util.Date + object CreateEnumEventDialog { - def showAndWait(parentWindow: Window): Option[InitEventParams] = { + def showAndWait( + parentWindow: Window, + initParamsOpt: Option[InitEventParams]): Option[InitEventParams] = { + val descriptorOpt = + initParamsOpt.map(_.descriptorTLV.asInstanceOf[EnumEventDescriptorV0TLV]) + val titleStr = initParamsOpt match { + case Some(value) => s"Clone of ${value.sanitizedEventName}" + case None => "Create New Enum Event" + } + val dialog = new Dialog[Option[InitEventParams]]() { initOwner(parentWindow) - title = "Create Enum Event" + title = titleStr } dialog.dialogPane().buttonTypes = Seq(ButtonType.OK, ButtonType.Cancel) dialog.dialogPane().stylesheets = GlobalData.currentStyleSheets dialog.resizable = true - val eventNameTF = new TextField() - val datePicker: DatePicker = new DatePicker() + val eventNameTF = new TextField() { + text = initParamsOpt.map(_.sanitizedEventName).getOrElse("") + minWidth = 300 + } + + val appendDateCheckBox = new CheckBox() { + alignmentInParent = Pos.CenterRight + selected = initParamsOpt.exists(_.hasAppendedDate) + } + + val datePicker: DatePicker = new DatePicker() { + minWidth = 300 + } val hourPicker = new ComboBox[Int](1.to(12)) { - value = 12 + value = initParamsOpt.map(_.hour).getOrElse(12) } + val minutePicker = new ComboBox[Int](0.to(59)) { - value = 0 + value = initParamsOpt.map(_.minute).getOrElse(0) converter = new StringConverter[Int] { override def fromString(string: String): Int = string.toInt override def toString(t: Int): String = { - if (t < 10) { - s"0$t" - } else t.toString + if (t < 10) s"0$t" + else t.toString } } } val amOrPmPicker = new ComboBox[String](Vector("AM", "PM")) { - value = "AM" + value = initParamsOpt.map(_.isAM) match { + case Some(true) | None => "AM" + case Some(false) => "PM" + } } val outcomeMap: scala.collection.mutable.Map[Int, TextField] = @@ -57,9 +83,11 @@ object CreateEnumEventDialog { vgap = 5 } - def addOutcomeRow(): Unit = { + def addOutcomeRow(string: String = ""): Unit = { - val outcomeTF = new TextField() + val outcomeTF = new TextField() { + text = string + } val row = nextOutcomeRow outcomeMap.addOne((row, outcomeTF)) @@ -72,8 +100,13 @@ object CreateEnumEventDialog { dialog.dialogPane().getScene.getWindow.sizeToScene() } - addOutcomeRow() - addOutcomeRow() + descriptorOpt match { + case Some(descriptor) => + descriptor.outcomes.foreach(outcome => addOutcomeRow(outcome)) + case None => + addOutcomeRow() + addOutcomeRow() + } val addOutcomeButton: Button = new Button("+") { onAction = _ => addOutcomeRow() @@ -93,6 +126,12 @@ object CreateEnumEventDialog { add(new Label("Event Name"), 0, row) add(eventNameTF, 1, row) + if (GlobalData.advancedMode) { + row += 1 + add(new Label("Append date to name"), 0, row) + add(appendDateCheckBox, 1, row) + } + row += 1 add(new Label("Maturity Date"), 0, row) add(datePicker, 1, row) @@ -137,14 +176,20 @@ object CreateEnumEventDialog { // When the OK button is clicked, convert the result to a T. dialog.resultConverter = dialogButton => if (dialogButton == ButtonType.OK) { - val eventName = eventNameTF.text.value - val maturityDate = KrystalBullUtil.toInstant( datePicker = datePicker, hourPicker = hourPicker, minutePicker = minutePicker, amOrPmPicker = amOrPmPicker) + val eventName = { + val name = eventNameTF.text.value + if (appendDateCheckBox.selected.value) { + val timeStr = TimeUtil.iso8601ToString(Date.from(maturityDate)) + s"$name $timeStr" + } else name + } + val outcomeStrs = outcomeMap.values.toVector.distinct val outcomes = outcomeStrs.flatMap { keyStr => if (keyStr.text.value.nonEmpty) { diff --git a/src/main/scala/com/krystal/bull/gui/dialog/CreateNumericEventDialog.scala b/src/main/scala/com/krystal/bull/gui/dialog/CreateNumericEventDialog.scala index ff5b45b..451fdfa 100644 --- a/src/main/scala/com/krystal/bull/gui/dialog/CreateNumericEventDialog.scala +++ b/src/main/scala/com/krystal/bull/gui/dialog/CreateNumericEventDialog.scala @@ -5,6 +5,7 @@ import com.krystal.bull.gui.home.InitEventParams import com.krystal.bull.gui.{GUIUtil, GlobalData, KrystalBullUtil} import org.bitcoins.core.number._ import org.bitcoins.core.protocol.tlv.DigitDecompositionEventDescriptorV0TLV +import org.bitcoins.core.util.TimeUtil import scalafx.Includes._ import scalafx.geometry.{Insets, Pos} import scalafx.scene.control._ @@ -12,52 +13,85 @@ import scalafx.scene.layout.{GridPane, HBox} import scalafx.stage.Window import scalafx.util.StringConverter +import java.util.Date + object CreateNumericEventDialog { - def showAndWait(parentWindow: Window): Option[InitEventParams] = { + def showAndWait( + parentWindow: Window, + initParamsOpt: Option[InitEventParams]): Option[InitEventParams] = { + val descriptorOpt = + initParamsOpt.map( + _.descriptorTLV.asInstanceOf[DigitDecompositionEventDescriptorV0TLV]) + + val titleStr = initParamsOpt match { + case Some(value) => s"Clone of ${value.sanitizedEventName}" + case None => "Create New Numeric Event" + } + val dialog = new Dialog[Option[InitEventParams]]() { initOwner(parentWindow) - title = "Create Numeric Event" + title = titleStr } dialog.dialogPane().buttonTypes = Seq(ButtonType.OK, ButtonType.Cancel) dialog.dialogPane().stylesheets = GlobalData.currentStyleSheets dialog.resizable = true - val eventNameTF = new TextField() - val datePicker: DatePicker = new DatePicker() + val eventNameTF = new TextField() { + text = initParamsOpt.map(_.sanitizedEventName).getOrElse("") + minWidth = 300 + } + + val appendDateCheckBox = new CheckBox() { + alignmentInParent = Pos.CenterRight + selected = initParamsOpt.exists(_.hasAppendedDate) + } + + val datePicker: DatePicker = new DatePicker() { + minWidth = 300 + } val hourPicker = new ComboBox[Int](1.to(12)) { - value = 12 + value = initParamsOpt.map(_.hour).getOrElse(12) } val minutePicker = new ComboBox[Int](0.to(59)) { - value = 0 + value = initParamsOpt.map(_.minute).getOrElse(0) converter = new StringConverter[Int] { override def fromString(string: String): Int = string.toInt override def toString(t: Int): String = { - if (t < 10) { - s"0$t" - } else t.toString + if (t < 10) s"0$t" + else t.toString } } } val amOrPmPicker = new ComboBox[String](Vector("AM", "PM")) { - value = "AM" + value = initParamsOpt.map(_.isAM) match { + case Some(true) | None => "AM" + case Some(false) => "PM" + } } - val maxTF = new TextField() + val maxTF = new TextField() { + text = descriptorOpt.map(_.maxNum.toString()).getOrElse("") + } GUIUtil.setNumericInput(maxTF) val isSignedCheckBox = new CheckBox() { alignmentInParent = Pos.Center + selected = descriptorOpt.exists(_.minNum != 0) + } + + val unitTF = new TextField() { + text = descriptorOpt.map(_.unit.normStr).getOrElse("") } - val unitTF = new TextField() val precisionTF = new TextField() { - text = "0" + text = descriptorOpt.map(_.precision.toLong.toString).getOrElse("0") } + GUIUtil.setNumericInput(precisionTF) dialog.dialogPane().content = new GridPane { @@ -69,6 +103,12 @@ object CreateNumericEventDialog { add(new Label("Event Name"), 0, row) add(eventNameTF, 1, row) + if (GlobalData.advancedMode) { + row += 1 + add(new Label("Append date to name"), 0, row) + add(appendDateCheckBox, 1, row) + } + row += 1 add(new Label("Maturity Date"), 0, row) add(datePicker, 1, row) @@ -110,7 +150,6 @@ object CreateNumericEventDialog { // When the OK button is clicked, convert the result to a T. dialog.resultConverter = dialogButton => if (dialogButton == ButtonType.OK) { - val eventName = eventNameTF.text.value val maturityDate = KrystalBullUtil.toInstant( datePicker = datePicker, @@ -118,6 +157,14 @@ object CreateNumericEventDialog { minutePicker = minutePicker, amOrPmPicker = amOrPmPicker) + val eventName = { + val name = eventNameTF.text.value + if (appendDateCheckBox.selected.value) { + val timeStr = TimeUtil.iso8601ToString(Date.from(maturityDate)) + s"$name $timeStr" + } else name + } + val maxNumber = numberFormatter.parse(maxTF.text.value).longValue() // log 2 of maxNumber gives us how many base 2 digits are needed diff --git a/src/main/scala/com/krystal/bull/gui/home/HomePane.scala b/src/main/scala/com/krystal/bull/gui/home/HomePane.scala index c92ea86..b8d3876 100644 --- a/src/main/scala/com/krystal/bull/gui/home/HomePane.scala +++ b/src/main/scala/com/krystal/bull/gui/home/HomePane.scala @@ -82,7 +82,7 @@ class HomePane(glassPane: VBox) { columns ++= Seq(labelCol, announcementCol, maturityDateCol, signatureCol) margin = Insets(10, 0, 10, 0) - val infoItem: MenuItem = new MenuItem("View Event") { + val viewEventItem: MenuItem = new MenuItem("View Event") { onAction = _ => { val event = selectionModel.value.getSelectedItem model.viewEvent(event) @@ -90,10 +90,17 @@ class HomePane(glassPane: VBox) { } } + val cloneEventItem: MenuItem = new MenuItem("Clone Event") { + onAction = _ => { + val event = selectionModel.value.getSelectedItem + model.cloneEvent(event, () => updateTable()) + } + } + columnResizePolicy = TableView.ConstrainedResizePolicy contextMenu = new ContextMenu() { - items += infoItem + items ++= Vector(viewEventItem, cloneEventItem) } } } @@ -202,38 +209,12 @@ class HomePane(glassPane: VBox) { } private val createEnumEventButton = new Button("Create Enum Event") { - onAction = _ => { - model.createEnumEvent() match { - case Some(params) => - oracle - .createNewEvent(params.eventName, - params.maturationTime, - params.descriptorTLV) - .map { _ => - updateTable() - } - case None => - () - } - } + onAction = _ => model.createEnumEvent(() => updateTable()) } private val createDigitDecompEventButton = new Button( "Create Numeric Event") { - onAction = _ => { - model.createNumericEvent() match { - case Some(params) => - oracle - .createNewEvent(params.eventName, - params.maturationTime, - params.descriptorTLV) - .map { _ => - updateTable() - } - case None => - () - } - } + onAction = _ => model.createNumericEvent(() => updateTable()) } private val createButtons = new HBox() { diff --git a/src/main/scala/com/krystal/bull/gui/home/HomePaneModel.scala b/src/main/scala/com/krystal/bull/gui/home/HomePaneModel.scala index 2f38b23..7553a42 100644 --- a/src/main/scala/com/krystal/bull/gui/home/HomePaneModel.scala +++ b/src/main/scala/com/krystal/bull/gui/home/HomePaneModel.scala @@ -11,20 +11,53 @@ import org.bitcoins.commons.serializers.SerializerUtil import org.bitcoins.core.api.dlcoracle.OracleEvent import org.bitcoins.core.currency.{CurrencyUnit, Satoshis} import org.bitcoins.core.protocol.BitcoinAddress -import org.bitcoins.core.protocol.tlv.EventDescriptorTLV -import org.bitcoins.dlc.oracle._ +import org.bitcoins.core.protocol.tlv.{ + DigitDecompositionEventDescriptorV0TLV, + EnumEventDescriptorV0TLV, + EventDescriptorTLV +} +import org.bitcoins.core.util.TimeUtil import play.api.libs.json._ import scalafx.beans.property.{ObjectProperty, StringProperty} import scalafx.stage.Window -import java.time.Instant +import java.time.{Instant, LocalDateTime, ZoneOffset} import scala.concurrent.Future -import scala.util.{Failure, Success} +import scala.util.{Failure, Success, Try} case class InitEventParams( eventName: String, maturationTime: Instant, - descriptorTLV: EventDescriptorTLV) + descriptorTLV: EventDescriptorTLV) { + + val sanitizedEventName: String = { + val dateStr = eventName.split(" ").last + val dateT = Try(TimeUtil.iso8601ToDate(dateStr)) + + dateT match { + case Failure(_) => eventName // no appended date + case Success(_) => + eventName.split(" ").init.mkString(" ").trim + } + } + + val hasAppendedDate: Boolean = eventName != sanitizedEventName + + val dateTime: LocalDateTime = + LocalDateTime.ofInstant(maturationTime, ZoneOffset.UTC) + + val hour: Int = { + val military = dateTime.getHour + + if (military > 12) military - 12 + else if (military == 0) 12 + else military + } + + val minute: Int = dateTime.getMinute + + val isAM: Boolean = dateTime.getHour <= 12 +} class HomePaneModel() extends Logging { var taskRunner: TaskRunner = _ @@ -35,18 +68,56 @@ class HomePaneModel() extends Logging { ObjectProperty[Window](null.asInstanceOf[Window]) } - def createEnumEvent(): Option[InitEventParams] = { - CreateEnumEventDialog.showAndWait(parentWindow.value) + def createEnumEvent( + onSuccess: () => Unit, + initParamsOpt: Option[InitEventParams] = None): Unit = { + CreateEnumEventDialog.showAndWait(parentWindow.value, initParamsOpt) match { + case Some(params) => createEvent(params, onSuccess) + case None => () + } + } + + def createNumericEvent( + onSuccess: () => Unit, + initParamsOpt: Option[InitEventParams] = None): Unit = { + CreateNumericEventDialog.showAndWait(parentWindow.value, + initParamsOpt) match { + case Some(params) => createEvent(params, onSuccess) + case None => () + } } - def createNumericEvent(): Option[InitEventParams] = { - CreateNumericEventDialog.showAndWait(parentWindow.value) + private def createEvent( + params: InitEventParams, + onSuccess: () => Unit): Unit = { + oracle + .createNewEvent(params.eventName, + params.maturationTime, + params.descriptorTLV) + .map { _ => + onSuccess() + } + () } def viewEvent(event: OracleEvent): Unit = { ViewEventDialog.showAndWait(parentWindow.value, event) } + def cloneEvent(event: OracleEvent, onSuccess: () => Unit): Unit = { + val initialParams = InitEventParams(eventName = event.eventName, + maturationTime = event.maturationTime, + descriptorTLV = + event.eventDescriptorTLV) + + event.eventDescriptorTLV match { + case _: EnumEventDescriptorV0TLV => + createEnumEvent(onSuccess, Some(initialParams)) + case _: DigitDecompositionEventDescriptorV0TLV => + createNumericEvent(onSuccess, Some(initialParams)) + } + } + case class AddressStats( address: BitcoinAddress, chain_stats: AddressChainStats,