Следующий шаг это написание графического интерфейса.
Плюсом графического интерфейса является конечно же
«дружелюбность» к обычным пользователям, не знакомым ни с какими
терминалами и консолями. Это возможно благодаря более высокому уровню
дизайна и действиям, рассчитанным не только на клавиатуру, но и на работу с
мышью.
Иногда графический интерфейс могут специально реализовывать таким
образом, чтобы он имел вид консольного интерфейса, но при обратном
действии, консольный интерфейс не способен принять вид графического (по
крайней мере полноценно). Из этого следует, что CLI можно рассматривать как
подмножество GUI, и потому сам CLI имеет меньшую сложность при
реализации.
Одним из интересных способов реализации графического интерфейса
служит создание и поднятие локального сайта, вместо создания обычного
десктопного (или мобильного) приложения. Такой способ выстраивания GUI
имеет множество положительных оттенков. Во-первых, приложение
реализованное таким образом имеет свойство кроссплатформенности. То-есть
способно корректно запускаться на Windows, Linux, Mac OS, Android, IOS и
других платформах за счёт использования обычных браузеров. Во-вторых, так
как используются браузеры и GUI приложение представляет из себя обычный
сайт, то и является возможным использовать безграничное количество
фреймворков и библиотек связанных с WEB-разработкой, не ограничиваясь
при этом стандартными (либо подгружаемыми) библиотеками выбранного
языка программирования. Для такого способа реализации GUI достаточно лишь
возможности использовать HTTP протокол.
Программирование узла блокчейн
Привязка интерфейса будет осуществляться к блокчейн сети,
реализовывая функции проверки баланса, просмотра блоков, создания
транзакций, регистрации и авторизации.
Графический интерфейс
При реализации GUI будет использоваться язык программирования Go
Графический клиент представляет отдельное приложение и потому
именуется как «main».
——————————————————————————————————
package main
——————————————————————————————————
(https://golang.org/) [2] представляющий HTTP сервер, а также языки
HTML/CSS и фреймворк Bootstrap (https://getbootstrap.com/).
Функция init.
——————————————————————————————————
func init() {
err := json.Unmarshal([]byte(readFile(ADDR_FILE)), &Addresses)
if err != nil {
panic("failed: load addresses")
}
if len(Addresses) == 0 {
panic("failed: len(Addresses) == 0")
}
}
——————————————————————————————————
Загружает список адресов к которым клиент подключается, как к узлам
локчейн-сети. Используется константа ADDR_FILE, а также глобальная
переменная Addresses и функция readFile, которые были реализованы ещё в
консольном интерфейсе [1].
Константа ADDR_FILE.
——————————————————————————————————
const (
ADDR_FILE = "addr.json"
)
——————————————————————————————————
Функция main.
——————————————————————————————————
func main() {
fmt.Println("Server is running ...")
http.Handle("/static/", http.StripPrefix(
"/static/",
handleFileServer(http.Dir(STTC_PATH))),
)
http.HandleFunc("/", indexPage)
http.HandleFunc("/login", loginPage)
http.HandleFunc("/signup", signupPage)
http.HandleFunc("/logout", logoutPage)
http.HandleFunc("/account", accountPage)
http.HandleFunc("/transaction", transactionPage)
http.HandleFunc("/blockchain", blockchainPage)
http.HandleFunc("/blockchain/", blockchainXPage)
6http.ListenAndServe(":7545", nil)
}
——————————————————————————————————
Точка входа в программу. Сначала указывает директорию со статичными
файлами (css) при помощи константы STTC_PATH и функции
handleFileServer. Далее, для маршрутизации используются функции
indexPage , loginPage, signupPage, logoutPage, accountPage
(transactionPage, blockchainPage, blockchainXPage. В
конце запускается прослушивание порта 7545.
Константа STTC_PATH.
——————————————————————————————————
const (
STTC_PATH = "static/"
)
——————————————————————————————————
Функция handleFileServer.
——————————————————————————————————
func handleFileServer(fs http.FileSystem) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, err := fs.Open(r.URL.Path); os.IsNotExist(err) {
indexPage(w, r)
return
}
http.FileServer(fs).ServeHTTP(w, r)
})
}
——————————————————————————————————
Функция indexPage.
——————————————————————————————————
func indexPage(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles(
TMPL_PATH+"base.html",
TMPL_PATH+"index.html",
)
if err != nil {
panic("can't load hmtl files")
}
var data struct{
User *bc.User
}
data.User = User
t.Execute(w, data)
}
——————————————————————————————————
Использует константу TMPL_PATH и подгружает файлы
«base.html» и «index.html». Также используется глобальная
переменная User взятая из консольного интерфейса [1].
Функция loginPage.
——————————————————————————————————
func loginPage(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles(
TMPL_PATH+"base.html",
TMPL_PATH+"login.html",
)
if err != nil {
panic("can't load hmtl files")
}
var data struct{
User *bc.User
Error string
}
if r.Method == "POST" {
r.ParseForm()
User = bc.LoadUser(r.FormValue("private"))
if User == nil {
data.Error = "Load Private Key Error"
} else {
http.Redirect(w, r, "/", 302)
return
}
}
data.User = User
t.Execute(w, data)
}
——————————————————————————————————
Подгружает файлы «base.html» и «login.html».
Функция signupPage.
——————————————————————————————————
func signupPage(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles(
TMPL_PATH+"base.html",
TMPL_PATH+"signup.html",
)
if err != nil {
panic("can't load hmtl files")
}
var data struct{
User *bc.User
PrivateKey string
}
data.User = User
if r.Method == "POST" {
data.PrivateKey = bc.NewUser().Purse()
}
t.Execute(w, data)
}
——————————————————————————————————
Подгружает файлы «base.html» и «signup.html».
Функция logoutPage.
——————————————————————————————————
func logoutPage(w http.ResponseWriter, r *http.Request) {
User = nil
http.Redirect(w, r, "/", 302)
}
——————————————————————————————————
Функция accountPage.
——————————————————————————————————
func accountPage(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles(
TMPL_PATH+"base.html",
TMPL_PATH+"account.html",
)
if err != nil {
panic("can't load hmtl files")
}
var data struct{
User *bc.User
Address string
Balance string
}
data.User = User
if data.User != nil {
data.Address = User.Address()
res := nt.Send(Addresses[0], &nt.Package{
Option: GET_BLNCE,
Data: data.Address,
})
if res != nil {
data.Balance = res.Data
}
} else {
http.Redirect(w, r, "/", 302)
return
}
t.Execute(w, data)
}
——————————————————————————————————
Подгружает файлы «base.html» и «account.html».
Функция transactionPage.
——————————————————————————————————
func transactionPage(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles(
TMPL_PATH+"base.html",
TMPL_PATH+"transaction.html",
)
if err != nil {
panic("can't load hmtl files")
}
var data struct{
User *bc.User
Error string
}
data.User = User
if r.Method == "POST" {
r.ParseForm()
if data.User == nil {
data.Error = "User not authorizated"
t.Execute(w, data)
return
}
receiver := r.FormValue("receiver")
num, err := strconv.Atoi(r.FormValue("value"))
if err != nil {
data.Error = "strconv.Atoi error"
t.Execute(w, data)
return
}
flag := false
for _, addr := range Addresses {
res := nt.Send(addr, &nt.Package{
Option: GET_LHASH,
})
if res == nil {
continue
}
tx := bc.NewTransaction(User, bc.Base64Decode(res.Data), receiver,
uint64(num))
res = nt.Send(addr, &nt.Package{
Option: ADD_TRNSX,
Data: bc.SerializeTX(tx),
})
if res == nil || res.Data != "ok" {
continue
}
flag = true
}
if !flag {
data.Error = "TX failed"
} else {
data.Error = "TX success"
}
}
t.Execute(w, data)
}
——————————————————————————————————
Подгружает файлы «base.html» и «transaction.html».
Функция blockchainPage.
——————————————————————————————————
func blockchainPage(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles(
TMPL_PATH+"base.html",
TMPL_PATH+"blockchain.html",
)
if err != nil {
panic("can't load hmtl files")
}
var data struct{
Error string
Size []bool
Address string
Balance string
User *bc.User
}
data.User = User
if r.Method == "POST" {
data.Address = r.FormValue("address")
res := nt.Send(Addresses[0], &nt.Package{
Option: GET_BLNCE,
Data: data.Address,
})
if res != nil {
data.Balance = res.Data
}
}
res := nt.Send(Addresses[0], &nt.Package{
Option: GET_CSIZE,
})
if res == nil || res.Data == "" {
data.Error = "Receive error"
t.Execute(w, data)
return
}
num, err := strconv.Atoi(res.Data)
if err != nil {
data.Error = "strconv.Atoi error"
t.Execute(w, data)
return
}
data.Size = make([]bool, num)
t.Execute(w, data)
}
——————————————————————————————————
Подгружает файлы «base.html» и «blockchain.html»).
Функция blockchainXPage.
——————————————————————————————————
func blockchainXPage(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles(
TMPL_PATH+"base.html",
TMPL_PATH+"blockchainX.html",
)
if err != nil {
panic("can't load hmtl files")
}
var data struct{
Error string
Block *bc.Block
User *bc.User
}
data.User = User
res := nt.Send(Addresses[0], &nt.Package{
Option: GET_BLOCK,
Data: strings.Replace(r.URL.Path, "/blockchain/", "", 1),
})
if res == nil || res.Data == "" {
data.Error = "Receive error"
t.Execute(w, data)
return
}
data.Block = bc.DeserializeBlock(res.Data)
if data.Block == nil {
data.Error = "Block is nil"
t.Execute(w, data)
return
}
t.Execute(w, data)
}
——————————————————————————————————
Подгружает файлы «base.html» и «blockchainX.html».
Константа TMPL_PATH.
——————————————————————————————————
const (
TMPL_PATH = "templates/"
)
——————————————————————————————————
Используются следующие пакеты.
——————————————————————————————————
import (
"os"
"fmt"
nt "./network"
bc "./blockchain"
"net/http"
"strconv"
"strings"
"html/template"
"encoding/json"
)
——————————————————————————————————
Файл base.html.
——————————————————————————————————
^!DOCTYPE html^
^html^
^head^
^title^
{{block "title" .}}
Title
{{end}}
^/title^
^meta charset="utf-8"^
^link rel="stylesheet" type="text/css" href="/static/css/bootstrap.css"^
^/head^
^body^
^header^
^nav class="navbar navbar-expand-lg navbar-dark bg-dark"^
^div class="container"^
^a href="/" class="navbar-brand" exact^^h4^Home^/h4^^/a^
^div class="navbar-collapse"^
^ul class="navbar-nav"^
^a href="/blockchain" class="nav-link"^^h5^Blockchain^/h5^^/a^
^/ul^
^ul class="navbar-nav ml-auto"^
{{ if (not .User) }}
^a href="/signup" class="nav-link"^^h5>Signup^/h5^^/a^
^a href="/login" class="nav-link"^^h5^Login^/h5^^a^
{{ else }}
^a href="/transaction" class="nav-link"^^h5^Transaction^/h5^^/a^
^a href="/account" class="nav-link"^^h5^Account^/h5^^/a^
^a href="/logout" class="nav-link"^^h5^Logout^/h5^^/a^
{{ end }}
^/url^
^/div^
^/div^
^/nav^
^/header^
^main^
{{block "content" .}}
Content
{{end}}
^/main^
^/body^
^/html^
——————————————————————————————————
Файл index.html.
——————————————————————————————————
{{define "title"}}
Index
{{end}}
{{define "content"}}
^div class="col-md-9 mx-auto"^
^div class="jumbotron"^
^h1 class="text-center" ^Blockchain^/h1^
^/div^
^/div^
{{end}}
——————————————————————————————————
Файл login.html.
——————————————————————————————————
{{define "title"}}
Login
{{end}}
{{define "content"}}
{{ if .Error }}
^p^{{ .Error }}^/p^
{{ end }}
^div class="col-md-8 mx-auto"^
^div class="jumbotron"^
^div class="col-10 mx-auto"^
^form method="POST" action="/login"^
^div class="form-group"^
^input type="password" class="form-control" name="private" placeholder="Private
Key"^
^/div^
^input type="submit" class="btn btn-success w-100" name="login" value="Login"^
^/form^
^/div^
^/div^
^/div^
{{end}}
——————————————————————————————————
Файл signup.html.
——————————————————————————————————
{{define "title"}}
Signup
{{end}}
{{define "content"}}
^div class="col-md-8 mx-auto"^
^div class="jumbotron"^
^div class="col-10 mx-auto"^
^form method="POST" action="/signup"^
^div class="form-group"^
{{ if .PrivateKey }}
^input readonly type="text" class="form-control bg-light" value="{{ .PrivateKey
}}" id="private"^
{{ end }}
^/div^
^input type="submit" class="btn btn-success w-100" name="generate"
value="Generate Private Key"^
^/form^
^/div^
^/div^
^/div^
{{end}}
——————————————————————————————————
Файл account.html.
——————————————————————————————————
{{define "title"}}
Account
{{end}}
{{define "content"}}
^div class="col-md-9 mx-auto"^
^div class="jumbotron"^
^div class="col-12 mx-auto"^
^form^
^div class="form-group"^
^input id="address" readonly class="form-control bg-light" type="text"
name="coins" value="Address: {{ .Address }}"^
^/div^
^div class="form-group"^
^input disabled class="form-control bg-light" type="text" name="coins"
value="Balance: {{ if .Balance }} {{ .Balance }} {{ else }} 0 {{ end }} coins;"^
^/div^
^/form^
^/div^
^/div^
^/div^
{{end}}
——————————————————————————————————
Файл transaction.html.
——————————————————————————————————
{{define "title"}}
Transaction
{{end}}
{{define "content"}}
^div class="col-md-8 mx-auto"^
^div class="jumbotron"^
^div class="col-10 mx-auto"^
^form method="POST" action="/transaction"^
^div class="form-group"^
{{ if .Error }}
^input disabled class="form-control bg-light" type="text" name="state" value="{{
.Error }};"^
{{ end }}
^/div^
^div class="form-group"^
^input type="text" class="form-control" name="receiver" placeholder="Receiver"^
^/div^
^div class="form-group"^
^input type="number" class="form-control" name="value" placeholder="Value"^
^/div^
^input type="submit" class="btn btn-success w-100" name="submit" value="Send"^
^/form^
^/div^
^/div^
^/div^
{{end}}
——————————————————————————————————
Файл blockchain.html.
——————————————————————————————————
{{define "title"}}
BlockChain
{{end}}
{{define "content"}}
^div class="col-md-9 mx-auto"^
^div class="jumbotron"^
^div class="col-12 mx-auto"^
^form method="POST" action="/blockchain"^
^div class="form-group"^
{{ if .Address }}
^input readonly class="form-control bg-light" type="text" name="coins"
value="Address: {{ .Address }}"^
^input disabled class="form-control bg-light" type="text" name="coins"
value="Balance: {{ .Balance }} coins;"^
{{ end }}
^/div^
^div class="form-group"^
^input type="text" class="form-control" name="address" placeholder="Address"^
^/div^
^input type="submit" class="btn btn-success w-100" name="submit"
value="Balance"^
^/form^
^/div^
^/div^
^div class="jumbotron"^
{{ if .Error }}
^p^{{ .Error }}^/p^
{{ else }}
{{ range $i, $e := .Size }}
^div class="card"^
^a class="btn btn-info" href="/blockchain/{{$i}}"^[Block {{ $i }}]^/a^
^/div^
{{ end }}
{{ end }}
^/div^
^/div^
{{end}}
——————————————————————————————————
Файл blockchainX.html.
——————————————————————————————————
{{define "title"}}
Block
{{end}}
{{define "content"}}
{{ if .Error }}
^p^{{ .Error }}^/p^
{{ else }}
^table border="1"^
^tr^
^th^Nonce^/th^
^td width="100%"^{{ .Block.Nonce }}^/td^
^/tr^
^tr^
^th^Difficulty^/th^
^td width="100%"^{{ .Block.Difficulty }}^/td^
^/tr^
^tr^
^th^CurrHash^/th^
^td width="100%"^{{ .Block.CurrHash }}^/td^
^/tr^
^tr^
^th^PrevHash^/th^
^td width="100%"^{{ .Block.PrevHash }}^/td^
^/tr^
^tr^
^th^Miner^/th^
^td width="100%"^{{ .Block.Miner }}^/td^
^/tr^
^tr^
^th^Signature^/th^
^td width="100%"^{{ .Block.Signature }}^/td^
^/tr^
^tr^
^th^TimeStamp^/th^
^td width="100%"^{{ .Block.TimeStamp }}^/td^
^/tr^
^tr^
^th^Transactions^/th^
^table border="1"^
^tr^
^th^RandBytes^/th^
^td width="100%"^{{ .RandBytes }}^/td^
^/tr^
^tr^
^th^PrevBlock^/th^
^td width="100%"^{{ .PrevBlock }}^/td^
^/tr^
^tr^
^th^Sender^/th^
^td width="100%"^{{ .Sender }}^/td^
^/tr^
^tr^
^th^Receiver^/th^
^td width="100%"^{{ .Receiver }}^/td^
^/tr^
^tr^
^th^Value^/th^
^td width="100%"^{{ .Value }}^/td^
^/tr^
^tr^
^th^ToStorage^/th^
^td width="100%"^{{ .ToStorage }}^/td^
^/tr^
^tr^
^th^CurrHash^/th^
^td width="100%"^{{ .CurrHash }}^/td^
^/tr^
^tr^
^th^Signature^/th^
^td width="100%"^{{ .Signature }}^/td^
^/tr^
^/table^
{{ end }}
^/td^
^/tr^
^tr^
^th^Mapping^/th^
^td^
{{ range $k, $e := .Block.Mapping }}
^table border="1"^
^tr^
^th width="100%"^{{ $k }}^/th^
^td^{{ $e }}^/td^
^/tr^
^/table^
{{ end }}
^/td^
^/tr^
^/table^
{{ end }}
{{end}}
——————————————————————————————————
Продолжение следует...