github.com/jonasnick/go-ethereum@v0.7.12-0.20150216215225-22176f05d387/cmd/mist/gui.go (about) 1 /* 2 This file is part of go-ethereum 3 4 go-ethereum is free software: you can redistribute it and/or modify 5 it under the terms of the GNU General Public License as published by 6 the Free Software Foundation, either version 3 of the License, or 7 (at your option) any later version. 8 9 go-ethereum is distributed in the hope that it will be useful, 10 but WITHOUT ANY WARRANTY; without even the implied warranty of 11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 GNU General Public License for more details. 13 14 You should have received a copy of the GNU General Public License 15 along with go-ethereum. If not, see <http://www.gnu.org/licenses/>. 16 */ 17 /** 18 * @authors 19 * Jeffrey Wilcke <i@jev.io> 20 */ 21 package main 22 23 import "C" 24 25 import ( 26 "bytes" 27 "encoding/json" 28 "fmt" 29 "io/ioutil" 30 "math/big" 31 "os" 32 "path" 33 "runtime" 34 "sort" 35 "strconv" 36 "time" 37 38 "github.com/jonasnick/go-ethereum/core" 39 "github.com/jonasnick/go-ethereum/core/types" 40 "github.com/jonasnick/go-ethereum/eth" 41 "github.com/jonasnick/go-ethereum/ethdb" 42 "github.com/jonasnick/go-ethereum/ethutil" 43 "github.com/jonasnick/go-ethereum/logger" 44 "github.com/jonasnick/go-ethereum/miner" 45 "github.com/jonasnick/go-ethereum/ui/qt/qwhisper" 46 "github.com/jonasnick/go-ethereum/xeth" 47 "github.com/obscuren/qml" 48 ) 49 50 var guilogger = logger.NewLogger("GUI") 51 52 type ServEv byte 53 54 const ( 55 setup ServEv = iota 56 update 57 ) 58 59 type Gui struct { 60 // The main application window 61 win *qml.Window 62 // QML Engine 63 engine *qml.Engine 64 component *qml.Common 65 // The ethereum interface 66 eth *eth.Ethereum 67 serviceEvents chan ServEv 68 69 // The public Ethereum library 70 uiLib *UiLib 71 whisper *qwhisper.Whisper 72 73 txDb *ethdb.LDBDatabase 74 75 logLevel logger.LogLevel 76 open bool 77 78 xeth *xeth.XEth 79 80 Session string 81 config *ethutil.ConfigManager 82 83 plugins map[string]plugin 84 85 miner *miner.Miner 86 } 87 88 // Create GUI, but doesn't start it 89 func NewWindow(ethereum *eth.Ethereum, config *ethutil.ConfigManager, session string, logLevel int) *Gui { 90 db, err := ethdb.NewLDBDatabase("tx_database") 91 if err != nil { 92 panic(err) 93 } 94 95 xeth := xeth.New(ethereum) 96 gui := &Gui{eth: ethereum, 97 txDb: db, 98 xeth: xeth, 99 logLevel: logger.LogLevel(logLevel), 100 Session: session, 101 open: false, 102 config: config, 103 plugins: make(map[string]plugin), 104 serviceEvents: make(chan ServEv, 1), 105 } 106 data, _ := ethutil.ReadAllFile(path.Join(ethutil.Config.ExecPath, "plugins.json")) 107 json.Unmarshal([]byte(data), &gui.plugins) 108 109 return gui 110 } 111 112 func (gui *Gui) Start(assetPath string) { 113 defer gui.txDb.Close() 114 115 guilogger.Infoln("Starting GUI") 116 117 go gui.service() 118 119 // Register ethereum functions 120 qml.RegisterTypes("Ethereum", 1, 0, []qml.TypeSpec{{ 121 Init: func(p *xeth.Block, obj qml.Object) { p.Number = 0; p.Hash = "" }, 122 }, { 123 Init: func(p *xeth.Transaction, obj qml.Object) { p.Value = ""; p.Hash = ""; p.Address = "" }, 124 }, { 125 Init: func(p *xeth.KeyVal, obj qml.Object) { p.Key = ""; p.Value = "" }, 126 }}) 127 // Create a new QML engine 128 gui.engine = qml.NewEngine() 129 context := gui.engine.Context() 130 gui.uiLib = NewUiLib(gui.engine, gui.eth, assetPath) 131 gui.whisper = qwhisper.New(gui.eth.Whisper()) 132 133 // Expose the eth library and the ui library to QML 134 context.SetVar("gui", gui) 135 context.SetVar("eth", gui.uiLib) 136 context.SetVar("shh", gui.whisper) 137 138 win, err := gui.showWallet(context) 139 if err != nil { 140 guilogger.Errorln("asset not found: you can set an alternative asset path on the command line using option 'asset_path'", err) 141 142 panic(err) 143 } 144 145 gui.open = true 146 win.Show() 147 148 // only add the gui guilogger after window is shown otherwise slider wont be shown 149 logger.AddLogSystem(gui) 150 win.Wait() 151 152 // need to silence gui guilogger after window closed otherwise logsystem hangs (but do not save loglevel) 153 gui.logLevel = logger.Silence 154 gui.open = false 155 } 156 157 func (gui *Gui) Stop() { 158 if gui.open { 159 gui.logLevel = logger.Silence 160 gui.open = false 161 gui.win.Hide() 162 } 163 164 gui.uiLib.jsEngine.Stop() 165 166 guilogger.Infoln("Stopped") 167 } 168 169 func (gui *Gui) showWallet(context *qml.Context) (*qml.Window, error) { 170 component, err := gui.engine.LoadFile(gui.uiLib.AssetPath("qml/main.qml")) 171 if err != nil { 172 return nil, err 173 } 174 175 gui.createWindow(component) 176 177 return gui.win, nil 178 } 179 180 func (gui *Gui) ImportKey(filePath string) { 181 } 182 183 func (gui *Gui) showKeyImport(context *qml.Context) (*qml.Window, error) { 184 context.SetVar("lib", gui) 185 component, err := gui.engine.LoadFile(gui.uiLib.AssetPath("qml/first_run.qml")) 186 if err != nil { 187 return nil, err 188 } 189 return gui.createWindow(component), nil 190 } 191 192 func (gui *Gui) createWindow(comp qml.Object) *qml.Window { 193 gui.win = comp.CreateWindow(nil) 194 gui.uiLib.win = gui.win 195 196 return gui.win 197 } 198 199 func (gui *Gui) ImportAndSetPrivKey(secret string) bool { 200 err := gui.eth.KeyManager().InitFromString(gui.Session, 0, secret) 201 if err != nil { 202 guilogger.Errorln("unable to import: ", err) 203 return false 204 } 205 guilogger.Errorln("successfully imported: ", err) 206 return true 207 } 208 209 func (gui *Gui) CreateAndSetPrivKey() (string, string, string, string) { 210 err := gui.eth.KeyManager().Init(gui.Session, 0, true) 211 if err != nil { 212 guilogger.Errorln("unable to create key: ", err) 213 return "", "", "", "" 214 } 215 return gui.eth.KeyManager().KeyPair().AsStrings() 216 } 217 218 func (gui *Gui) setInitialChain(ancientBlocks bool) { 219 sBlk := gui.eth.ChainManager().LastBlockHash() 220 blk := gui.eth.ChainManager().GetBlock(sBlk) 221 for ; blk != nil; blk = gui.eth.ChainManager().GetBlock(sBlk) { 222 sBlk = blk.ParentHash() 223 224 gui.processBlock(blk, true) 225 } 226 } 227 228 func (gui *Gui) loadAddressBook() { 229 /* 230 view := gui.getObjectByName("infoView") 231 nameReg := gui.xeth.World().Config().Get("NameReg") 232 if nameReg != nil { 233 it := nameReg.Trie().Iterator() 234 for it.Next() { 235 if it.Key[0] != 0 { 236 view.Call("addAddress", struct{ Name, Address string }{string(it.Key), ethutil.Bytes2Hex(it.Value)}) 237 } 238 239 } 240 } 241 */ 242 } 243 244 func (self *Gui) loadMergedMiningOptions() { 245 /* 246 view := self.getObjectByName("mergedMiningModel") 247 248 mergeMining := self.xeth.World().Config().Get("MergeMining") 249 if mergeMining != nil { 250 i := 0 251 it := mergeMining.Trie().Iterator() 252 for it.Next() { 253 view.Call("addMergedMiningOption", struct { 254 Checked bool 255 Name, Address string 256 Id, ItemId int 257 }{false, string(it.Key), ethutil.Bytes2Hex(it.Value), 0, i}) 258 259 i++ 260 261 } 262 } 263 */ 264 } 265 266 func (gui *Gui) insertTransaction(window string, tx *types.Transaction) { 267 addr := gui.address() 268 269 var inout string 270 if bytes.Compare(tx.From(), addr) == 0 { 271 inout = "send" 272 } else { 273 inout = "recv" 274 } 275 276 var ( 277 ptx = xeth.NewTx(tx) 278 send = ethutil.Bytes2Hex(tx.From()) 279 rec = ethutil.Bytes2Hex(tx.To()) 280 ) 281 ptx.Sender = send 282 ptx.Address = rec 283 284 if window == "post" { 285 //gui.getObjectByName("transactionView").Call("addTx", ptx, inout) 286 } else { 287 gui.getObjectByName("pendingTxView").Call("addTx", ptx, inout) 288 } 289 } 290 291 func (gui *Gui) readPreviousTransactions() { 292 it := gui.txDb.NewIterator() 293 for it.Next() { 294 tx := types.NewTransactionFromBytes(it.Value()) 295 296 gui.insertTransaction("post", tx) 297 298 } 299 it.Release() 300 } 301 302 func (gui *Gui) processBlock(block *types.Block, initial bool) { 303 name := ethutil.Bytes2Hex(block.Coinbase()) 304 b := xeth.NewBlock(block) 305 b.Name = name 306 307 gui.getObjectByName("chainView").Call("addBlock", b, initial) 308 } 309 310 func (gui *Gui) setWalletValue(amount, unconfirmedFunds *big.Int) { 311 var str string 312 if unconfirmedFunds != nil { 313 pos := "+" 314 if unconfirmedFunds.Cmp(big.NewInt(0)) < 0 { 315 pos = "-" 316 } 317 val := ethutil.CurrencyToString(new(big.Int).Abs(ethutil.BigCopy(unconfirmedFunds))) 318 str = fmt.Sprintf("%v (%s %v)", ethutil.CurrencyToString(amount), pos, val) 319 } else { 320 str = fmt.Sprintf("%v", ethutil.CurrencyToString(amount)) 321 } 322 323 gui.win.Root().Call("setWalletValue", str) 324 } 325 326 func (self *Gui) getObjectByName(objectName string) qml.Object { 327 return self.win.Root().ObjectByName(objectName) 328 } 329 330 func loadJavascriptAssets(gui *Gui) (jsfiles string) { 331 for _, fn := range []string{"ext/q.js", "ext/eth.js/main.js", "ext/eth.js/qt.js", "ext/setup.js"} { 332 f, err := os.Open(gui.uiLib.AssetPath(fn)) 333 if err != nil { 334 fmt.Println(err) 335 continue 336 } 337 338 content, err := ioutil.ReadAll(f) 339 if err != nil { 340 fmt.Println(err) 341 continue 342 } 343 jsfiles += string(content) 344 } 345 346 return 347 } 348 349 func (gui *Gui) SendCommand(cmd ServEv) { 350 gui.serviceEvents <- cmd 351 } 352 353 func (gui *Gui) service() { 354 for ev := range gui.serviceEvents { 355 switch ev { 356 case setup: 357 go gui.setup() 358 case update: 359 go gui.update() 360 } 361 } 362 } 363 364 func (gui *Gui) setup() { 365 for gui.win == nil { 366 time.Sleep(time.Millisecond * 200) 367 } 368 369 for _, plugin := range gui.plugins { 370 guilogger.Infoln("Loading plugin ", plugin.Name) 371 gui.win.Root().Call("addPlugin", plugin.Path, "") 372 } 373 374 go func() { 375 go gui.setInitialChain(false) 376 gui.loadAddressBook() 377 gui.loadMergedMiningOptions() 378 gui.setPeerInfo() 379 }() 380 381 gui.whisper.SetView(gui.getObjectByName("whisperView")) 382 383 gui.SendCommand(update) 384 } 385 386 // Simple go routine function that updates the list of peers in the GUI 387 func (gui *Gui) update() { 388 peerUpdateTicker := time.NewTicker(5 * time.Second) 389 generalUpdateTicker := time.NewTicker(500 * time.Millisecond) 390 statsUpdateTicker := time.NewTicker(5 * time.Second) 391 392 state := gui.eth.ChainManager().TransState() 393 394 gui.win.Root().Call("setWalletValue", fmt.Sprintf("%v", ethutil.CurrencyToString(state.GetAccount(gui.address()).Balance()))) 395 396 lastBlockLabel := gui.getObjectByName("lastBlockLabel") 397 miningLabel := gui.getObjectByName("miningLabel") 398 399 events := gui.eth.EventMux().Subscribe( 400 //eth.PeerListEvent{}, 401 core.NewBlockEvent{}, 402 core.TxPreEvent{}, 403 core.TxPostEvent{}, 404 ) 405 406 defer events.Unsubscribe() 407 for { 408 select { 409 case ev, isopen := <-events.Chan(): 410 if !isopen { 411 return 412 } 413 switch ev := ev.(type) { 414 case core.NewBlockEvent: 415 gui.processBlock(ev.Block, false) 416 //gui.setWalletValue(gui.eth.ChainManager().State().GetBalance(gui.address()), nil) 417 balance := ethutil.CurrencyToString(gui.eth.ChainManager().State().GetBalance(gui.address())) 418 gui.getObjectByName("balanceLabel").Set("text", fmt.Sprintf("%v", balance)) 419 420 case core.TxPreEvent: 421 tx := ev.Tx 422 423 tstate := gui.eth.ChainManager().TransState() 424 cstate := gui.eth.ChainManager().State() 425 426 taccount := tstate.GetAccount(gui.address()) 427 caccount := cstate.GetAccount(gui.address()) 428 unconfirmedFunds := new(big.Int).Sub(taccount.Balance(), caccount.Balance()) 429 430 gui.setWalletValue(taccount.Balance(), unconfirmedFunds) 431 gui.insertTransaction("pre", tx) 432 433 case core.TxPostEvent: 434 tx := ev.Tx 435 object := state.GetAccount(gui.address()) 436 437 if bytes.Compare(tx.From(), gui.address()) == 0 { 438 object.SubAmount(tx.Value()) 439 440 gui.txDb.Put(tx.Hash(), tx.RlpEncode()) 441 } else if bytes.Compare(tx.To(), gui.address()) == 0 { 442 object.AddAmount(tx.Value()) 443 444 gui.txDb.Put(tx.Hash(), tx.RlpEncode()) 445 } 446 447 gui.setWalletValue(object.Balance(), nil) 448 state.UpdateStateObject(object) 449 } 450 451 case <-peerUpdateTicker.C: 452 gui.setPeerInfo() 453 454 case <-generalUpdateTicker.C: 455 statusText := "#" + gui.eth.ChainManager().CurrentBlock().Number().String() 456 lastBlockLabel.Set("text", statusText) 457 miningLabel.Set("text", "Mining @ "+strconv.FormatInt(gui.uiLib.miner.HashRate(), 10)+"/Khash") 458 459 /* 460 blockLength := gui.eth.BlockPool().BlocksProcessed 461 chainLength := gui.eth.BlockPool().ChainLength 462 463 var ( 464 pct float64 = 1.0 / float64(chainLength) * float64(blockLength) 465 dlWidget = gui.win.Root().ObjectByName("downloadIndicator") 466 dlLabel = gui.win.Root().ObjectByName("downloadLabel") 467 ) 468 dlWidget.Set("value", pct) 469 dlLabel.Set("text", fmt.Sprintf("%d / %d", blockLength, chainLength)) 470 */ 471 472 case <-statsUpdateTicker.C: 473 gui.setStatsPane() 474 } 475 } 476 } 477 478 func (gui *Gui) setStatsPane() { 479 var memStats runtime.MemStats 480 runtime.ReadMemStats(&memStats) 481 482 statsPane := gui.getObjectByName("statsPane") 483 statsPane.Set("text", fmt.Sprintf(`###### Mist %s (%s) ####### 484 485 eth %d (p2p = %d) 486 487 CPU: # %d 488 Goroutines: # %d 489 CGoCalls: # %d 490 491 Alloc: %d 492 Heap Alloc: %d 493 494 CGNext: %x 495 NumGC: %d 496 `, Version, runtime.Version(), 497 eth.ProtocolVersion, 2, 498 runtime.NumCPU, runtime.NumGoroutine(), runtime.NumCgoCall(), 499 memStats.Alloc, memStats.HeapAlloc, 500 memStats.NextGC, memStats.NumGC, 501 )) 502 } 503 504 type qmlpeer struct{ Addr, NodeID, Caps string } 505 506 type peersByID []*qmlpeer 507 508 func (s peersByID) Len() int { return len(s) } 509 func (s peersByID) Less(i, j int) bool { return s[i].NodeID < s[j].NodeID } 510 func (s peersByID) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 511 512 func (gui *Gui) setPeerInfo() { 513 peers := gui.eth.Peers() 514 qpeers := make(peersByID, len(peers)) 515 for i, p := range peers { 516 qpeers[i] = &qmlpeer{ 517 NodeID: p.ID().String(), 518 Addr: p.RemoteAddr().String(), 519 Caps: fmt.Sprint(p.Caps()), 520 } 521 } 522 // we need to sort the peers because they jump around randomly 523 // otherwise. order returned by eth.Peers is random because they 524 // are taken from a map. 525 sort.Sort(qpeers) 526 527 gui.win.Root().Call("setPeerCounters", fmt.Sprintf("%d / %d", len(peers), gui.eth.MaxPeers())) 528 gui.win.Root().Call("clearPeers") 529 for _, p := range qpeers { 530 gui.win.Root().Call("addPeer", p) 531 } 532 } 533 534 func (gui *Gui) privateKey() string { 535 return ethutil.Bytes2Hex(gui.eth.KeyManager().PrivateKey()) 536 } 537 538 func (gui *Gui) address() []byte { 539 return gui.eth.KeyManager().Address() 540 } 541 542 /* 543 func LoadExtension(path string) (uintptr, error) { 544 lib, err := ffi.NewLibrary(path) 545 if err != nil { 546 return 0, err 547 } 548 549 so, err := lib.Fct("sharedObject", ffi.Pointer, nil) 550 if err != nil { 551 return 0, err 552 } 553 554 ptr := so() 555 556 err = lib.Close() 557 if err != nil { 558 return 0, err 559 } 560 561 return ptr.Interface().(uintptr), nil 562 } 563 */ 564 /* 565 vec, errr := LoadExtension("/Users/jeffrey/Desktop/build-libqmltest-Desktop_Qt_5_2_1_clang_64bit-Debug/liblibqmltest_debug.dylib") 566 fmt.Printf("Fetched vec with addr: %#x\n", vec) 567 if errr != nil { 568 fmt.Println(errr) 569 } else { 570 context.SetVar("vec", (unsafe.Pointer)(vec)) 571 } 572 */