20741a96ac
Please be aware that if any of the instances on xeth.frontend are called the program will crash due to the default, temporarily, frontend interface.
522 lines
12 KiB
Go
522 lines
12 KiB
Go
/*
|
|
This file is part of go-ethereum
|
|
|
|
go-ethereum is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
go-ethereum is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
/**
|
|
* @authors
|
|
* Jeffrey Wilcke <i@jev.io>
|
|
*/
|
|
package main
|
|
|
|
import "C"
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"math/big"
|
|
"os"
|
|
"path"
|
|
"runtime"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/ethereum/go-ethereum/core"
|
|
"github.com/ethereum/go-ethereum/core/types"
|
|
"github.com/ethereum/go-ethereum/eth"
|
|
"github.com/ethereum/go-ethereum/ethdb"
|
|
"github.com/ethereum/go-ethereum/ethutil"
|
|
"github.com/ethereum/go-ethereum/logger"
|
|
"github.com/ethereum/go-ethereum/ui/qt/qwhisper"
|
|
"github.com/ethereum/go-ethereum/xeth"
|
|
"github.com/obscuren/qml"
|
|
)
|
|
|
|
var guilogger = logger.NewLogger("GUI")
|
|
|
|
type ServEv byte
|
|
|
|
const (
|
|
setup ServEv = iota
|
|
update
|
|
)
|
|
|
|
type Gui struct {
|
|
// The main application window
|
|
win *qml.Window
|
|
// QML Engine
|
|
engine *qml.Engine
|
|
component *qml.Common
|
|
// The ethereum interface
|
|
eth *eth.Ethereum
|
|
serviceEvents chan ServEv
|
|
|
|
// The public Ethereum library
|
|
uiLib *UiLib
|
|
whisper *qwhisper.Whisper
|
|
|
|
txDb *ethdb.LDBDatabase
|
|
|
|
logLevel logger.LogLevel
|
|
open bool
|
|
|
|
xeth *xeth.XEth
|
|
|
|
Session string
|
|
config *ethutil.ConfigManager
|
|
|
|
plugins map[string]plugin
|
|
}
|
|
|
|
// Create GUI, but doesn't start it
|
|
func NewWindow(ethereum *eth.Ethereum, config *ethutil.ConfigManager, session string, logLevel int) *Gui {
|
|
db, err := ethdb.NewLDBDatabase("tx_database")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
xeth := xeth.New(ethereum, nil)
|
|
gui := &Gui{eth: ethereum,
|
|
txDb: db,
|
|
xeth: xeth,
|
|
logLevel: logger.LogLevel(logLevel),
|
|
Session: session,
|
|
open: false,
|
|
config: config,
|
|
plugins: make(map[string]plugin),
|
|
serviceEvents: make(chan ServEv, 1),
|
|
}
|
|
data, _ := ethutil.ReadAllFile(path.Join(ethereum.DataDir, "plugins.json"))
|
|
json.Unmarshal([]byte(data), &gui.plugins)
|
|
|
|
return gui
|
|
}
|
|
|
|
func (gui *Gui) Start(assetPath string) {
|
|
defer gui.txDb.Close()
|
|
|
|
guilogger.Infoln("Starting GUI")
|
|
|
|
go gui.service()
|
|
|
|
// Register ethereum functions
|
|
qml.RegisterTypes("Ethereum", 1, 0, []qml.TypeSpec{{
|
|
Init: func(p *xeth.Block, obj qml.Object) { p.Number = 0; p.Hash = "" },
|
|
}, {
|
|
Init: func(p *xeth.Transaction, obj qml.Object) { p.Value = ""; p.Hash = ""; p.Address = "" },
|
|
}, {
|
|
Init: func(p *xeth.KeyVal, obj qml.Object) { p.Key = ""; p.Value = "" },
|
|
}})
|
|
// Create a new QML engine
|
|
gui.engine = qml.NewEngine()
|
|
context := gui.engine.Context()
|
|
gui.uiLib = NewUiLib(gui.engine, gui.eth, assetPath)
|
|
gui.whisper = qwhisper.New(gui.eth.Whisper())
|
|
|
|
// Expose the eth library and the ui library to QML
|
|
context.SetVar("gui", gui)
|
|
context.SetVar("eth", gui.uiLib)
|
|
context.SetVar("shh", gui.whisper)
|
|
//clipboard.SetQMLClipboard(context)
|
|
|
|
win, err := gui.showWallet(context)
|
|
if err != nil {
|
|
guilogger.Errorln("asset not found: you can set an alternative asset path on the command line using option 'asset_path'", err)
|
|
|
|
panic(err)
|
|
}
|
|
|
|
gui.open = true
|
|
win.Show()
|
|
|
|
// only add the gui guilogger after window is shown otherwise slider wont be shown
|
|
logger.AddLogSystem(gui)
|
|
win.Wait()
|
|
|
|
// need to silence gui guilogger after window closed otherwise logsystem hangs (but do not save loglevel)
|
|
gui.logLevel = logger.Silence
|
|
gui.open = false
|
|
}
|
|
|
|
func (gui *Gui) Stop() {
|
|
if gui.open {
|
|
gui.logLevel = logger.Silence
|
|
gui.open = false
|
|
gui.win.Hide()
|
|
}
|
|
|
|
guilogger.Infoln("Stopped")
|
|
}
|
|
|
|
func (gui *Gui) showWallet(context *qml.Context) (*qml.Window, error) {
|
|
component, err := gui.engine.LoadFile(gui.uiLib.AssetPath("qml/main.qml"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gui.createWindow(component)
|
|
|
|
return gui.win, nil
|
|
}
|
|
|
|
func (gui *Gui) ImportKey(filePath string) {
|
|
}
|
|
|
|
func (gui *Gui) showKeyImport(context *qml.Context) (*qml.Window, error) {
|
|
context.SetVar("lib", gui)
|
|
component, err := gui.engine.LoadFile(gui.uiLib.AssetPath("qml/first_run.qml"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return gui.createWindow(component), nil
|
|
}
|
|
|
|
func (gui *Gui) createWindow(comp qml.Object) *qml.Window {
|
|
gui.win = comp.CreateWindow(nil)
|
|
gui.uiLib.win = gui.win
|
|
|
|
return gui.win
|
|
}
|
|
|
|
func (gui *Gui) ImportAndSetPrivKey(secret string) bool {
|
|
err := gui.eth.KeyManager().InitFromString(gui.Session, 0, secret)
|
|
if err != nil {
|
|
guilogger.Errorln("unable to import: ", err)
|
|
return false
|
|
}
|
|
guilogger.Errorln("successfully imported: ", err)
|
|
return true
|
|
}
|
|
|
|
func (gui *Gui) CreateAndSetPrivKey() (string, string, string, string) {
|
|
err := gui.eth.KeyManager().Init(gui.Session, 0, true)
|
|
if err != nil {
|
|
guilogger.Errorln("unable to create key: ", err)
|
|
return "", "", "", ""
|
|
}
|
|
return gui.eth.KeyManager().KeyPair().AsStrings()
|
|
}
|
|
|
|
func (gui *Gui) setInitialChain(ancientBlocks bool) {
|
|
sBlk := gui.eth.ChainManager().LastBlockHash()
|
|
blk := gui.eth.ChainManager().GetBlock(sBlk)
|
|
for ; blk != nil; blk = gui.eth.ChainManager().GetBlock(sBlk) {
|
|
sBlk = blk.ParentHash()
|
|
|
|
gui.processBlock(blk, true)
|
|
}
|
|
}
|
|
|
|
func (gui *Gui) loadAddressBook() {
|
|
/*
|
|
view := gui.getObjectByName("infoView")
|
|
nameReg := gui.xeth.World().Config().Get("NameReg")
|
|
if nameReg != nil {
|
|
it := nameReg.Trie().Iterator()
|
|
for it.Next() {
|
|
if it.Key[0] != 0 {
|
|
view.Call("addAddress", struct{ Name, Address string }{string(it.Key), ethutil.Bytes2Hex(it.Value)})
|
|
}
|
|
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
|
|
func (self *Gui) loadMergedMiningOptions() {
|
|
/*
|
|
view := self.getObjectByName("mergedMiningModel")
|
|
|
|
mergeMining := self.xeth.World().Config().Get("MergeMining")
|
|
if mergeMining != nil {
|
|
i := 0
|
|
it := mergeMining.Trie().Iterator()
|
|
for it.Next() {
|
|
view.Call("addMergedMiningOption", struct {
|
|
Checked bool
|
|
Name, Address string
|
|
Id, ItemId int
|
|
}{false, string(it.Key), ethutil.Bytes2Hex(it.Value), 0, i})
|
|
|
|
i++
|
|
|
|
}
|
|
}
|
|
*/
|
|
}
|
|
|
|
func (gui *Gui) insertTransaction(window string, tx *types.Transaction) {
|
|
addr := gui.address()
|
|
|
|
var inout string
|
|
if bytes.Compare(tx.From(), addr) == 0 {
|
|
inout = "send"
|
|
} else {
|
|
inout = "recv"
|
|
}
|
|
|
|
var (
|
|
ptx = xeth.NewTx(tx)
|
|
send = ethutil.Bytes2Hex(tx.From())
|
|
rec = ethutil.Bytes2Hex(tx.To())
|
|
)
|
|
ptx.Sender = send
|
|
ptx.Address = rec
|
|
|
|
if window == "post" {
|
|
//gui.getObjectByName("transactionView").Call("addTx", ptx, inout)
|
|
} else {
|
|
gui.getObjectByName("pendingTxView").Call("addTx", ptx, inout)
|
|
}
|
|
}
|
|
|
|
func (gui *Gui) readPreviousTransactions() {
|
|
it := gui.txDb.NewIterator()
|
|
for it.Next() {
|
|
tx := types.NewTransactionFromBytes(it.Value())
|
|
|
|
gui.insertTransaction("post", tx)
|
|
|
|
}
|
|
it.Release()
|
|
}
|
|
|
|
func (gui *Gui) processBlock(block *types.Block, initial bool) {
|
|
name := ethutil.Bytes2Hex(block.Coinbase())
|
|
b := xeth.NewBlock(block)
|
|
b.Name = name
|
|
|
|
gui.getObjectByName("chainView").Call("addBlock", b, initial)
|
|
}
|
|
|
|
func (gui *Gui) setWalletValue(amount, unconfirmedFunds *big.Int) {
|
|
var str string
|
|
if unconfirmedFunds != nil {
|
|
pos := "+"
|
|
if unconfirmedFunds.Cmp(big.NewInt(0)) < 0 {
|
|
pos = "-"
|
|
}
|
|
val := ethutil.CurrencyToString(new(big.Int).Abs(ethutil.BigCopy(unconfirmedFunds)))
|
|
str = fmt.Sprintf("%v (%s %v)", ethutil.CurrencyToString(amount), pos, val)
|
|
} else {
|
|
str = fmt.Sprintf("%v", ethutil.CurrencyToString(amount))
|
|
}
|
|
|
|
gui.win.Root().Call("setWalletValue", str)
|
|
}
|
|
|
|
func (self *Gui) getObjectByName(objectName string) qml.Object {
|
|
return self.win.Root().ObjectByName(objectName)
|
|
}
|
|
|
|
func loadJavascriptAssets(gui *Gui) (jsfiles string) {
|
|
for _, fn := range []string{"ext/q.js", "ext/eth.js/main.js", "ext/eth.js/qt.js", "ext/setup.js"} {
|
|
f, err := os.Open(gui.uiLib.AssetPath(fn))
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
continue
|
|
}
|
|
|
|
content, err := ioutil.ReadAll(f)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
continue
|
|
}
|
|
jsfiles += string(content)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (gui *Gui) SendCommand(cmd ServEv) {
|
|
gui.serviceEvents <- cmd
|
|
}
|
|
|
|
func (gui *Gui) service() {
|
|
for ev := range gui.serviceEvents {
|
|
switch ev {
|
|
case setup:
|
|
go gui.setup()
|
|
case update:
|
|
go gui.update()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (gui *Gui) setup() {
|
|
for gui.win == nil {
|
|
time.Sleep(time.Millisecond * 200)
|
|
}
|
|
|
|
for _, plugin := range gui.plugins {
|
|
guilogger.Infoln("Loading plugin ", plugin.Name)
|
|
gui.win.Root().Call("addPlugin", plugin.Path, "")
|
|
}
|
|
|
|
go func() {
|
|
go gui.setInitialChain(false)
|
|
gui.loadAddressBook()
|
|
gui.loadMergedMiningOptions()
|
|
gui.setPeerInfo()
|
|
}()
|
|
|
|
gui.whisper.SetView(gui.getObjectByName("whisperView"))
|
|
|
|
gui.SendCommand(update)
|
|
}
|
|
|
|
// Simple go routine function that updates the list of peers in the GUI
|
|
func (gui *Gui) update() {
|
|
peerUpdateTicker := time.NewTicker(5 * time.Second)
|
|
generalUpdateTicker := time.NewTicker(500 * time.Millisecond)
|
|
statsUpdateTicker := time.NewTicker(5 * time.Second)
|
|
|
|
lastBlockLabel := gui.getObjectByName("lastBlockLabel")
|
|
//miningLabel := gui.getObjectByName("miningLabel")
|
|
|
|
events := gui.eth.EventMux().Subscribe(
|
|
core.ChainEvent{},
|
|
core.TxPreEvent{},
|
|
core.TxPostEvent{},
|
|
)
|
|
|
|
defer events.Unsubscribe()
|
|
for {
|
|
select {
|
|
case ev, isopen := <-events.Chan():
|
|
if !isopen {
|
|
return
|
|
}
|
|
switch ev := ev.(type) {
|
|
case core.ChainEvent:
|
|
gui.processBlock(ev.Block, false)
|
|
case core.TxPreEvent:
|
|
gui.insertTransaction("pre", ev.Tx)
|
|
|
|
case core.TxPostEvent:
|
|
gui.getObjectByName("pendingTxView").Call("removeTx", xeth.NewTx(ev.Tx))
|
|
}
|
|
|
|
case <-peerUpdateTicker.C:
|
|
gui.setPeerInfo()
|
|
|
|
case <-generalUpdateTicker.C:
|
|
statusText := "#" + gui.eth.ChainManager().CurrentBlock().Number().String()
|
|
lastBlockLabel.Set("text", statusText)
|
|
//miningLabel.Set("text", strconv.FormatInt(gui.uiLib.Miner().HashRate(), 10))
|
|
case <-statsUpdateTicker.C:
|
|
gui.setStatsPane()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (gui *Gui) setStatsPane() {
|
|
var memStats runtime.MemStats
|
|
runtime.ReadMemStats(&memStats)
|
|
|
|
statsPane := gui.getObjectByName("statsPane")
|
|
statsPane.Set("text", fmt.Sprintf(`###### Mist %s (%s) #######
|
|
|
|
eth %d (p2p = %d)
|
|
|
|
CPU: # %d
|
|
Goroutines: # %d
|
|
CGoCalls: # %d
|
|
|
|
Alloc: %d
|
|
Heap Alloc: %d
|
|
|
|
CGNext: %x
|
|
NumGC: %d
|
|
`, Version, runtime.Version(),
|
|
eth.ProtocolVersion, 2,
|
|
runtime.NumCPU, runtime.NumGoroutine(), runtime.NumCgoCall(),
|
|
memStats.Alloc, memStats.HeapAlloc,
|
|
memStats.NextGC, memStats.NumGC,
|
|
))
|
|
}
|
|
|
|
type qmlpeer struct{ Addr, NodeID, Name, Caps string }
|
|
|
|
type peersByID []*qmlpeer
|
|
|
|
func (s peersByID) Len() int { return len(s) }
|
|
func (s peersByID) Less(i, j int) bool { return s[i].NodeID < s[j].NodeID }
|
|
func (s peersByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
|
|
|
func (gui *Gui) setPeerInfo() {
|
|
peers := gui.eth.Peers()
|
|
qpeers := make(peersByID, len(peers))
|
|
for i, p := range peers {
|
|
qpeers[i] = &qmlpeer{
|
|
NodeID: p.ID().String(),
|
|
Addr: p.RemoteAddr().String(),
|
|
Name: p.Name(),
|
|
Caps: fmt.Sprint(p.Caps()),
|
|
}
|
|
}
|
|
// we need to sort the peers because they jump around randomly
|
|
// otherwise. order returned by eth.Peers is random because they
|
|
// are taken from a map.
|
|
sort.Sort(qpeers)
|
|
|
|
gui.win.Root().Call("setPeerCounters", fmt.Sprintf("%d / %d", len(peers), gui.eth.MaxPeers()))
|
|
gui.win.Root().Call("clearPeers")
|
|
for _, p := range qpeers {
|
|
gui.win.Root().Call("addPeer", p)
|
|
}
|
|
}
|
|
|
|
func (gui *Gui) privateKey() string {
|
|
return ethutil.Bytes2Hex(gui.eth.KeyManager().PrivateKey())
|
|
}
|
|
|
|
func (gui *Gui) address() []byte {
|
|
return gui.eth.KeyManager().Address()
|
|
}
|
|
|
|
/*
|
|
func LoadExtension(path string) (uintptr, error) {
|
|
lib, err := ffi.NewLibrary(path)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
so, err := lib.Fct("sharedObject", ffi.Pointer, nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
ptr := so()
|
|
|
|
err = lib.Close()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
return ptr.Interface().(uintptr), nil
|
|
}
|
|
*/
|
|
/*
|
|
vec, errr := LoadExtension("/Users/jeffrey/Desktop/build-libqmltest-Desktop_Qt_5_2_1_clang_64bit-Debug/liblibqmltest_debug.dylib")
|
|
fmt.Printf("Fetched vec with addr: %#x\n", vec)
|
|
if errr != nil {
|
|
fmt.Println(errr)
|
|
} else {
|
|
context.SetVar("vec", (unsafe.Pointer)(vec))
|
|
}
|
|
*/
|