mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-02-22 14:33:06 +01:00
Constructed simple Bitcoin-S wallet GUI (#1285)
* Constructed simple Bitcoin-S wallet GUI * Added note about bitcoin-s server in doc * Removed wallet dep from gui * Replaced lambdas with constructors for compatibility with scala 2.11 * Fixed after rebase
This commit is contained in:
parent
47c2bc08c4
commit
c31a4a3f8a
14 changed files with 459 additions and 5 deletions
|
@ -237,11 +237,15 @@ object ConsoleCli {
|
|||
case Some(conf) => conf
|
||||
}
|
||||
|
||||
exec(config.command, config.debug)
|
||||
}
|
||||
|
||||
def exec(command: CliCommand, debugEnabled: Boolean = false): Try[String] = {
|
||||
import System.err.{println => printerr}
|
||||
|
||||
/** Prints the given message to stderr if debug is set */
|
||||
def debug(message: Any): Unit = {
|
||||
if (config.debug) {
|
||||
if (debugEnabled) {
|
||||
printerr(s"DEBUG: $message")
|
||||
}
|
||||
}
|
||||
|
@ -251,7 +255,7 @@ object ConsoleCli {
|
|||
Failure(new RuntimeException(message))
|
||||
}
|
||||
|
||||
val requestParam: RequestParam = config.command match {
|
||||
val requestParam: RequestParam = command match {
|
||||
case GetBalance(isSats) =>
|
||||
RequestParam("getbalance", Seq(up.writeJs(isSats)))
|
||||
case GetNewAddress =>
|
||||
|
|
12
app/gui/gui.sbt
Normal file
12
app/gui/gui.sbt
Normal file
|
@ -0,0 +1,12 @@
|
|||
name := "bitcoin-s-gui"
|
||||
|
||||
libraryDependencies ++= Deps.gui
|
||||
|
||||
mainClass := Some("org.bitcoins.gui.WalletGUI")
|
||||
|
||||
enablePlugins(JavaAppPackaging)
|
||||
|
||||
publish / skip := true
|
||||
|
||||
// Fork a new JVM for 'run' and 'test:run' to avoid JavaFX double initialization problems
|
||||
fork := true
|
9
app/gui/src/main/scala/org/bitcoins/gui/GlobalData.scala
Normal file
9
app/gui/src/main/scala/org/bitcoins/gui/GlobalData.scala
Normal file
|
@ -0,0 +1,9 @@
|
|||
package org.bitcoins.gui
|
||||
|
||||
import scalafx.beans.property.{DoubleProperty, StringProperty}
|
||||
|
||||
object GlobalData {
|
||||
val currentBalance: DoubleProperty = DoubleProperty(0)
|
||||
|
||||
val log: StringProperty = StringProperty("")
|
||||
}
|
76
app/gui/src/main/scala/org/bitcoins/gui/TaskRunner.scala
Normal file
76
app/gui/src/main/scala/org/bitcoins/gui/TaskRunner.scala
Normal file
|
@ -0,0 +1,76 @@
|
|||
package org.bitcoins.gui
|
||||
|
||||
import scalafx.application.Platform
|
||||
import scalafx.scene.Node
|
||||
import scalafx.scene.control.Alert.AlertType
|
||||
import scalafx.scene.control.{Alert, Label}
|
||||
|
||||
/**
|
||||
* Runs a background task disabling the `mainView` and main visible `glassPane`.
|
||||
* Shows statis using `statusLabel`.
|
||||
*
|
||||
* Copied from [[https://github.com/scalafx/ScalaFX-Tutorials/blob/master/slick-table/src/main/scala/org/scalafx/slick_table/TaskRunner.scala]]
|
||||
*/
|
||||
class TaskRunner(mainView: Node, glassPane: Node, statusLabel: Label) {
|
||||
|
||||
/**
|
||||
* Run an operation on a separate thread. Return and wait for its completion,
|
||||
* then return result of running that operation.
|
||||
*
|
||||
* A progress indicator is displayed while running the operation.
|
||||
*
|
||||
* @param caption name for the thread (useful in debugging) and status displayed
|
||||
* when running the task.
|
||||
* @param op operation to run.
|
||||
* @tparam R type of result returned by the operation.
|
||||
* @return result returned by operation `op`.
|
||||
*/
|
||||
def run[R](caption: String, op: => R): Unit = {
|
||||
|
||||
def showProgress(progressEnabled: Boolean): Unit = {
|
||||
mainView.disable = progressEnabled
|
||||
glassPane.visible = progressEnabled
|
||||
}
|
||||
|
||||
// Indicate task in progress
|
||||
Platform.runLater {
|
||||
showProgress(true)
|
||||
statusLabel.text = caption
|
||||
}
|
||||
|
||||
val task = new javafx.concurrent.Task[R] {
|
||||
override def call(): R = {
|
||||
op
|
||||
}
|
||||
override def succeeded(): Unit = {
|
||||
showProgress(false)
|
||||
statusLabel.text = caption + " - Done."
|
||||
// Do callback, if defined
|
||||
}
|
||||
override def failed(): Unit = {
|
||||
|
||||
showProgress(false)
|
||||
statusLabel.text = caption + " - Failed."
|
||||
val t = Option(getException)
|
||||
t.foreach(_.printStackTrace())
|
||||
// Show error message
|
||||
val _ = new Alert(AlertType.Error) {
|
||||
initOwner(owner)
|
||||
title = caption
|
||||
headerText = "Operation failed. " + t
|
||||
.map("Exception: " + _.getClass)
|
||||
.getOrElse("")
|
||||
contentText = t.map(_.getMessage).getOrElse("")
|
||||
}.showAndWait()
|
||||
}
|
||||
override def cancelled(): Unit = {
|
||||
showProgress(false)
|
||||
statusLabel.text = caption + " - Cancelled."
|
||||
}
|
||||
}
|
||||
|
||||
val th = new Thread(task, caption)
|
||||
th.setDaemon(true)
|
||||
th.start()
|
||||
}
|
||||
}
|
95
app/gui/src/main/scala/org/bitcoins/gui/WalletGUI.scala
Normal file
95
app/gui/src/main/scala/org/bitcoins/gui/WalletGUI.scala
Normal file
|
@ -0,0 +1,95 @@
|
|||
package org.bitcoins.gui
|
||||
|
||||
import javafx.event.{ActionEvent, EventHandler}
|
||||
import scalafx.application.{JFXApp, Platform}
|
||||
import scalafx.beans.property.StringProperty
|
||||
import scalafx.geometry.{Insets, Pos}
|
||||
import scalafx.scene.Scene
|
||||
import scalafx.scene.control.Alert.AlertType
|
||||
import scalafx.scene.control._
|
||||
import scalafx.scene.layout.{BorderPane, HBox, StackPane, VBox}
|
||||
|
||||
object WalletGUI extends JFXApp {
|
||||
// Catch unhandled exceptions on FX Application thread
|
||||
Thread
|
||||
.currentThread()
|
||||
.setUncaughtExceptionHandler(
|
||||
new Thread.UncaughtExceptionHandler {
|
||||
override def uncaughtException(t: Thread, ex: Throwable): Unit = {
|
||||
ex.printStackTrace()
|
||||
val _ = new Alert(AlertType.Error) {
|
||||
initOwner(owner)
|
||||
title = "Unhandled exception"
|
||||
headerText = "Exception: " + ex.getClass + ""
|
||||
contentText = Option(ex.getMessage).getOrElse("")
|
||||
}.showAndWait()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
private val glassPane = new VBox {
|
||||
children = new ProgressIndicator {
|
||||
progress = ProgressIndicator.IndeterminateProgress
|
||||
visible = true
|
||||
}
|
||||
alignment = Pos.Center
|
||||
visible = false
|
||||
}
|
||||
|
||||
private val statusLabel = new Label {
|
||||
maxWidth = Double.MaxValue
|
||||
padding = Insets(0, 10, 10, 10)
|
||||
}
|
||||
|
||||
private val resultArea = new TextArea {
|
||||
editable = false
|
||||
wrapText = true
|
||||
text <== StringProperty("Your current balance is: ") + GlobalData.currentBalance + StringProperty(
|
||||
s"\n\n${(0 until 60).map(_ => "-").mkString("")}\n\n") + GlobalData.log
|
||||
}
|
||||
|
||||
private val model = new WalletGUIModel()
|
||||
|
||||
private val getNewAddressButton = new Button {
|
||||
text = "Get New Addreses"
|
||||
onAction = new EventHandler[ActionEvent] {
|
||||
override def handle(event: ActionEvent): Unit = model.onGetNewAddress()
|
||||
}
|
||||
}
|
||||
|
||||
private val sendButton = new Button {
|
||||
text = "Send"
|
||||
onAction = new EventHandler[ActionEvent] {
|
||||
override def handle(event: ActionEvent): Unit = model.onSend()
|
||||
}
|
||||
}
|
||||
|
||||
private val buttonBar = new HBox {
|
||||
children = Seq(getNewAddressButton, sendButton)
|
||||
alignment = Pos.Center
|
||||
spacing <== width / 2
|
||||
}
|
||||
|
||||
private val borderPane = new BorderPane {
|
||||
top = buttonBar
|
||||
center = resultArea
|
||||
bottom = statusLabel
|
||||
}
|
||||
|
||||
private val rootView = new StackPane {
|
||||
children = Seq(
|
||||
borderPane,
|
||||
glassPane
|
||||
)
|
||||
}
|
||||
|
||||
stage = new JFXApp.PrimaryStage {
|
||||
title = "Bitcoin-S Wallet"
|
||||
scene = new Scene(rootView)
|
||||
}
|
||||
|
||||
private val taskRunner = new TaskRunner(resultArea, glassPane, statusLabel)
|
||||
model.taskRunner = taskRunner
|
||||
|
||||
Platform.runLater(sendButton.requestFocus())
|
||||
}
|
80
app/gui/src/main/scala/org/bitcoins/gui/WalletGUIModel.scala
Normal file
80
app/gui/src/main/scala/org/bitcoins/gui/WalletGUIModel.scala
Normal file
|
@ -0,0 +1,80 @@
|
|||
package org.bitcoins.gui
|
||||
|
||||
import org.bitcoins.cli.CliCommand.{GetBalance, GetNewAddress, SendToAddress}
|
||||
import org.bitcoins.cli.ConsoleCli
|
||||
import org.bitcoins.core.currency.Bitcoins
|
||||
import org.bitcoins.core.protocol.BitcoinAddress
|
||||
import org.bitcoins.gui.dialog.{GetNewAddressDialog, SendDialog}
|
||||
import scalafx.beans.property.{ObjectProperty, StringProperty}
|
||||
import scalafx.scene.control.Alert.AlertType
|
||||
import scalafx.scene.control.Alert
|
||||
import scalafx.stage.Window
|
||||
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
class WalletGUIModel() {
|
||||
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])
|
||||
|
||||
updateBalance()
|
||||
|
||||
def onGetNewAddress(): Unit = {
|
||||
val address = StringProperty("")
|
||||
|
||||
taskRunner.run(
|
||||
caption = "Get New Address",
|
||||
op = {
|
||||
ConsoleCli.exec(GetNewAddress) match {
|
||||
case Success(commandReturn) => address.value = commandReturn
|
||||
case Failure(err) => throw err
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
GetNewAddressDialog.showAndWait(parentWindow.value, address)
|
||||
}
|
||||
|
||||
def onSend(): Unit = {
|
||||
val result = SendDialog.showAndWait(parentWindow.value)
|
||||
|
||||
result match {
|
||||
case Some((address, amount)) =>
|
||||
taskRunner.run(
|
||||
caption = s"Send $amount to $address",
|
||||
op = {
|
||||
ConsoleCli.exec(
|
||||
SendToAddress(BitcoinAddress(address).get,
|
||||
Bitcoins(BigDecimal(amount)),
|
||||
satoshisPerVirtualByte = None)) match {
|
||||
case Success(txid) =>
|
||||
GlobalData.log.value =
|
||||
s"Sent $amount to $address in tx: $txid\n\n${GlobalData.log()}"
|
||||
case Failure(err) => throw err
|
||||
}
|
||||
}
|
||||
)
|
||||
case None => ()
|
||||
}
|
||||
|
||||
updateBalance()
|
||||
}
|
||||
|
||||
private def updateBalance(): Unit = {
|
||||
ConsoleCli.exec(GetBalance(isSats = false)) match {
|
||||
case Success(commandReturn) =>
|
||||
GlobalData.currentBalance.value = commandReturn.split(' ').head.toDouble
|
||||
case Failure(err) =>
|
||||
err.printStackTrace()
|
||||
val _ = new Alert(AlertType.Error) {
|
||||
initOwner(owner)
|
||||
title = "Could not retrieve wallet balance"
|
||||
headerText = s"Operation failed. Exception: ${err.getClass}"
|
||||
contentText = err.getMessage
|
||||
}.showAndWait()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package org.bitcoins.gui.dialog
|
||||
|
||||
import scalafx.Includes._
|
||||
import scalafx.beans.property.StringProperty
|
||||
import scalafx.scene.control.{ButtonType, Dialog, TextArea}
|
||||
import scalafx.stage.Window
|
||||
|
||||
object GetNewAddressDialog {
|
||||
|
||||
def showAndWait(parentWindow: Window, address: String): Unit = {
|
||||
showAndWait(parentWindow, StringProperty(address))
|
||||
}
|
||||
|
||||
def showAndWait(parentWindow: Window, address: StringProperty): Unit = {
|
||||
val dialog = new Dialog[Unit]() {
|
||||
initOwner(parentWindow)
|
||||
title = "New Address"
|
||||
}
|
||||
|
||||
// TODO make a button to copy the address to clipboard
|
||||
|
||||
dialog.dialogPane().buttonTypes = Seq(ButtonType.OK)
|
||||
|
||||
dialog.dialogPane().content = new TextArea {
|
||||
text <== address
|
||||
editable = false
|
||||
}
|
||||
|
||||
val _ = dialog.showAndWait()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package org.bitcoins.gui.dialog
|
||||
|
||||
import scalafx.Includes._
|
||||
import scalafx.application.Platform
|
||||
import scalafx.geometry.Insets
|
||||
import scalafx.scene.control.{ButtonType, Dialog, Label, TextField}
|
||||
import scalafx.scene.layout.GridPane
|
||||
import scalafx.stage.Window
|
||||
|
||||
object SendDialog {
|
||||
|
||||
def showAndWait(parentWindow: Window): Option[(String, String)] = {
|
||||
val dialog = new Dialog[Option[(String, String)]]() {
|
||||
initOwner(parentWindow)
|
||||
title = "Send"
|
||||
}
|
||||
|
||||
dialog.dialogPane().buttonTypes = Seq(ButtonType.OK, ButtonType.Cancel)
|
||||
|
||||
val addressTF = new TextField()
|
||||
val amountTF = new TextField()
|
||||
|
||||
dialog.dialogPane().content = new GridPane {
|
||||
hgap = 10
|
||||
vgap = 10
|
||||
padding = Insets(20, 100, 10, 10)
|
||||
|
||||
var nextRow: Int = 0
|
||||
def addRow(label: String, textField: TextField): Unit = {
|
||||
add(new Label(label), 0, nextRow)
|
||||
add(textField, 1, nextRow)
|
||||
nextRow += 1
|
||||
}
|
||||
|
||||
addRow("Address", addressTF)
|
||||
addRow("Amount (in BTC)", amountTF)
|
||||
}
|
||||
|
||||
// Enable/Disable OK button depending on whether all data was entered.
|
||||
val okButton = dialog.dialogPane().lookupButton(ButtonType.OK)
|
||||
// Simple validation that sufficient data was entered
|
||||
okButton.disable <== addressTF.text.isEmpty || amountTF.text.isEmpty
|
||||
|
||||
Platform.runLater(addressTF.requestFocus())
|
||||
|
||||
// When the OK button is clicked, convert the result to a T.
|
||||
dialog.resultConverter = dialogButton =>
|
||||
if (dialogButton == ButtonType.OK) {
|
||||
Some((addressTF.text(), amountTF.text()))
|
||||
} else None
|
||||
|
||||
dialog.showAndWait() match {
|
||||
case Some(Some((address: String, amount: String))) =>
|
||||
Some((address, amount))
|
||||
case Some(_) | None => None
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,6 +52,7 @@ lazy val `bitcoin-s` = project
|
|||
bench,
|
||||
eclairRpc,
|
||||
eclairRpcTest,
|
||||
gui,
|
||||
keyManager,
|
||||
keyManagerTest,
|
||||
node,
|
||||
|
@ -242,6 +243,13 @@ lazy val cliTest = project
|
|||
testkit
|
||||
)
|
||||
|
||||
lazy val gui = project
|
||||
.in(file("app/gui"))
|
||||
.settings(CommonSettings.prodSettings: _*)
|
||||
.dependsOn(
|
||||
cli
|
||||
)
|
||||
|
||||
lazy val chainDbSettings = dbFlywaySettings("chaindb")
|
||||
lazy val chain = project
|
||||
.in(file("chain"))
|
||||
|
|
BIN
docs/applications/gui-snapshot.png
Normal file
BIN
docs/applications/gui-snapshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
55
docs/applications/gui.md
Normal file
55
docs/applications/gui.md
Normal file
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
id: gui
|
||||
title: GUI
|
||||
---
|
||||
|
||||
|
||||
## Bitcoin-S Graphical User Interface
|
||||
There is now a GUI built using [scalafx](https://www.scalafx.org/) to allow users who do not wish to use the [command line interface](cli.md) to interact with a Bitcoin-S wallet. This GUI is currently very minimal and looks something like this:
|
||||
|
||||

|
||||
|
||||
At the time of writing this document, creating addresses and sending Bitcoin are the only supported wallet functionalities in the wallet's RPC API. The expansion of this interface will enable a more robust GUI and this issue is being tracked [here](https://github.com/bitcoin-s/bitcoin-s/issues/1284).
|
||||
|
||||
### Running the GUI
|
||||
|
||||
The GUI will only function properly if it can connect to the [bitcoin-s server](server.md) which must be running in the background before running the GUI.
|
||||
|
||||
To run the gui, simply execute
|
||||
|
||||
```bashrc
|
||||
sbt gui/run
|
||||
```
|
||||
|
||||
alternatively the gui can be built into an executable using the [sbt native packager](https://www.scala-sbt.org/sbt-native-packager/).
|
||||
|
||||
### ScalaFX
|
||||
|
||||
[ScalaFX](https://www.scalafx.org/) is a scala library wrapping [JavaFX](https://openjfx.io/) which is itself a java interface to [GTK](https://www.gtk.org/) (which is useful to know when googling for answers as it is easier to search for answers in GTK or sometimes javafx and then translate them to scalafx).
|
||||
|
||||
ScalaFX itself requires the dependency
|
||||
|
||||
```scala
|
||||
"org.scalafx" %% "scalafx"
|
||||
```
|
||||
|
||||
but full use of the library requires also adding these dependencies:
|
||||
|
||||
```scala
|
||||
"org.openjfx" % "javafx-base"
|
||||
"org.openjfx" % "javafx-controls"
|
||||
"org.openjfx" % "javafx-fxml"
|
||||
"org.openjfx" % "javafx-graphics"
|
||||
"org.openjfx" % "javafx-media"
|
||||
"org.openjfx" % "javafx-swing"
|
||||
"org.openjfx" % "javafx-web"
|
||||
```
|
||||
|
||||
which for some reason must all be classified by the OS of the machine they are running on.
|
||||
|
||||
A GUI can now be built by creating an object which extends `scalafx.application.JFXApp` and setting the `stage` var to a `new JFXApp.PrimaryStage` whose `scene` is your `Node` (where `Node` is the super-class in this framework for all GUI elements).
|
||||
|
||||
#### ScalaFX Examples
|
||||
|
||||
Aside from looking at how the Bitcoin-S GUI is implemented, for more useful examples of ScalaFX, see [these tutorials](https://github.com/scalafx/ScalaFX-Tutorials) and specifically [this one](https://github.com/scalafx/ScalaFX-Tutorials/tree/master/slick-table) which I think is most illustrative.
|
||||
|
|
@ -20,6 +20,9 @@ object Deps {
|
|||
val nativeLoaderV = "2.3.4"
|
||||
val typesafeConfigV = "1.4.0"
|
||||
|
||||
val scalaFxV = "12.0.2-R18"
|
||||
val javaFxV = "12.0.2"
|
||||
|
||||
// async dropped Scala 2.11 in 0.10.0
|
||||
val asyncOldScalaV = "0.9.7"
|
||||
val asyncNewScalaV = "0.10.0"
|
||||
|
@ -66,6 +69,23 @@ object Deps {
|
|||
val akkaStream = "com.typesafe.akka" %% "akka-stream" % V.akkaStreamv withSources () withJavadoc ()
|
||||
val akkaActor = "com.typesafe.akka" %% "akka-actor" % V.akkaStreamv withSources () withJavadoc ()
|
||||
|
||||
val scalaFx = "org.scalafx" %% "scalafx" % V.scalaFxV withSources () withJavadoc ()
|
||||
lazy val osName = System.getProperty("os.name") match {
|
||||
case n if n.startsWith("Linux") => "linux"
|
||||
case n if n.startsWith("Mac") => "mac"
|
||||
case n if n.startsWith("Windows") => "win"
|
||||
case _ => throw new Exception("Unknown platform!")
|
||||
}
|
||||
// Not sure if all of these are needed, some might be possible to remove
|
||||
lazy val javaFxBase = "org.openjfx" % s"javafx-base" % V.javaFxV classifier osName withSources () withJavadoc ()
|
||||
lazy val javaFxControls = "org.openjfx" % s"javafx-controls" % V.javaFxV classifier osName withSources () withJavadoc ()
|
||||
lazy val javaFxFxml = "org.openjfx" % s"javafx-fxml" % V.javaFxV classifier osName withSources () withJavadoc ()
|
||||
lazy val javaFxGraphics = "org.openjfx" % s"javafx-graphics" % V.javaFxV classifier osName withSources () withJavadoc ()
|
||||
lazy val javaFxMedia = "org.openjfx" % s"javafx-media" % V.javaFxV classifier osName withSources () withJavadoc ()
|
||||
lazy val javaFxSwing = "org.openjfx" % s"javafx-swing" % V.javaFxV classifier osName withSources () withJavadoc ()
|
||||
lazy val javaFxWeb = "org.openjfx" % s"javafx-web" % V.javaFxV classifier osName withSources () withJavadoc ()
|
||||
lazy val javaFxDeps = List(javaFxBase, javaFxControls, javaFxFxml, javaFxGraphics, javaFxMedia, javaFxSwing, javaFxWeb)
|
||||
|
||||
val playJson = "com.typesafe.play" %% "play-json" % V.playv withSources () withJavadoc ()
|
||||
val typesafeConfig = "com.typesafe" % "config" % V.typesafeConfigV withSources () withJavadoc ()
|
||||
|
||||
|
@ -199,6 +219,8 @@ object Deps {
|
|||
Compile.codehaus
|
||||
)
|
||||
|
||||
val gui = List(Compile.scalaFx) ++ Compile.javaFxDeps
|
||||
|
||||
def picklers(scalaVersion: String) = List(
|
||||
if (scalaVersion.startsWith("2.11")) Compile.oldMicroPickle
|
||||
else Compile.newMicroPickle
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"title": "CLI"
|
||||
},
|
||||
"applications/configuration": {
|
||||
"title": "Application Configuration"
|
||||
"title": "Application configuration"
|
||||
},
|
||||
"applications/dlc": {
|
||||
"title": "Executing A DLC with Bitcoin-S"
|
||||
|
@ -20,8 +20,11 @@
|
|||
"applications/filter-sync": {
|
||||
"title": "Syncing Blockfilters"
|
||||
},
|
||||
"applications/gui": {
|
||||
"title": "GUI"
|
||||
},
|
||||
"applications/node": {
|
||||
"title": "Light Client"
|
||||
"title": "Light client"
|
||||
},
|
||||
"applications/server": {
|
||||
"title": "Application Server"
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
"Getting Setup": ["getting-setup"],
|
||||
"Applications": [
|
||||
"applications/cli",
|
||||
"applications/server"
|
||||
"applications/server",
|
||||
"applications/gui"
|
||||
],
|
||||
"Chain": [
|
||||
"chain/chain",
|
||||
|
|
Loading…
Add table
Reference in a new issue