diff --git a/LICENSE b/LICENSE index b77f7909a..78efdaabe 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,16 @@ -The MIT License (MIT) +Copyright (c) 2013-2014, Jeffrey Wilcke. All rights reserved. -Copyright (c) 2013 Jeffrey Wilcke +This library 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 2.1 of the License, or (at your option) any later version. -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +This library 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. -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +You should have received a copy of the GNU General Public License +along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301 USA diff --git a/README.md b/README.md index 790ee541e..da75e4d9e 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ Ethereum ======== -[![Build Status](https://travis-ci.org/ethereum/go-ethereum.png?branch=master)](https://travis-ci.org/ethereum/go-ethereum) +Master [![Build +Status](http://cpt-obvious.ethercasts.com:8010/buildstatusimage?builder=go-ethereum-master-docker)](http://cpt-obvious.ethercasts.com:8010/builders/go-ethereum-master-docker/builds/-1) Develop [![Build +Status](http://cpt-obvious.ethercasts.com:8010/buildstatusimage?builder=go-ethereum-develop-docker)](http://cpt-obvious.ethercasts.com:8010/builders/go-ethereum-develop-docker/builds/-1) Ethereum Go Client © 2014 Jeffrey Wilcke. -Current state: Proof of Concept 0.6.0. +Current state: Proof of Concept 0.6.3. For the development package please see the [eth-go package](https://github.com/ethereum/eth-go). diff --git a/ethereal/assets/back.png b/ethereal/assets/back.png new file mode 100644 index 000000000..38fc84d6e Binary files /dev/null and b/ethereal/assets/back.png differ diff --git a/ethereal/assets/bug.png b/ethereal/assets/bug.png new file mode 100644 index 000000000..f5e85dc99 Binary files /dev/null and b/ethereal/assets/bug.png differ diff --git a/ethereal/assets/close.png b/ethereal/assets/close.png new file mode 100644 index 000000000..88df442c5 Binary files /dev/null and b/ethereal/assets/close.png differ diff --git a/ethereal/assets/ext/ethereum.js b/ethereal/assets/ext/ethereum.js index de6fb0255..697a404a3 100644 --- a/ethereal/assets/ext/ethereum.js +++ b/ethereal/assets/ext/ethereum.js @@ -1,39 +1,124 @@ // Main Ethereum library window.eth = { prototype: Object(), + _callbacks: {}, + _onCallbacks: {}, + + test: function() { + var t = undefined; + postData({call: "test"}) + navigator.qt.onmessage = function(d) {console.log("onmessage called"); t = d; } + for(;;) { + if(t !== undefined) { + return t + } + } + }, + + mutan: function(code, cb) { + postData({call: "mutan", args: [code]}, cb) + }, + + toHex: function(str) { + var hex = ""; + for(var i = 0; i < str.length; i++) { + var n = str.charCodeAt(i).toString(16); + hex += n.length < 2 ? '0' + n : n; + } + + return hex; + }, + + toAscii: function(hex) { + // Find termination + var str = ""; + var i = 0, l = hex.length; + for(; i < l; i+=2) { + var code = hex.charCodeAt(i) + if(code == 0) { + break; + } + + str += String.fromCharCode(parseInt(hex.substr(i, 2), 16)); + } + + return str; + }, + + fromAscii: function(str, pad) { + if(pad === undefined) { + pad = 32 + } + + var hex = this.toHex(str); + + while(hex.length < pad*2) + hex += "00"; + + return hex + }, + // Retrieve block // // Either supply a number or a string. Type is determent for the lookup method // string - Retrieves the block by looking up the hash // number - Retrieves the block by looking up the block number - getBlock: function(numberOrHash, cb) { - var func; - if(typeof numberOrHash == "string") { - func = "getBlockByHash"; - } else { - func = "getBlockByNumber"; - } - postData({call: func, args: [numberOrHash]}, cb); - }, + getBlock: function(numberOrHash, cb) { + var func; + if(typeof numberOrHash == "string") { + func = "getBlockByHash"; + } else { + func = "getBlockByNumber"; + } + postData({call: func, args: [numberOrHash]}, cb); + }, // Create transaction // // Transact between two state objects - transact: function(sec, recipient, value, gas, gasPrice, data, cb) { - postData({call: "transact", args: [sec, recipient, value, gas, gasPrice, data]}, cb); + transact: function(params, cb) { + if(params === undefined) { + params = {}; + } + + if(params.endowment !== undefined) + params.value = params.endowment; + if(params.code !== undefined) + params.data = params.code; + + // Make sure everything is string + var fields = ["to", "from", "value", "gas", "gasPrice"]; + for(var i = 0; i < fields.length; i++) { + if(params[fields[i]] === undefined) { + params[fields[i]] = ""; + } + params[fields[i]] = params[fields[i]].toString(); + } + + var data; + if(typeof params.data === "object") { + data = ""; + for(var i = 0; i < params.data.length; i++) { + data += params.data[i] + } + } else { + data = params.data; + } + + postData({call: "transact", args: [params.from, params.to, params.value, params.gas, params.gasPrice, "0x"+data]}, cb); }, - create: function(sec, value, gas, gasPrice, init, body, cb) { - postData({call: "create", args: [sec, value, gas, gasPrice, init, body]}, cb); + getMessages: function(filter, cb) { + postData({call: "messages", args: [filter]}, cb); }, getStorageAt: function(address, storageAddress, cb) { postData({call: "getStorage", args: [address, storageAddress]}, cb); }, - getStateKeyVals: function(address, cb){ - postData({call: "getStateKeyVals", args: [address]}, cb); + getEachStorageAt: function(address, cb){ + postData({call: "getEachStorage", args: [address]}, cb); }, getKey: function(cb) { @@ -66,6 +151,7 @@ window.eth = { postData({call: "getSecretToAddress", args: [sec]}, cb); }, + /* watch: function(address, storageAddrOrCb, cb) { var ev; if(cb === undefined) { @@ -95,6 +181,16 @@ window.eth = { postData({call: "disconnect", args: [address, storageAddrOrCb]}); }, + */ + + watch: function(options) { + var filter = new Filter(options); + filter.number = newWatchNum().toString() + + postData({call: "watch", args: [options, filter.number]}) + + return filter; + }, set: function(props) { postData({call: "set", args: props}); @@ -137,9 +233,63 @@ window.eth = { } } }, - - } -window.eth._callbacks = {} -window.eth._onCallbacks = {} + + +var Filter = function(options) { + this.options = options; +}; +Filter.prototype.changed = function(callback) { + // Register the watched:. Qml will call the appropriate event if anything + // interesting happens in the land of Go. + eth.on("watched:"+this.number, callback) +} +Filter.prototype.getMessages = function(cb) { + return eth.getMessages(this.options, cb) +} + +var watchNum = 0; +function newWatchNum() { + return watchNum++; +} + +function postData(data, cb) { + data._seed = Math.floor(Math.random() * 1000000) + if(cb) { + eth._callbacks[data._seed] = cb; + } + + if(data.args === undefined) { + data.args = []; + } + + navigator.qt.postMessage(JSON.stringify(data)); +} + +navigator.qt.onmessage = function(ev) { + var data = JSON.parse(ev.data) + + if(data._event !== undefined) { + eth.trigger(data._event, data.data); + } else { + if(data._seed) { + var cb = eth._callbacks[data._seed]; + if(cb) { + cb.call(this, data.data) + + // Remove the "trigger" callback + delete eth._callbacks[ev._seed]; + } + } + } +} + +eth.on("chain:changed", function() { +}) + +eth.on("messages", { /* filters */}, function(messages){ +}) + +eth.on("pending:changed", function() { +}) diff --git a/ethereal/assets/ext/filter.js b/ethereal/assets/ext/filter.js new file mode 100644 index 000000000..5c1c03aad --- /dev/null +++ b/ethereal/assets/ext/filter.js @@ -0,0 +1,31 @@ +var Filter = function(options) { + this.callbacks = {}; + this.seed = Math.floor(Math.random() * 1000000); + this.options = options; + + if(options == "chain") { + eth.registerFilterString(options, this.seed); + } else if(typeof options === "object") { + eth.registerFilter(options, this.seed); + } +}; + +Filter.prototype.changed = function(callback) { + var cbseed = Math.floor(Math.random() * 1000000); + eth.registerFilterCallback(this.seed, cbseed); + + var self = this; + message.connect(function(messages, seed, callbackSeed) { + if(seed == self.seed && callbackSeed == cbseed) { + callback.call(self, messages); + } + }); +}; + +Filter.prototype.uninstall = function() { + eth.uninstallFilter(this.seed) +} + +Filter.prototype.messages = function() { + return JSON.parse(eth.messages(this.options)) +} diff --git a/ethereal/assets/ext/home.html b/ethereal/assets/ext/home.html new file mode 100644 index 000000000..a524e8403 --- /dev/null +++ b/ethereal/assets/ext/home.html @@ -0,0 +1,22 @@ + + + +Ethereum + + + + + +

... Ethereum ...

+ + + + diff --git a/ethereal/assets/ext/pre.js b/ethereal/assets/ext/pre.js index ca520f152..3e8a534e9 100644 --- a/ethereal/assets/ext/pre.js +++ b/ethereal/assets/ext/pre.js @@ -1,18 +1,3 @@ -function debug(/**/) { - var args = arguments; - var msg = "" - for(var i = 0; i < args.length; i++){ - if(typeof args[i] === "object") { - msg += " " + JSON.stringify(args[i]) - } else { - msg += " " + args[i] - } - } - - postData({call:"debug", args:[msg]}) - document.getElementById("debug").innerHTML += "
" + msg -} - // Helper function for generating pseudo callbacks and sending data to the QML part of the application function postData(data, cb) { data._seed = Math.floor(Math.random() * 1000000) @@ -50,9 +35,3 @@ navigator.qt.onmessage = function(ev) { } } } - -window.onerror = function(message, file, lineNumber, column, errorObj) { - debug(file, message, lineNumber+":"+column, errorObj); - - return false; -} diff --git a/ethereal/assets/ext/test.html b/ethereal/assets/ext/test.html new file mode 100644 index 000000000..4bac7d36f --- /dev/null +++ b/ethereal/assets/ext/test.html @@ -0,0 +1,44 @@ + + + +Tests + + + + + + + + + + diff --git a/ethereal/assets/pick.png b/ethereal/assets/pick.png new file mode 100644 index 000000000..2f5a261c2 Binary files /dev/null and b/ethereal/assets/pick.png differ diff --git a/ethereal/assets/qml/views/chain.qml b/ethereal/assets/qml/views/chain.qml new file mode 100644 index 000000000..9eaa49db1 --- /dev/null +++ b/ethereal/assets/qml/views/chain.qml @@ -0,0 +1,256 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.0; +import QtQuick.Layouts 1.0; +import QtQuick.Dialogs 1.0; +import QtQuick.Window 2.1; +import QtQuick.Controls.Styles 1.1 +import Ethereum 1.0 + +Rectangle { + id: root + property var title: "Network" + property var iconSource: "../net.png" + property var secondary: "Hi" + property var menuItem + + objectName: "chainView" + visible: false + anchors.fill: parent + + TableView { + id: blockTable + width: parent.width + anchors.top: parent.top + anchors.bottom: parent.bottom + TableViewColumn{ role: "number" ; title: "#" ; width: 100 } + TableViewColumn{ role: "hash" ; title: "Hash" ; width: 560 } + TableViewColumn{ role: "txAmount" ; title: "Tx amount" ; width: 100 } + + model: blockModel + + itemDelegate: Item { + Text { + anchors { + left: parent.left + right: parent.right + leftMargin: 10 + verticalCenter: parent.verticalCenter + } + color: styleData.textColor + elide: styleData.elideMode + text: styleData.value + font.pixelSize: 11 + MouseArea { + acceptedButtons: Qt.LeftButton | Qt.RightButton + propagateComposedEvents: true + anchors.fill: parent + onClicked: { + blockTable.selection.clear() + blockTable.selection.select(styleData.row) + + if(mouse.button == Qt.RightButton) { + contextMenu.row = styleData.row; + contextMenu.popup() + } + } + + onDoubleClicked: { + popup.visible = true + popup.setDetails(blockModel.get(styleData.row)) + } + } + } + + } + + Menu { + id: contextMenu + property var row; + MenuItem { + text: "Details" + onTriggered: { + popup.visible = true + popup.setDetails(blockModel.get(this.row)) + } + } + + MenuSeparator{} + + MenuItem { + text: "Copy" + onTriggered: { + copyToClipboard(blockModel.get(this.row).hash) + } + } + + MenuItem { + text: "Dump State" + onTriggered: { + generalFileDialog.show(false, function(path) { + var hash = blockModel.get(this.row).hash; + + gui.dumpState(hash, path); + }); + } + } + } + } + + + + function addBlock(block, initial) { + var txs = JSON.parse(block.transactions); + var amount = 0 + if(initial == undefined){ + initial = false + } + + if(txs != null){ + amount = txs.length + } + + if(initial){ + blockModel.append({number: block.number, name: block.name, gasLimit: block.gasLimit, gasUsed: block.gasUsed, coinbase: block.coinbase, hash: block.hash, txs: txs, txAmount: amount, time: block.time, prettyTime: convertToPretty(block.time)}) + } else { + blockModel.insert(0, {number: block.number, name: block.name, gasLimit: block.gasLimit, gasUsed: block.gasUsed, coinbase: block.coinbase, hash: block.hash, txs: txs, txAmount: amount, time: block.time, prettyTime: convertToPretty(block.time)}) + } + + //root.secondary.text = "#" + block.number; + } + + Window { + id: popup + visible: false + //flags: Qt.CustomizeWindowHint | Qt.Tool | Qt.WindowCloseButtonHint + property var block + width: root.width + height: 300 + Component{ + id: blockDetailsDelegate + Rectangle { + color: "#252525" + width: popup.width + height: 150 + Column { + anchors.leftMargin: 10 + anchors.topMargin: 5 + anchors.top: parent.top + anchors.left: parent.left + Text { text: '

Block details

'; color: "#F2F2F2"} + Text { text: 'Block number: ' + number; color: "#F2F2F2"} + Text { text: 'Hash: ' + hash; color: "#F2F2F2"} + Text { text: 'Coinbase: <' + name + '> ' + coinbase; color: "#F2F2F2"} + Text { text: 'Block found at: ' + prettyTime; color: "#F2F2F2"} + Text { text: 'Gas used: ' + gasUsed + " / " + gasLimit; color: "#F2F2F2"} + } + } + } + ListView { + model: singleBlock + delegate: blockDetailsDelegate + anchors.top: parent.top + height: 100 + anchors.leftMargin: 20 + id: listViewThing + Layout.maximumHeight: 40 + } + TableView { + id: txView + anchors.top: listViewThing.bottom + anchors.topMargin: 50 + width: parent.width + + TableViewColumn{width: 90; role: "value" ; title: "Value" } + TableViewColumn{width: 200; role: "hash" ; title: "Hash" } + TableViewColumn{width: 200; role: "sender" ; title: "Sender" } + TableViewColumn{width: 200;role: "address" ; title: "Receiver" } + TableViewColumn{width: 60; role: "gas" ; title: "Gas" } + TableViewColumn{width: 60; role: "gasPrice" ; title: "Gas Price" } + TableViewColumn{width: 60; role: "isContract" ; title: "Contract" } + + model: transactionModel + onClicked: { + var tx = transactionModel.get(row) + if(tx.data) { + popup.showContractData(tx) + }else{ + popup.height = 440 + } + } + } + + function showContractData(tx) { + txDetailsDebugButton.tx = tx + if(tx.createsContract) { + contractData.text = tx.data + contractLabel.text = "

Transaction created contract " + tx.address + "

" + }else{ + contractLabel.text = "

Transaction ran contract " + tx.address + "

" + contractData.text = tx.rawData + } + popup.height = 540 + } + + Rectangle { + id: txDetails + width: popup.width + height: 300 + anchors.left: listViewThing.left + anchors.top: txView.bottom + Label { + text: "

Contract data

" + anchors.top: parent.top + anchors.left: parent.left + id: contractLabel + anchors.leftMargin: 10 + } + Button { + property var tx + id: txDetailsDebugButton + anchors.right: parent.right + anchors.rightMargin: 10 + anchors.top: parent.top + anchors.topMargin: 10 + text: "Debug contract" + onClicked: { + if(tx.createsContract){ + eth.startDbWithCode(tx.rawData) + }else { + eth.startDbWithContractAndData(tx.address, tx.rawData) + } + } + } + TextArea { + id: contractData + text: "Contract" + anchors.top: contractLabel.bottom + anchors.left: parent.left + anchors.bottom: popup.bottom + wrapMode: Text.Wrap + width: parent.width - 30 + height: 80 + anchors.leftMargin: 10 + } + } + property var transactionModel: ListModel { + id: transactionModel + } + property var singleBlock: ListModel { + id: singleBlock + } + function setDetails(block){ + singleBlock.set(0,block) + popup.height = 300 + transactionModel.clear() + if(block.txs != undefined){ + for(var i = 0; i < block.txs.count; ++i) { + transactionModel.insert(0, block.txs.get(i)) + } + if(block.txs.get(0).data){ + popup.showContractData(block.txs.get(0)) + } + } + txView.forceActiveFocus() + } + } +} diff --git a/ethereal/assets/qml/views/history.qml b/ethereal/assets/qml/views/history.qml new file mode 100644 index 000000000..9eee883e3 --- /dev/null +++ b/ethereal/assets/qml/views/history.qml @@ -0,0 +1,52 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.0; +import QtQuick.Layouts 1.0; +import QtQuick.Dialogs 1.0; +import QtQuick.Window 2.1; +import QtQuick.Controls.Styles 1.1 +import Ethereum 1.0 + +Rectangle { + property var iconSource: "../tx.png" + property var title: "Transactions" + property var menuItem + + + id: historyView + visible: false + anchors.fill: parent + objectName: "transactionView" + + property var txModel: ListModel { + id: txModel + } + TableView { + id: txTableView + anchors.fill: parent + TableViewColumn{ role: "inout" ; title: "" ; width: 40 } + TableViewColumn{ role: "value" ; title: "Value" ; width: 100 } + TableViewColumn{ role: "address" ; title: "Address" ; width: 430 } + TableViewColumn{ role: "contract" ; title: "Contract" ; width: 100 } + + model: txModel + } + + function addTx(tx, inout) { + var isContract + if (tx.contract == true){ + isContract = "Yes" + }else{ + isContract = "No" + } + + + var address; + if(inout == "recv") { + address = tx.sender; + } else { + address = tx.address; + } + + txModel.insert(0, {inout: inout, hash: tx.hash, address: address, value: tx.value, contract: isContract}) + } +} diff --git a/ethereal/assets/qml/views/info.qml b/ethereal/assets/qml/views/info.qml new file mode 100644 index 000000000..ca6ca077e --- /dev/null +++ b/ethereal/assets/qml/views/info.qml @@ -0,0 +1,179 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.0; +import QtQuick.Layouts 1.0; +import QtQuick.Dialogs 1.0; +import QtQuick.Window 2.1; +import QtQuick.Controls.Styles 1.1 +import Ethereum 1.0 + +Rectangle { + property var title: "Information" + property var iconSource: "../heart.png" + property var menuItem + + objectName: "infoView" + visible: false + anchors.fill: parent + + color: "#00000000" + + Column { + id: info + spacing: 3 + anchors.fill: parent + anchors.topMargin: 5 + anchors.leftMargin: 5 + + Label { + id: addressLabel + text: "Address" + } + TextField { + text: eth.key().address + width: 500 + } + + Label { + text: "Client ID" + } + TextField { + text: gui.getCustomIdentifier() + width: 500 + placeholderText: "Anonymous" + onTextChanged: { + gui.setCustomIdentifier(text) + } + } + } + + property var addressModel: ListModel { + id: addressModel + } + TableView { + id: addressView + width: parent.width + height: 200 + anchors.bottom: logLayout.top + TableViewColumn{ role: "name"; title: "name" } + TableViewColumn{ role: "address"; title: "address"; width: 300} + + model: addressModel + itemDelegate: Item { + Text { + anchors { + left: parent.left + right: parent.right + leftMargin: 10 + verticalCenter: parent.verticalCenter + } + color: styleData.textColor + elide: styleData.elideMode + text: styleData.value + font.pixelSize: 11 + MouseArea { + acceptedButtons: Qt.LeftButton | Qt.RightButton + propagateComposedEvents: true + anchors.fill: parent + onClicked: { + addressView.selection.clear() + addressView.selection.select(styleData.row) + + if(mouse.button == Qt.RightButton) { + contextMenu.row = styleData.row; + contextMenu.popup() + } + } + } + } + + } + + Menu { + id: contextMenu + property var row; + + MenuItem { + text: "Copy" + onTriggered: { + copyToClipboard(addressModel.get(this.row).address) + } + } + } + } + + property var logModel: ListModel { + id: logModel + } + RowLayout { + id: logLayout + width: parent.width + height: 200 + anchors.bottom: parent.bottom + TableView { + id: logView + headerVisible: false + anchors { + right: logLevelSlider.left + left: parent.left + bottom: parent.bottom + top: parent.top + } + + TableViewColumn{ role: "description" ; title: "log" } + + model: logModel + } + + Slider { + id: logLevelSlider + value: gui.getLogLevelInt() + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + + rightMargin: 5 + leftMargin: 5 + topMargin: 5 + bottomMargin: 5 + } + + orientation: Qt.Vertical + maximumValue: 5 + stepSize: 1 + + onValueChanged: { + gui.setLogLevel(value) + } + } + } + + function addDebugMessage(message){ + debuggerLog.append({value: message}) + } + + function addAddress(address) { + addressModel.append({name: address.name, address: address.address}) + } + + function clearAddress() { + addressModel.clear() + } + + function addLog(str) { + // Remove first item once we've reached max log items + if(logModel.count > 250) { + logModel.remove(0) + } + + if(str.len != 0) { + if(logView.flickableItem.atYEnd) { + logModel.append({description: str}) + logView.positionViewAtRow(logView.rowCount - 1, ListView.Contain) + } else { + logModel.append({description: str}) + } + } + + } +} diff --git a/ethereal/assets/qml/views/javascript.qml b/ethereal/assets/qml/views/javascript.qml new file mode 100644 index 000000000..ea05c4148 --- /dev/null +++ b/ethereal/assets/qml/views/javascript.qml @@ -0,0 +1,45 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.0; +import QtQuick.Layouts 1.0; +import QtQuick.Dialogs 1.0; +import QtQuick.Window 2.1; +import QtQuick.Controls.Styles 1.1 +import Ethereum 1.0 + +Rectangle { + property var title: "JavaScript" + property var iconSource: "../tx.png" + property var menuItem + + objectName: "javascriptView" + visible: false + anchors.fill: parent + + TextField { + id: input + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + height: 20 + + Keys.onReturnPressed: { + var res = eth.evalJavascriptString(this.text); + this.text = ""; + + output.append(res) + } + } + + TextArea { + id: output + text: "> JSRE Ready..." + anchors { + top: parent.top + left: parent.left + right: parent.right + bottom: input.top + } + } +} diff --git a/ethereal/assets/qml/views/pending_tx.qml b/ethereal/assets/qml/views/pending_tx.qml new file mode 100644 index 000000000..abfa25790 --- /dev/null +++ b/ethereal/assets/qml/views/pending_tx.qml @@ -0,0 +1,45 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.0; +import QtQuick.Layouts 1.0; +import QtQuick.Dialogs 1.0; +import QtQuick.Window 2.1; +import QtQuick.Controls.Styles 1.1 +import Ethereum 1.0 + +Rectangle { + property var title: "Pending Transactions" + property var iconSource: "../tx.png" + property var menuItem + + objectName: "pendingTxView" + anchors.fill: parent + visible: false + id: pendingTxView + + property var pendingTxModel: ListModel { + id: pendingTxModel + } + + TableView { + id: pendingTxTableView + anchors.fill: parent + TableViewColumn{ role: "value" ; title: "Value" ; width: 100 } + TableViewColumn{ role: "from" ; title: "sender" ; width: 230 } + TableViewColumn{ role: "to" ; title: "Reciever" ; width: 230 } + TableViewColumn{ role: "contract" ; title: "Contract" ; width: 100 } + + model: pendingTxModel + } + + function addTx(tx, inout) { + var isContract + if (tx.contract == true){ + isContract = "Yes" + }else{ + isContract = "No" + } + + + pendingTxModel.insert(0, {hash: tx.hash, to: tx.address, from: tx.sender, value: tx.value, contract: isContract}) + } +} diff --git a/ethereal/assets/qml/views/transaction.qml b/ethereal/assets/qml/views/transaction.qml new file mode 100644 index 000000000..fb8ba8a6d --- /dev/null +++ b/ethereal/assets/qml/views/transaction.qml @@ -0,0 +1,215 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.0; +import QtQuick.Layouts 1.0; +import QtQuick.Dialogs 1.0; +import QtQuick.Window 2.1; +import QtQuick.Controls.Styles 1.1 +import Ethereum 1.0 + +Rectangle { + property var iconSource: "../new.png" + property var title: "New transaction" + property var menuItem + + objectName: "newTxView" + visible: false + anchors.fill: parent + color: "#00000000" + + Column { + id: mainContractColumn + anchors.fill: parent + + + states: [ + State{ + name: "ERROR" + + PropertyChanges { target: txResult; visible:true} + PropertyChanges { target: codeView; visible:true} + }, + State { + name: "DONE" + + PropertyChanges { target: txValue; visible:false} + PropertyChanges { target: txGas; visible:false} + PropertyChanges { target: txGasPrice; visible:false} + PropertyChanges { target: codeView; visible:false} + PropertyChanges { target: txButton; visible:false} + PropertyChanges { target: txDataLabel; visible:false} + PropertyChanges { target: atLabel; visible:false} + PropertyChanges { target: txFuelRecipient; visible:false} + PropertyChanges { target: valueDenom; visible:false} + PropertyChanges { target: gasDenom; visible:false} + + PropertyChanges { target: txResult; visible:true} + PropertyChanges { target: txOutput; visible:true} + PropertyChanges { target: newTxButton; visible:true} + }, + State { + name: "SETUP" + + PropertyChanges { target: txValue; visible:true; text: ""} + PropertyChanges { target: txGas; visible:true;} + PropertyChanges { target: txGasPrice; visible:true;} + PropertyChanges { target: codeView; visible:true; text: ""} + PropertyChanges { target: txButton; visible:true} + PropertyChanges { target: txDataLabel; visible:true} + PropertyChanges { target: valueDenom; visible:true} + PropertyChanges { target: gasDenom; visible:true} + + PropertyChanges { target: txResult; visible:false} + PropertyChanges { target: txOutput; visible:false} + PropertyChanges { target: newTxButton; visible:false} + } + ] + width: 400 + spacing: 5 + anchors.left: parent.left + anchors.top: parent.top + anchors.leftMargin: 5 + anchors.topMargin: 5 + + ListModel { + id: denomModel + ListElement { text: "Wei" ; zeros: "" } + ListElement { text: "Ada" ; zeros: "000" } + ListElement { text: "Babbage" ; zeros: "000000" } + ListElement { text: "Shannon" ; zeros: "000000000" } + ListElement { text: "Szabo" ; zeros: "000000000000" } + ListElement { text: "Finney" ; zeros: "000000000000000" } + ListElement { text: "Ether" ; zeros: "000000000000000000" } + ListElement { text: "Einstein" ;zeros: "000000000000000000000" } + ListElement { text: "Douglas" ; zeros: "000000000000000000000000000000000000000000" } + } + + + TextField { + id: txFuelRecipient + placeholderText: "Address / Name or empty for contract" + //validator: RegExpValidator { regExp: /[a-f0-9]{40}/ } + width: 400 + } + + RowLayout { + TextField { + id: txValue + width: 222 + placeholderText: "Amount" + validator: RegExpValidator { regExp: /\d*/ } + onTextChanged: { + contractFormReady() + } + } + + ComboBox { + id: valueDenom + currentIndex: 6 + model: denomModel + } + } + + RowLayout { + TextField { + id: txGas + width: 50 + validator: RegExpValidator { regExp: /\d*/ } + placeholderText: "Gas" + text: "500" + } + Label { + id: atLabel + text: "@" + } + + TextField { + id: txGasPrice + width: 200 + placeholderText: "Gas price" + text: "10" + validator: RegExpValidator { regExp: /\d*/ } + } + + ComboBox { + id: gasDenom + currentIndex: 4 + model: denomModel + } + } + + Label { + id: txDataLabel + text: "Data" + } + + TextArea { + id: codeView + height: 300 + anchors.topMargin: 5 + width: 400 + onTextChanged: { + contractFormReady() + } + } + + + Button { + id: txButton + /* enabled: false */ + states: [ + State { + name: "READY" + PropertyChanges { target: txButton; /*enabled: true*/} + }, + State { + name: "NOTREADY" + PropertyChanges { target: txButton; /*enabled:false*/} + } + ] + text: "Send" + onClicked: { + var value = txValue.text + denomModel.get(valueDenom.currentIndex).zeros; + var gasPrice = txGasPrice.text + denomModel.get(gasDenom.currentIndex).zeros; + var res = gui.transact(txFuelRecipient.text, value, txGas.text, gasPrice, codeView.text) + if(res[1]) { + txResult.text = "Your contract could not be sent over the network:\n" + txResult.text += res[1].error() + txResult.text += "" + mainContractColumn.state = "ERROR" + } else { + txResult.text = "Your transaction has been submitted:\n" + txOutput.text = res[0].address + mainContractColumn.state = "DONE" + } + } + } + Text { + id: txResult + visible: false + } + TextField { + id: txOutput + visible: false + width: 530 + } + Button { + id: newTxButton + visible: false + text: "Create a new transaction" + onClicked: { + this.visible = false + txResult.text = "" + txOutput.text = "" + mainContractColumn.state = "SETUP" + } + } + } + + function contractFormReady(){ + if(codeView.text.length > 0 && txValue.text.length > 0 && txGas.text.length > 0 && txGasPrice.length > 0) { + txButton.state = "READY" + }else{ + txButton.state = "NOTREADY" + } + } +} diff --git a/ethereal/assets/qml/views/wallet.qml b/ethereal/assets/qml/views/wallet.qml new file mode 100644 index 000000000..5e10a7022 --- /dev/null +++ b/ethereal/assets/qml/views/wallet.qml @@ -0,0 +1,165 @@ +import QtQuick 2.0 +import QtQuick.Controls 1.0; +import QtQuick.Layouts 1.0; +import QtQuick.Dialogs 1.0; +import QtQuick.Window 2.1; +import QtQuick.Controls.Styles 1.1 +import Ethereum 1.0 + +Rectangle { + id: root + property var title: "Wallet" + property var iconSource: "../wallet.png" + property var menuItem + + objectName: "walletView" + anchors.fill: parent + + function onReady() { + menuItem.secondaryTitle = eth.numberToHuman(eth.balanceAt(eth.key().address)) + } + + ListModel { + id: denomModel + ListElement { text: "Wei" ; zeros: "" } + ListElement { text: "Ada" ; zeros: "000" } + ListElement { text: "Babbage" ; zeros: "000000" } + ListElement { text: "Shannon" ; zeros: "000000000" } + ListElement { text: "Szabo" ; zeros: "000000000000" } + ListElement { text: "Finney" ; zeros: "000000000000000" } + ListElement { text: "Ether" ; zeros: "000000000000000000" } + ListElement { text: "Einstein" ;zeros: "000000000000000000000" } + ListElement { text: "Douglas" ; zeros: "000000000000000000000000000000000000000000" } + } + + ColumnLayout { + spacing: 10 + y: 40 + anchors.fill: parent + + Text { + id: balance + text: "Balance: " + eth.numberToHuman(eth.balanceAt(eth.key().address)) + font.pixelSize: 24 + anchors { + horizontalCenter: parent.horizontalCenter + top: parent.top + topMargin: 20 + } + } + + Rectangle { + id: newTxPane + color: "#ececec" + border.color: "#cccccc" + border.width: 1 + anchors { + top: balance.bottom + topMargin: 10 + left: parent.left + leftMargin: 5 + right: parent.right + rightMargin: 5 + } + height: 100 + + RowLayout { + id: amountFields + spacing: 10 + anchors { + top: parent.top + topMargin: 20 + left: parent.left + leftMargin: 20 + } + + Text { + text: "Ξ " + } + + // There's something off with the row layout where textfields won't listen to the width setting + Rectangle { + width: 50 + height: 20 + TextField { + id: txValue + width: parent.width + placeholderText: "0.00" + } + } + + ComboBox { + id: valueDenom + currentIndex: 6 + model: denomModel + } + + } + + RowLayout { + id: toFields + spacing: 10 + anchors { + top: amountFields.bottom + topMargin: 5 + left: parent.left + leftMargin: 20 + } + + Text { + text: "To" + } + + Rectangle { + width: 200 + height: 20 + TextField { + id: txTo + width: parent.width + placeholderText: "Address or name" + } + } + + Button { + text: "Send" + onClicked: { + var value = txValue.text + denomModel.get(valueDenom.currentIndex).zeros; + var gasPrice = "10000000000000" + var res = eth.transact({from: eth.key().privateKey, to: txTo.text, value: value, gas: "500", gasPrice: gasPrice}) + console.log(res) + } + } + } + } + + Rectangle { + anchors { + left: parent.left + right: parent.right + top: newTxPane.bottom + topMargin: 10 + bottom: parent.bottom + } + TableView { + id: txTableView + anchors.fill : parent + TableViewColumn{ role: "num" ; title: "#" ; width: 30 } + TableViewColumn{ role: "from" ; title: "From" ; width: 280 } + TableViewColumn{ role: "to" ; title: "To" ; width: 280 } + TableViewColumn{ role: "value" ; title: "Amount" ; width: 100 } + + model: ListModel { + id: txModel + Component.onCompleted: { + var messages = JSON.parse(eth.messages({latest: -1, from: "e6716f9544a56c530d868e4bfbacb172315bdead"})) + for(var i = 0; i < messages.length; i++) { + var message = messages[i]; + this.insert(0, {num: i, from: message.from, to: message.to, value: eth.numberToHuman(message.value)}) + } + } + } + } + } + + } +} diff --git a/ethereal/assets/qml/wallet.qml b/ethereal/assets/qml/wallet.qml index eef49824f..094349bab 100644 --- a/ethereal/assets/qml/wallet.qml +++ b/ethereal/assets/qml/wallet.qml @@ -6,17 +6,76 @@ import QtQuick.Window 2.1; import QtQuick.Controls.Styles 1.1 import Ethereum 1.0 +import "../ext/filter.js" as Eth ApplicationWindow { id: root property alias miningButtonText: miningButton.text + width: 900 height: 600 minimumHeight: 300 - title: "Ethereal" + title: "Ether browser" + + // This signal is used by the filter API. The filter API connects using this signal handler from + // the different QML files and plugins. + signal message(var callback, int seed, int seedCallback); + function invokeFilterCallback(data, receiverSeed, callbackSeed) { + var messages = JSON.parse(data) + // Signal handler + message(messages, receiverSeed, callbackSeed); + } + + TextField { + id: copyElementHax + visible: false + } + + function copyToClipboard(text) { + copyElementHax.text = text + copyElementHax.selectAll() + copyElementHax.copy() + } + + // Takes care of loading all default plugins + Component.onCompleted: { + var walletView = addPlugin("./views/wallet.qml", {noAdd: true, section: "ethereum", active: true}) + var historyView = addPlugin("./views/history.qml", {noAdd: true, section: "legacy"}) + var newTxView = addPlugin("./views/transaction.qml", {noAdd: true, section: "legacy"}) + var chainView = addPlugin("./views/chain.qml", {noAdd: true, section: "legacy"}) + var infoView = addPlugin("./views/info.qml", {noAdd: true, section: "legacy"}) + var pendingTxView = addPlugin("./views/pending_tx.qml", {noAdd: true, section: "legacy"}) + var pendingTxView = addPlugin("./views/javascript.qml", {noAdd: true, section: "legacy"}) + + // Call the ready handler + gui.done() + + } + + function addPlugin(path, options) { + var component = Qt.createComponent(path); + if(component.status != Component.Ready) { + if(component.status == Component.Error) { + console.debug("Error:"+ component.errorString()); + } + + return + } + + var views = mainSplit.addComponent(component, options) + views.menuItem.path = path + + mainSplit.views.push(views); + + if(!options.noAdd) { + gui.addPlugin(path) + } + + return views.view + } MenuBar { Menu { @@ -24,7 +83,44 @@ ApplicationWindow { MenuItem { text: "Import App" shortcut: "Ctrl+o" - onTriggered: openAppDialog.open() + onTriggered: { + generalFileDialog.show(true, importApp) + } + } + + MenuItem { + text: "Browser" + onTriggered: eth.openBrowser() + } + + MenuItem { + text: "Add plugin" + onTriggered: { + generalFileDialog.show(true, function(path) { + addPlugin(path, {canClose: true, section: "apps"}) + }) + } + } + + MenuSeparator {} + + MenuItem { + text: "Import key" + shortcut: "Ctrl+i" + onTriggered: { + generalFileDialog.show(true, function(path) { + gui.importKey(path) + }) + } + } + + MenuItem { + text: "Export keys" + shortcut: "Ctrl+e" + onTriggered: { + generalFileDialog.show(false, function(path) { + }) + } } } @@ -33,7 +129,33 @@ ApplicationWindow { MenuItem { text: "Debugger" shortcut: "Ctrl+d" - onTriggered: ui.startDebugger() + onTriggered: eth.startDebugger() + } + + MenuItem { + text: "Import Tx" + onTriggered: { + txImportDialog.visible = true + } + } + + MenuItem { + text: "Run JS file" + onTriggered: { + generalFileDialog.show(true, function(path) { + eth.evalJavascriptFile(path) + }) + } + } + + MenuItem { + text: "Dump state" + onTriggered: { + generalFileDialog.show(false, function(path) { + // Empty hash for latest + gui.dumpState("", path) + }) + } } } @@ -67,1037 +189,541 @@ ApplicationWindow { } + statusBar: StatusBar { + height: 32 + RowLayout { + Button { + id: miningButton + text: "Start Mining" + onClicked: { + gui.toggleMining() + } + } + + Button { + id: importAppButton + text: "Browser" + onClicked: { + eth.openBrowser() + } + } + + RowLayout { + Label { + id: walletValueLabel + + font.pixelSize: 10 + styleColor: "#797979" + } + } + } + + Label { + y: 6 + id: lastBlockLabel + objectName: "lastBlockLabel" + visible: true + text: "" + font.pixelSize: 10 + anchors.right: peerGroup.left + anchors.rightMargin: 5 + } + + ProgressBar { + id: syncProgressIndicator + visible: false + objectName: "syncProgressIndicator" + y: 3 + width: 140 + indeterminate: true + anchors.right: peerGroup.left + anchors.rightMargin: 5 + } + + RowLayout { + id: peerGroup + y: 7 + anchors.right: parent.right + MouseArea { + onDoubleClicked: peerWindow.visible = true + anchors.fill: parent + } + + Label { + id: peerLabel + font.pixelSize: 8 + text: "0 / 0" + } + Image { + id: peerImage + width: 10; height: 10 + source: "../network.png" + } + } + } + property var blockModel: ListModel { id: blockModel } - function setView(view) { - networkView.visible = false - historyView.visible = false - newTxView.visible = false - infoView.visible = false - view.visible = true - //root.title = "Ethereal - " = view.title - } - SplitView { + property var views: []; + + id: mainSplit anchors.fill: parent resizing: false - Rectangle { - id: menu - Layout.minimumWidth: 80 - Layout.maximumWidth: 80 - anchors.bottom: parent.bottom - anchors.top: parent.top - //color: "#D9DDE7" - color: "#252525" + function setView(view, menu) { + for(var i = 0; i < views.length; i++) { + views[i].view.visible = false - ColumnLayout { - y: 50 - anchors.left: parent.left - anchors.right: parent.right - height: 200 - Image { - source: "../tx.png" - anchors.horizontalCenter: parent.horizontalCenter - MouseArea { - anchors.fill: parent - onClicked: { - setView(historyView) - } - } - } - Image { - source: "../new.png" - anchors.horizontalCenter: parent.horizontalCenter - MouseArea { - anchors.fill: parent - onClicked: { - setView(newTxView) - } - } - } - Image { - source: "../net.png" - anchors.horizontalCenter: parent.horizontalCenter - MouseArea { - anchors.fill: parent - onClicked: { - setView(networkView) - } - } - } - - Image { - source: "../heart.png" - anchors.horizontalCenter: parent.horizontalCenter - MouseArea { - anchors.fill: parent - onClicked: { - setView(infoView) - } - } - } + views[i].menuItem.border.color = "#00000000" + views[i].menuItem.color = "#00000000" } + view.visible = true + + menu.border.color = "#CCCCCC" + menu.color = "#FFFFFFFF" } - Rectangle { - id: mainView - color: "#00000000" - anchors.right: parent.right - anchors.left: menu.right - anchors.bottom: parent.bottom - anchors.top: parent.top + function addComponent(component, options) { + var view = mainView.createView(component, options) + view.visible = false + view.anchors.fill = mainView - property var txModel: ListModel { - id: txModel + if( !view.hasOwnProperty("iconSource") ) { + console.log("Could not load plugin. Property 'iconSourc' not found on view."); + return; } - Rectangle { - id: historyView - anchors.fill: parent - - property var title: "Transactions" - TableView { - id: txTableView - anchors.fill: parent - TableViewColumn{ role: "inout" ; title: "" ; width: 40 } - TableViewColumn{ role: "value" ; title: "Value" ; width: 100 } - TableViewColumn{ role: "address" ; title: "Address" ; width: 430 } - TableViewColumn{ role: "contract" ; title: "Contract" ; width: 100 } - - model: txModel - } + var menuItem = menu.createMenuItem(view.iconSource, view, options); + if( view.hasOwnProperty("menuItem") ) { + view.menuItem = menuItem; } - Rectangle { - id: newTxView - property var title: "New transaction" - visible: false - anchors.fill: parent - color: "#00000000" - /* - TabView{ - anchors.fill: parent - anchors.rightMargin: 5 - anchors.leftMargin: 5 - anchors.topMargin: 5 - anchors.bottomMargin: 5 - id: newTransactionTab - Component.onCompleted:{ - addTab("Simple send", newTransaction) - addTab("Contracts", newContract) - } - } - */ - Component.onCompleted: { - newContract.createObject(newTxView) - } + if( view.hasOwnProperty("onReady") ) { + view.onReady.call(view) } - Rectangle { - id: networkView - property var title: "Network" - visible: false - anchors.fill: parent - - TableView { - id: blockTable - width: parent.width - anchors.top: parent.top - anchors.bottom: parent.bottom - TableViewColumn{ role: "number" ; title: "#" ; width: 100 } - TableViewColumn{ role: "hash" ; title: "Hash" ; width: 560 } - TableViewColumn{ role: "txAmount" ; title: "Tx amount" ; width: 100 } - - model: blockModel - - onDoubleClicked: { - popup.visible = true - popup.setDetails(blockModel.get(row)) - } - } - + if( options.active ) { + setView(view, menuItem) } - Rectangle { - id: infoView - property var title: "Information" - visible: false - color: "#00000000" - anchors.fill: parent - Column { - spacing: 3 - anchors.fill: parent - anchors.topMargin: 5 - anchors.leftMargin: 5 + return {view: view, menuItem: menuItem} + } - Label { - id: addressLabel - text: "Address" - } - TextField { - text: pub.getKey().address - width: 500 - } + /********************* + * Main menu. + ********************/ + Rectangle { + id: menu + Layout.minimumWidth: 180 + Layout.maximumWidth: 180 + anchors.top: parent.top + color: "#ececec" - Label { - text: "Client ID" - } - TextField { - text: eth.getCustomIdentifier() - width: 500 - placeholderText: "Anonymous" - onTextChanged: { - eth.setCustomIdentifier(text) - } - } - } - - property var addressModel: ListModel { - id: addressModel - } - TableView { - id: addressView - width: parent.width - 200 - height: 200 - anchors.bottom: logLayout.top - TableViewColumn{ role: "name"; title: "name" } - TableViewColumn{ role: "address"; title: "address"; width: 300} - - model: addressModel - } - - Rectangle { - anchors.top: addressView.top - anchors.left: addressView.right - anchors.leftMargin: 20 - - TextField { - placeholderText: "Name to register" - id: nameToReg - width: 150 - } - - Button { - anchors.top: nameToReg.bottom - text: "Register" - MouseArea{ - anchors.fill: parent - onClicked: { - eth.registerName(nameToReg.text) - nameToReg.text = "" - } - } - } - } - - - property var logModel: ListModel { - id: logModel - } - RowLayout { - id: logLayout - width: parent.width - height: 200 - anchors.bottom: parent.bottom - TableView { - id: logView - headerVisible: false - anchors { - right: logLevelSlider.left - left: parent.left - bottom: parent.bottom - top: parent.top - } - - TableViewColumn{ role: "description" ; title: "log" } - - model: logModel - } - - Slider { - id: logLevelSlider - value: eth.getLogLevelInt() - anchors { - right: parent.right - top: parent.top - bottom: parent.bottom - - rightMargin: 5 - leftMargin: 5 - topMargin: 5 - bottomMargin: 5 - } - - orientation: Qt.Vertical - maximumValue: 5 - stepSize: 1 - - onValueChanged: { - eth.setLogLevel(value) - } - } - } - } - - /* - signal addPlugin(string name) Component { - id: pluginWindow + id: menuItemTemplate Rectangle { - anchors.fill: parent - Label { - id: pluginTitle - anchors.centerIn: parent - text: "Hello world" + id: menuItem + property var view; + property var path; + + property alias title: label.text + property alias icon: icon.source + property alias secondaryTitle: secondary.text + + width: 180 + height: 28 + border.color: "#00000000" + border.width: 1 + radius: 5 + color: "#00000000" + + anchors { + left: parent.left + leftMargin: 4 + } + + MouseArea { + anchors.fill: parent + onClicked: { + mainSplit.setView(view, menuItem) + } + } + + Image { + id: icon + height: 20 + width: 20 + anchors { + left: parent.left + verticalCenter: parent.verticalCenter + leftMargin: 3 + } + MouseArea { + anchors.fill: parent + onClicked: { + menuItem.closeApp() + } + } + } + + Text { + id: label + anchors { + left: icon.right + verticalCenter: parent.verticalCenter + leftMargin: 3 + } + + color: "#0D0A01" + font.pixelSize: 12 + } + + Text { + id: secondary + anchors { + right: parent.right + rightMargin: 8 + verticalCenter: parent.verticalCenter + } + color: "#AEADBE" + font.pixelSize: 12 + } + + + function closeApp() { + if(this.view.hasOwnProperty("onDestroy")) { + this.view.onDestroy.call(this.view) + } + + this.view.destroy() + this.destroy() + gui.removePlugin(this.path) } - Component.onCompleted: setView(this) } } - onAddPlugin: { - var pluginWin = pluginWindow.createObject(mainView) - console.log(pluginWin) - pluginWin.pluginTitle.text = "Test" + function createMenuItem(icon, view, options) { + if(options === undefined) { + options = {}; + } + + var section; + switch(options.section) { + case "ethereum": + section = menuDefault; + break; + case "legacy": + section = menuLegacy; + break; + default: + section = menuApps; + break; + } + + var comp = menuItemTemplate.createObject(section) + + comp.view = view + comp.title = view.title + comp.icon = view.iconSource + /* + if(view.secondary !== undefined) { + comp.secondary = view.secondary + } + */ + + return comp + + /* + if(options.canClose) { + //comp.closeButton.visible = options.canClose + } + */ } - */ - } - } - FileDialog { - id: openAppDialog - title: "Open QML Application" - onAccepted: { - //ui.open(openAppDialog.fileUrl.toString()) - //ui.openHtml(Qt.resolvedUrl(ui.assetPath("test.html"))) - var path = openAppDialog.fileUrl.toString() - console.log(path) - var ext = path.split('.').pop() - console.log(ext) - if(ext == "html" || ext == "htm") { - ui.openHtml(path) - }else if(ext == "qml"){ - ui.openQml(path) - } - } - } + ColumnLayout { + id: menuColumn + y: 10 + width: parent.width + anchors.left: parent.left + anchors.right: parent.right + spacing: 3 - statusBar: StatusBar { - height: 30 - RowLayout { - Button { - id: miningButton - onClicked: { - eth.toggleMining() - } - text: "Start Mining" - } + Text { + text: "ETHEREUM" + font.bold: true + anchors { + left: parent.left + leftMargin: 5 + } + color: "#888888" + } - Button { - property var enabled: true - id: debuggerWindow - onClicked: { - ui.startDebugger() - } - text: "Debugger" - } - - Button { - id: importAppButton - anchors.left: debuggerWindow.right - anchors.leftMargin: 5 - onClicked: openAppDialog.open() - text: "Import App" - } - - Label { - anchors.left: importAppButton.right - anchors.leftMargin: 5 - id: walletValueLabel - } - } - - Label { - y: 6 - id: lastBlockLabel - objectName: "lastBlockLabel" - visible: true - text: "" - font.pixelSize: 10 - anchors.right: peerGroup.left - anchors.rightMargin: 5 - } - - ProgressBar { - id: syncProgressIndicator - visible: false - objectName: "syncProgressIndicator" - y: 3 - width: 140 - indeterminate: true - anchors.right: peerGroup.left - anchors.rightMargin: 5 - } - - RowLayout { - id: peerGroup - y: 7 - anchors.right: parent.right - MouseArea { - onDoubleClicked: peerWindow.visible = true - anchors.fill: parent - } - - Label { - id: peerLabel - font.pixelSize: 8 - text: "0 / 0" - } - Image { - id: peerImage - width: 10; height: 10 - source: "../network.png" - } - } - } - - Window { - id: popup - visible: false - //flags: Qt.CustomizeWindowHint | Qt.Tool | Qt.WindowCloseButtonHint - property var block - width: root.width - height: 300 - Component{ - id: blockDetailsDelegate - Rectangle { - color: "#252525" - width: popup.width - height: 150 - Column { - anchors.leftMargin: 10 - anchors.topMargin: 5 - anchors.top: parent.top - anchors.left: parent.left - Text { text: '

Block details

'; color: "#F2F2F2"} - Text { text: 'Block number: ' + number; color: "#F2F2F2"} - Text { text: 'Hash: ' + hash; color: "#F2F2F2"} - Text { text: 'Coinbase: <' + name + '> ' + coinbase; color: "#F2F2F2"} - Text { text: 'Block found at: ' + prettyTime; color: "#F2F2F2"} - Text { text: 'Gas used: ' + gasUsed + " / " + gasLimit; color: "#F2F2F2"} - } - } - } - ListView { - model: singleBlock - delegate: blockDetailsDelegate - anchors.top: parent.top - height: 100 - anchors.leftMargin: 20 - id: listViewThing - Layout.maximumHeight: 40 - } - TableView { - id: txView - anchors.top: listViewThing.bottom - anchors.topMargin: 50 - width: parent.width - - TableViewColumn{width: 90; role: "value" ; title: "Value" } - TableViewColumn{width: 200; role: "hash" ; title: "Hash" } - TableViewColumn{width: 200; role: "sender" ; title: "Sender" } - TableViewColumn{width: 200;role: "address" ; title: "Receiver" } - TableViewColumn{width: 60; role: "gas" ; title: "Gas" } - TableViewColumn{width: 60; role: "gasPrice" ; title: "Gas Price" } - TableViewColumn{width: 60; role: "isContract" ; title: "Contract" } - - model: transactionModel - onClicked: { - var tx = transactionModel.get(row) - if(tx.data) { - popup.showContractData(tx) - }else{ - popup.height = 440 - } - } - } - - function showContractData(tx) { - txDetailsDebugButton.tx = tx - if(tx.createsContract) { - contractData.text = tx.data - contractLabel.text = "

Transaction created contract " + tx.address + "

" - }else{ - contractLabel.text = "

Transaction ran contract " + tx.address + "

" - contractData.text = tx.rawData - } - popup.height = 540 - } - - Rectangle { - id: txDetails - width: popup.width - height: 300 - anchors.left: listViewThing.left - anchors.top: txView.bottom - Label { - text: "

Contract data

" - anchors.top: parent.top - anchors.left: parent.left - id: contractLabel - anchors.leftMargin: 10 - } - Button { - property var tx - id: txDetailsDebugButton - anchors.right: parent.right - anchors.rightMargin: 10 - anchors.top: parent.top - anchors.topMargin: 10 - text: "Debug contract" - onClicked: { - if(tx.createsContract){ - ui.startDbWithCode(tx.rawData) - }else { - ui.startDbWithContractAndData(tx.address, tx.rawData) - } - } - } - TextArea { - id: contractData - text: "Contract" - anchors.top: contractLabel.bottom - anchors.left: parent.left - anchors.bottom: popup.bottom - wrapMode: Text.Wrap - width: parent.width - 30 - height: 80 - anchors.leftMargin: 10 - } - } - property var transactionModel: ListModel { - id: transactionModel - } - property var singleBlock: ListModel { - id: singleBlock - } - function setDetails(block){ - singleBlock.set(0,block) - popup.height = 300 - transactionModel.clear() - if(block.txs != undefined){ - for(var i = 0; i < block.txs.count; ++i) { - transactionModel.insert(0, block.txs.get(i)) - } - if(block.txs.get(0).data){ - popup.showContractData(block.txs.get(0)) - } - } - txView.forceActiveFocus() - } - } - - Window { - id: addPeerWin - //flags: Qt.CustomizeWindowHint | Qt.Tool | Qt.WindowCloseButtonHint - visible: false - minimumWidth: 230 - maximumWidth: 230 - maximumHeight: 50 - minimumHeight: 50 - - TextField { - id: addrField - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 10 - placeholderText: "address:port" - onAccepted: { - ui.connectToPeer(addrField.text) - addPeerWin.visible = false - } - } - Button { - anchors.left: addrField.right - anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: 5 - text: "Add" - onClicked: { - ui.connectToPeer(addrField.text) - addPeerWin.visible = false - } - } - Component.onCompleted: { - addrField.focus = true - } - } - - Window { - id: aboutWin - visible: false - title: "About" - minimumWidth: 350 - maximumWidth: 350 - maximumHeight: 200 - minimumHeight: 200 - - Image { - id: aboutIcon - height: 150 - width: 150 - fillMode: Image.PreserveAspectFit - smooth: true - source: "../facet.png" - x: 10 - y: 10 - } - - Text { - anchors.left: aboutIcon.right - anchors.leftMargin: 10 - font.pointSize: 12 - text: "

Ethereal


Development

Jeffrey Wilcke
Maran Hidskes
Viktor Trón
" - } - } - - function addDebugMessage(message){ - debuggerLog.append({value: message}) - } - - function addAddress(address) { - addressModel.append({name: address.name, address: address.address}) - } - function clearAddress() { - addressModel.clear() - } - - function loadPlugin(name) { - console.log("Loading plugin" + name) - mainView.addPlugin(name) - } - - function setWalletValue(value) { - walletValueLabel.text = value - } - - function addTx(tx, inout) { - var isContract - if (tx.contract == true){ - isContract = "Yes" - }else{ - isContract = "No" - } - - var address; - if(inout == "recv") { - address = tx.sender; - } else { - address = tx.address; - } - txModel.insert(0, {inout: inout, hash: tx.hash, address: address, value: tx.value, contract: isContract}) - } - - function addBlock(block, initial) { - var txs = JSON.parse(block.transactions); - var amount = 0 - if(initial == undefined){ - initial = false - } - - if(txs != null){ - amount = txs.length - } - - if(initial){ - blockModel.append({number: block.number, name: block.name, gasLimit: block.gasLimit, gasUsed: block.gasUsed, coinbase: block.coinbase, hash: block.hash, txs: txs, txAmount: amount, time: block.time, prettyTime: convertToPretty(block.time)}) - }else{ - blockModel.insert(0, {number: block.number, name: block.name, gasLimit: block.gasLimit, gasUsed: block.gasUsed, coinbase: block.coinbase, hash: block.hash, txs: txs, txAmount: amount, time: block.time, prettyTime: convertToPretty(block.time)}) - } - } - - function addLog(str) { - // Remove first item once we've reached max log items - if(logModel.count > 250) { - logModel.remove(0) - } - - if(str.len != 0) { - if(logView.flickableItem.atYEnd) { - logModel.append({description: str}) - logView.positionViewAtRow(logView.rowCount - 1, ListView.Contain) - } else { - logModel.append({description: str}) - } - } - - } - - function setPeers(text) { - peerLabel.text = text - } - - function addPeer(peer) { - // We could just append the whole peer object but it cries if you try to alter them - peerModel.append({ip: peer.ip, port: peer.port, lastResponse:timeAgo(peer.lastSend), latency: peer.latency, version: peer.version}) - } - - function resetPeers(){ - peerModel.clear() - } - - function timeAgo(unixTs){ - var lapsed = (Date.now() - new Date(unixTs*1000)) / 1000 - return (lapsed + " seconds ago") - } - function convertToPretty(unixTs){ - var a = new Date(unixTs*1000); - var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; - var year = a.getFullYear(); - var month = months[a.getMonth()]; - var date = a.getDate(); - var hour = a.getHours(); - var min = a.getMinutes(); - var sec = a.getSeconds(); - var time = date+' '+month+' '+year+' '+hour+':'+min+':'+sec ; - return time; - } - // ****************************************** - // Windows - // ****************************************** - Window { - id: peerWindow - //flags: Qt.CustomizeWindowHint | Qt.Tool | Qt.WindowCloseButtonHint - height: 200 - width: 700 - Rectangle { - anchors.fill: parent - property var peerModel: ListModel { - id: peerModel - } - TableView { - anchors.fill: parent - id: peerTable - model: peerModel - TableViewColumn{width: 100; role: "ip" ; title: "IP" } - TableViewColumn{width: 60; role: "port" ; title: "Port" } - TableViewColumn{width: 140; role: "lastResponse"; title: "Last event" } - TableViewColumn{width: 100; role: "latency"; title: "Latency" } - TableViewColumn{width: 260; role: "version" ; title: "Version" } - } - } - } - - // ******************************************* - // Components - // ******************************************* - - // New Contract component - Component { - id: newContract - Column { - id: mainContractColumn - anchors.fill: parent - function contractFormReady(){ - if(codeView.text.length > 0 && txValue.text.length > 0 && txGas.text.length > 0 && txGasPrice.length > 0) { - txButton.state = "READY" - }else{ - txButton.state = "NOTREADY" - } - } - states: [ - State{ - name: "ERROR" - PropertyChanges { target: txResult; visible:true} - PropertyChanges { target: codeView; visible:true} - }, - State { - name: "DONE" - PropertyChanges { target: txValue; visible:false} - PropertyChanges { target: txGas; visible:false} - PropertyChanges { target: txGasPrice; visible:false} - PropertyChanges { target: codeView; visible:false} - PropertyChanges { target: txButton; visible:false} - PropertyChanges { target: txDataLabel; visible:false} - PropertyChanges { target: atLabel; visible:false} - PropertyChanges { target: txFuelRecipient; visible:false} - - PropertyChanges { target: txResult; visible:true} - PropertyChanges { target: txOutput; visible:true} - PropertyChanges { target: newTxButton; visible:true} - }, - State { - name: "SETUP" - PropertyChanges { target: txValue; visible:true; text: ""} - PropertyChanges { target: txGas; visible:true; text: ""} - PropertyChanges { target: txGasPrice; visible:true; text: ""} - PropertyChanges { target: codeView; visible:true; text: ""} - PropertyChanges { target: txButton; visible:true} - PropertyChanges { target: txDataLabel; visible:true} - - PropertyChanges { target: txResult; visible:false} - PropertyChanges { target: txOutput; visible:false} - PropertyChanges { target: newTxButton; visible:false} - } - ] - width: 400 - spacing: 5 - anchors.left: parent.left - anchors.top: parent.top - anchors.leftMargin: 5 - anchors.topMargin: 5 - - ListModel { - id: denomModel - ListElement { text: "Wei" ; zeros: "" } - ListElement { text: "Ada" ; zeros: "000" } - ListElement { text: "Babbage" ; zeros: "000000" } - ListElement { text: "Shannon" ; zeros: "000000000" } - ListElement { text: "Szabo" ; zeros: "000000000000" } - ListElement { text: "Finney" ; zeros: "000000000000000" } - ListElement { text: "Ether" ; zeros: "000000000000000000" } - ListElement { text: "Einstein" ;zeros: "000000000000000000000" } - ListElement { text: "Douglas" ; zeros: "000000000000000000000000000000000000000000" } - } + ColumnLayout { + id: menuDefault + spacing: 3 + anchors { + left: parent.left + right: parent.right + } + } - TextField { - id: txFuelRecipient - placeholderText: "Address / Name or empty for contract" - //validator: RegExpValidator { regExp: /[a-f0-9]{40}/ } - width: 400 - } + Text { + text: "APPS" + font.bold: true + anchors { + left: parent.left + leftMargin: 5 + } + color: "#888888" + } - RowLayout { - TextField { - id: txValue - width: 222 - placeholderText: "Amount" - validator: RegExpValidator { regExp: /\d*/ } - onTextChanged: { - contractFormReady() - } - } + ColumnLayout { + id: menuApps + spacing: 3 + anchors { + left: parent.left + right: parent.right + } + } - ComboBox { - id: valueDenom - currentIndex: 6 - model: denomModel - } - } + Text { + text: "DEBUG" + font.bold: true + anchors { + left: parent.left + leftMargin: 5 + } + color: "#888888" + } - RowLayout { - TextField { - id: txGas - width: 50 - validator: RegExpValidator { regExp: /\d*/ } - placeholderText: "Gas" - text: "500" - /* - onTextChanged: { - contractFormReady() - } - */ - } - Label { - id: atLabel - text: "@" - } + ColumnLayout { + id: menuLegacy + spacing: 3 + anchors { + left: parent.left + right: parent.right + } + } + } + } - TextField { - id: txGasPrice - width: 200 - placeholderText: "Gas price" - text: "10" - validator: RegExpValidator { regExp: /\d*/ } - /* - onTextChanged: { - contractFormReady() - } - */ - } + /********************* + * Main view + ********************/ + Rectangle { + id: mainView + color: "#00000000" - ComboBox { - id: gasDenom - currentIndex: 4 - model: denomModel - } - } + anchors.right: parent.right + anchors.left: menu.right + anchors.bottom: parent.bottom + anchors.top: parent.top - Label { - id: txDataLabel - text: "Data" - } + function createView(component) { + var view = component.createObject(mainView) - TextArea { - id: codeView - height: 300 - anchors.topMargin: 5 - width: 400 - onTextChanged: { - contractFormReady() - } - } + return view; + } + } - Button { - id: txButton - /* enabled: false */ - states: [ - State { - name: "READY" - PropertyChanges { target: txButton; /*enabled: true*/} - }, - State { - name: "NOTREADY" - PropertyChanges { target: txButton; /*enabled:false*/} - } - ] - text: "Send" - onClicked: { - var value = txValue.text + denomModel.get(valueDenom.currentIndex).zeros; - var gasPrice = txGasPrice.text + denomModel.get(gasDenom.currentIndex).zeros; - var res = eth.create(txFuelRecipient.text, value, txGas.text, gasPrice, codeView.text) - if(res[1]) { - txResult.text = "Your contract could not be send over the network:\n" - txResult.text += res[1].error() - txResult.text += "" - mainContractColumn.state = "ERROR" - } else { - txResult.text = "Your transaction has been submitted:\n" - txOutput.text = res[0].address - mainContractColumn.state = "DONE" - } - } - } - Text { - id: txResult - visible: false - } - TextField { - id: txOutput - visible: false - width: 530 - } - Button { - id: newTxButton - visible: false - text: "Create a new transaction" - onClicked: { - this.visible = false - txResult.text = "" - txOutput.text = "" - mainContractColumn.state = "SETUP" - } - } - } - } - // New Transaction component - Component { - id: newTransaction - Column { - id: simpleSendColumn - states: [ - State{ - name: "ERROR" - }, - State { - name: "DONE" - PropertyChanges { target: txSimpleValue; visible:false} - PropertyChanges { target: txSimpleRecipient; visible:false} - PropertyChanges { target:newSimpleTxButton; visible:false} + } - PropertyChanges { target: txSimpleResult; visible:true} - PropertyChanges { target: txSimpleOutput; visible:true} - PropertyChanges { target:newSimpleTxButton; visible:true} - }, - State { - name: "SETUP" - PropertyChanges { target: txSimpleValue; visible:true; text: ""} - PropertyChanges { target: txSimpleRecipient; visible:true; text: ""} - PropertyChanges { target: txSimpleButton; visible:true} - PropertyChanges { target:newSimpleTxButton; visible:false} - } - ] - spacing: 5 - anchors.leftMargin: 5 - anchors.topMargin: 5 - anchors.top: parent.top - anchors.left: parent.left - function checkFormState(){ - if(txSimpleRecipient.text.length == 40 && txSimpleValue.text.length > 0) { - txSimpleButton.state = "READY" - }else{ - txSimpleButton.state = "NOTREADY" - } - } + /****************** + * Dialogs + *****************/ + FileDialog { + id: generalFileDialog + property var callback; + onAccepted: { + var path = this.fileUrl.toString(); + callback.call(this, path); + } - TextField { - id: txSimpleRecipient - placeholderText: "Recipient address" - Layout.fillWidth: true - //validator: RegExpValidator { regExp: /[a-f0-9]{40}/ } - width: 530 - onTextChanged: { checkFormState() } - } - TextField { - id: txSimpleValue - width: 200 - placeholderText: "Amount" - anchors.rightMargin: 5 - validator: RegExpValidator { regExp: /\d*/ } - onTextChanged: { checkFormState() } - } - Button { - id: txSimpleButton - /*enabled: false*/ - states: [ - State { - name: "READY" - PropertyChanges { target: txSimpleButton; /*enabled: true*/} - }, - State { - name: "NOTREADY" - PropertyChanges { target: txSimpleButton; /*enabled: false*/} - } - ] - text: "Send" - onClicked: { - //this.enabled = false - var res = eth.transact(txSimpleRecipient.text, txSimpleValue.text, "500", "1000000", "") - if(res[1]) { - txSimpleResult.text = "There has been an error broadcasting your transaction:" + res[1].error() - } else { - txSimpleResult.text = "Your transaction has been broadcasted over the network.\nYour transaction id is:" - txSimpleOutput.text = res[0].hash - this.visible = false - simpleSendColumn.state = "DONE" - } - } - } - Text { - id: txSimpleResult - visible: false + function show(selectExisting, callback) { + generalFileDialog.callback = callback; + generalFileDialog.selectExisting = selectExisting; - } - TextField { - id: txSimpleOutput - visible: false - width: 530 - } - Button { - id: newSimpleTxButton - visible: false - text: "Create an other transaction" - onClicked: { - this.visible = false - simpleSendColumn.state = "SETUP" - } - } - } - } -} + this.open(); + } + } + + + /****************** + * Wallet functions + *****************/ + function importApp(path) { + var ext = path.split('.').pop() + if(ext == "html" || ext == "htm") { + eth.openHtml(path) + }else if(ext == "qml"){ + addPlugin(path, {canClose: true, section: "apps"}) + } + } + + + function setWalletValue(value) { + walletValueLabel.text = value + } + + function loadPlugin(name) { + console.log("Loading plugin" + name) + var view = mainView.addPlugin(name) + } + + function setPeers(text) { + peerLabel.text = text + } + + function addPeer(peer) { + // We could just append the whole peer object but it cries if you try to alter them + peerModel.append({ip: peer.ip, port: peer.port, lastResponse:timeAgo(peer.lastSend), latency: peer.latency, version: peer.version}) + } + + function resetPeers(){ + peerModel.clear() + } + + function timeAgo(unixTs){ + var lapsed = (Date.now() - new Date(unixTs*1000)) / 1000 + return (lapsed + " seconds ago") + } + + function convertToPretty(unixTs){ + var a = new Date(unixTs*1000); + var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; + var year = a.getFullYear(); + var month = months[a.getMonth()]; + var date = a.getDate(); + var hour = a.getHours(); + var min = a.getMinutes(); + var sec = a.getSeconds(); + var time = date+' '+month+' '+year+' '+hour+':'+min+':'+sec ; + return time; + } + + /********************** + * Windows + *********************/ + Window { + id: peerWindow + //flags: Qt.CustomizeWindowHint | Qt.Tool | Qt.WindowCloseButtonHint + height: 200 + width: 700 + Rectangle { + anchors.fill: parent + property var peerModel: ListModel { + id: peerModel + } + TableView { + anchors.fill: parent + id: peerTable + model: peerModel + TableViewColumn{width: 100; role: "ip" ; title: "IP" } + TableViewColumn{width: 60; role: "port" ; title: "Port" } + TableViewColumn{width: 140; role: "lastResponse"; title: "Last event" } + TableViewColumn{width: 100; role: "latency"; title: "Latency" } + TableViewColumn{width: 260; role: "version" ; title: "Version" } + } + } + } + + Window { + id: aboutWin + visible: false + title: "About" + minimumWidth: 350 + maximumWidth: 350 + maximumHeight: 200 + minimumHeight: 200 + + Image { + id: aboutIcon + height: 150 + width: 150 + fillMode: Image.PreserveAspectFit + smooth: true + source: "../facet.png" + x: 10 + y: 10 + } + + Text { + anchors.left: aboutIcon.right + anchors.leftMargin: 10 + font.pointSize: 12 + text: "

Ethereal - Adrastea


Development

Jeffrey Wilcke
Maran Hidskes
Viktor Trón
" + } + } + + Window { + id: txImportDialog + minimumWidth: 270 + maximumWidth: 270 + maximumHeight: 50 + minimumHeight: 50 + TextField { + id: txImportField + width: 170 + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 10 + onAccepted: { + } + } + Button { + anchors.left: txImportField.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 5 + text: "Import" + onClicked: { + eth.importTx(txImportField.text) + txImportField.visible = false + } + } + Component.onCompleted: { + addrField.focus = true + } + } + + Window { + id: addPeerWin + visible: false + minimumWidth: 230 + maximumWidth: 230 + maximumHeight: 50 + minimumHeight: 50 + + TextField { + id: addrField + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 10 + placeholderText: "address:port" + onAccepted: { + eth.connectToPeer(addrField.text) + addPeerWin.visible = false + } + } + Button { + anchors.left: addrField.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 5 + text: "Add" + onClicked: { + eth.connectToPeer(addrField.text) + addPeerWin.visible = false + } + } + Component.onCompleted: { + addrField.focus = true + } + } + } diff --git a/ethereal/assets/qml/webapp.qml b/ethereal/assets/qml/webapp.qml index 5e4c035d8..ca6860036 100644 --- a/ethereal/assets/qml/webapp.qml +++ b/ethereal/assets/qml/webapp.qml @@ -2,6 +2,7 @@ import QtQuick 2.0 import QtWebKit 3.0 import QtWebKit.experimental 1.0 import QtQuick.Controls 1.0; +import QtQuick.Controls.Styles 1.0 import QtQuick.Layouts 1.0; import QtQuick.Window 2.1; import Ethereum 1.0 @@ -9,8 +10,8 @@ import Ethereum 1.0 ApplicationWindow { id: window title: "Ethereum" - width: 900 - height: 600 + width: 1000 + height: 800 minimumHeight: 300 property alias url: webview.url @@ -22,19 +23,113 @@ ApplicationWindow { anchors.fill: parent state: "inspectorShown" + RowLayout { + id: navBar + height: 40 + anchors { + left: parent.left + right: parent.right + leftMargin: 7 + } + + Button { + id: back + onClicked: { + webview.goBack() + } + style: ButtonStyle { + background: Image { + source: "../back.png" + width: 30 + height: 30 + } + } + } + + TextField { + anchors { + left: back.right + right: toggleInspector.left + leftMargin: 5 + rightMargin: 5 + } + id: uriNav + y: parent.height / 2 - this.height / 2 + + Keys.onReturnPressed: { + webview.url = this.text; + } + } + + Button { + id: toggleInspector + anchors { + right: parent.right + } + iconSource: "../bug.png" + onClicked: { + if(inspector.visible == true){ + inspector.visible = false + }else{ + inspector.visible = true + inspector.url = webview.experimental.remoteInspectorUrl + } + } + } + } + + WebView { objectName: "webView" id: webview - anchors.fill: parent - /* - anchors { - left: parent.left - right: parent.right - bottom: sizeGrip.top - top: parent.top - } - */ + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + top: navBar.bottom + } onTitleChanged: { window.title = title } + + property var cleanPath: false + onNavigationRequested: { + if(!this.cleanPath) { + var uri = request.url.toString(); + if(!/.*\:\/\/.*/.test(uri)) { + uri = "http://" + uri; + } + + var reg = /(^https?\:\/\/(?:www\.)?)([a-zA-Z0-9_\-]*\.eth)(.*)/ + + if(reg.test(uri)) { + uri.replace(reg, function(match, pre, domain, path) { + uri = pre; + + var lookup = ui.lookupDomain(domain.substring(0, domain.length - 4)); + var ip = []; + for(var i = 0, l = lookup.length; i < l; i++) { + ip.push(lookup.charCodeAt(i)) + } + + if(ip.length != 0) { + uri += lookup; + } else { + uri += domain; + } + + uri += path; + }); + } + + this.cleanPath = true; + + webview.url = uri; + } else { + // Prevent inf loop. + this.cleanPath = false; + } + } + + experimental.preferences.javascriptEnabled: true experimental.preferences.navigatorQtObjectEnabled: true experimental.preferences.developerExtrasEnabled: true @@ -46,105 +141,126 @@ ApplicationWindow { try { switch(data.call) { - case "getCoinBase": - postData(data._seed, eth.getCoinBase()) + case "getCoinBase": + postData(data._seed, eth.coinBase()) - break - case "getIsListening": - postData(data._seed, eth.getIsListening()) + break - break - case "getIsMining": - postData(data._seed, eth.getIsMining()) + case "getIsListening": + postData(data._seed, eth.isListening()) - break - case "getPeerCount": - postData(data._seed, eth.getPeerCount()) + break - break + case "getIsMining": + postData(data._seed, eth.isMining()) - case "getTxCountAt": - require(1) - postData(data._seed, eth.getTxCountAt(data.args[0])) + break - break - case "getBlockByNumber": - var block = eth.getBlock(data.args[0]) + case "getPeerCount": + postData(data._seed, eth.peerCount()) + + break + + case "getTxCountAt": + require(1) + postData(data._seed, eth.txCountAt(data.args[0])) + + break + + case "getBlockByNumber": + var block = eth.blockByNumber(data.args[0]) postData(data._seed, block) break - case "getBlockByHash": - var block = eth.getBlock(data.args[0]) + + case "getBlockByHash": + var block = eth.blockByHash(data.args[0]) postData(data._seed, block) break - case "transact": + + case "transact": require(5) var tx = eth.transact(data.args[0], data.args[1], data.args[2],data.args[3],data.args[4],data.args[5]) postData(data._seed, tx) break - case "create": - postData(data._seed, null) - break - case "getStorage": + case "getStorage": require(2); - var stateObject = eth.getStateObject(data.args[0]) - var storage = stateObject.getStorage(data.args[1]) + var stateObject = eth.stateObject(data.args[0]) + var storage = stateObject.storageAt(data.args[1]) postData(data._seed, storage) break - case "getStateKeyVals": - require(1); - var stateObject = eth.getStateObject(data.args[0]).stateKeyVal(true) - postData(data._seed,stateObject) + + case "getEachStorage": + require(1); + var storage = JSON.parse(eth.eachStorage(data.args[0])) + postData(data._seed, storage) break - case "getTransactionsFor": - require(1); - var txs = eth.getTransactionsFor(data.args[0], true) - postData(data._seed, txs) - break - case "getBalance": + case "getTransactionsFor": + require(1); + var txs = eth.transactionsFor(data.args[0], true) + postData(data._seed, txs) + + break + + case "getBalance": require(1); - postData(data._seed, eth.getStateObject(data.args[0]).value()); + postData(data._seed, eth.stateObject(data.args[0]).value()); break - case "getKey": - var key = eth.getKey().privateKey; + + case "getKey": + var key = eth.key().privateKey; postData(data._seed, key) break - case "watch": - require(1) - eth.watch(data.args[0], data.args[1]); - break - case "disconnect": + + /* + case "watch": + require(1) + eth.watch(data.args[0], data.args[1]); + + break + */ + case "watch": + require(2) + eth.watch(data.args[0], data.args[1]) + + case "disconnect": require(1) postData(data._seed, null) + break; - case "set": - console.log("'Set' has been depcrecated") - /* - for(var key in data.args) { - if(webview.hasOwnProperty(key)) { - window[key] = data.args[key]; - } - } - */ - break; - case "getSecretToAddress": + + case "getSecretToAddress": require(1) postData(data._seed, eth.secretToAddress(data.args[0])) + break; - case "debug": - console.log(data.args[0]); - break; + + case "messages": + require(1); + + var messages = JSON.parse(eth.getMessages(data.args[0])) + postData(data._seed, messages) + + break + + case "mutan": + require(1) + + var code = eth.compileMutan(data.args[0]) + postData(data._seed, "0x"+code) + + break; } } catch(e) { console.log(data.call + ": " + e) @@ -153,6 +269,11 @@ ApplicationWindow { } } + function post(seed, data) { + console.log("data", data) + postData(data._seed, data) + } + function require(args, num) { if(args.length < num) { throw("required argument count of "+num+" got "+args.length); @@ -165,6 +286,11 @@ ApplicationWindow { webview.experimental.postMessage(JSON.stringify({data: data, _event: event})) } + function onWatchedCb(data, id) { + var messages = JSON.parse(data) + postEvent("watched:"+id, messages) + } + function onNewBlockCb(block) { postEvent("block:new", block) } @@ -176,31 +302,7 @@ ApplicationWindow { postEvent(ev, [storageObject.address, storageObject.value]) } } - Rectangle { - id: toggleInspector - color: "#bcbcbc" - visible: true - height: 12 - width: 12 - anchors { - right: root.right - } - MouseArea { - onClicked: { - if(inspector.visible == true){ - inspector.visible = false - }else{ - inspector.visible = true - inspector.url = webview.experimental.remoteInspectorUrl - } - } - onDoubleClicked: { - console.log('refreshing') - webView.reload() - } - anchors.fill: parent - } - } + Rectangle { id: sizeGrip diff --git a/ethereal/assets/wallet.png b/ethereal/assets/wallet.png new file mode 100644 index 000000000..92c401e52 Binary files /dev/null and b/ethereal/assets/wallet.png differ diff --git a/ethereal/debugger.go b/ethereal/debugger.go index 096387405..7bc544377 100644 --- a/ethereal/debugger.go +++ b/ethereal/debugger.go @@ -2,15 +2,16 @@ package main import ( "fmt" + "math/big" + "strconv" + "strings" + "github.com/ethereum/eth-go/ethchain" "github.com/ethereum/eth-go/ethstate" "github.com/ethereum/eth-go/ethutil" "github.com/ethereum/eth-go/ethvm" "github.com/ethereum/go-ethereum/utils" - "github.com/go-qml/qml" - "math/big" - "strconv" - "strings" + "gopkg.in/qml.v1" ) type DebuggerWindow struct { @@ -102,14 +103,7 @@ func (self *DebuggerWindow) Debug(valueStr, gasStr, gasPriceStr, scriptStr, data } }() - data := ethutil.StringToByteFunc(dataStr, func(s string) (ret []byte) { - slice := strings.Split(dataStr, "\n") - for _, dataItem := range slice { - d := ethutil.FormatData(dataItem) - ret = append(ret, d...) - } - return - }) + data := utils.FormatTransactionData(dataStr) var err error script := ethutil.StringToByteFunc(scriptStr, func(s string) (ret []byte) { @@ -134,26 +128,13 @@ func (self *DebuggerWindow) Debug(valueStr, gasStr, gasPriceStr, scriptStr, data state := self.lib.eth.StateManager().TransState() account := self.lib.eth.StateManager().TransState().GetAccount(keyPair.Address()) contract := ethstate.NewStateObject([]byte{0}) - contract.Amount = value + contract.Balance = value self.SetAsm(script) - callerClosure := ethvm.NewClosure(account, contract, script, gas, gasPrice) - block := self.lib.eth.BlockChain().CurrentBlock - /* - vm := ethchain.NewVm(state, self.lib.eth.StateManager(), ethchain.RuntimeVars{ - Block: block, - Origin: account.Address(), - BlockNumber: block.Number, - PrevHash: block.PrevHash, - Coinbase: block.Coinbase, - Time: block.Time, - Diff: block.Difficulty, - Value: ethutil.Big(valueStr), - }) - */ + callerClosure := ethvm.NewClosure(ðstate.Message{}, account, contract, script, gas, gasPrice) env := utils.NewEnv(state, block, account.Address(), value) vm := ethvm.New(env) vm.Verbose = true diff --git a/ethereal/ext_app.go b/ethereal/ext_app.go index ac3e090f9..514084c97 100644 --- a/ethereal/ext_app.go +++ b/ethereal/ext_app.go @@ -1,12 +1,14 @@ package main import ( - "fmt" + "encoding/json" + "github.com/ethereum/eth-go/ethchain" - "github.com/ethereum/eth-go/ethpub" + "github.com/ethereum/eth-go/ethpipe" + "github.com/ethereum/eth-go/ethreact" "github.com/ethereum/eth-go/ethstate" - "github.com/ethereum/eth-go/ethutil" - "github.com/go-qml/qml" + "github.com/ethereum/go-ethereum/javascript" + "gopkg.in/qml.v1" ) type AppContainer interface { @@ -17,34 +19,37 @@ type AppContainer interface { Engine() *qml.Engine NewBlock(*ethchain.Block) - ObjectChanged(*ethstate.StateObject) - StorageChanged(*ethstate.StorageState) NewWatcher(chan bool) + Messages(ethstate.Messages, string) + Post(string, int) } type ExtApplication struct { - *ethpub.PEthereum + *ethpipe.JSPipe + eth ethchain.EthManager - blockChan chan ethutil.React - changeChan chan ethutil.React + blockChan chan ethreact.Event + messageChan chan ethreact.Event quitChan chan bool watcherQuitChan chan bool - container AppContainer - lib *UiLib - registeredEvents []string + filters map[string]*ethchain.Filter + + container AppContainer + lib *UiLib } func NewExtApplication(container AppContainer, lib *UiLib) *ExtApplication { app := &ExtApplication{ - ethpub.NewPEthereum(lib.eth), - make(chan ethutil.React, 1), - make(chan ethutil.React, 1), + ethpipe.NewJSPipe(lib.eth), + lib.eth, + make(chan ethreact.Event, 100), + make(chan ethreact.Event, 100), make(chan bool), make(chan bool), + make(map[string]*ethchain.Filter), container, lib, - nil, } return app @@ -58,8 +63,7 @@ func (app *ExtApplication) run() { err := app.container.Create() if err != nil { - fmt.Println(err) - + logger.Errorln(err) return } @@ -69,6 +73,7 @@ func (app *ExtApplication) run() { // Subscribe to events reactor := app.lib.eth.Reactor() reactor.Subscribe("newBlock", app.blockChan) + reactor.Subscribe("messages", app.messageChan) app.container.NewWatcher(app.watcherQuitChan) @@ -83,9 +88,6 @@ func (app *ExtApplication) stop() { // Clean up reactor := app.lib.eth.Reactor() reactor.Unsubscribe("newBlock", app.blockChan) - for _, event := range app.registeredEvents { - reactor.Unsubscribe(event, app.changeChan) - } // Kill the main loop app.quitChan <- true @@ -93,7 +95,6 @@ func (app *ExtApplication) stop() { close(app.blockChan) close(app.quitChan) - close(app.changeChan) app.container.Destroy() } @@ -108,26 +109,37 @@ out: if block, ok := block.Resource.(*ethchain.Block); ok { app.container.NewBlock(block) } - case object := <-app.changeChan: - if stateObject, ok := object.Resource.(*ethstate.StateObject); ok { - app.container.ObjectChanged(stateObject) - } else if storageObject, ok := object.Resource.(*ethstate.StorageState); ok { - app.container.StorageChanged(storageObject) + case msg := <-app.messageChan: + if messages, ok := msg.Resource.(ethstate.Messages); ok { + for id, filter := range app.filters { + msgs := filter.FilterMessages(messages) + if len(msgs) > 0 { + app.container.Messages(msgs, id) + } + } } } } } -func (app *ExtApplication) Watch(addr, storageAddr string) { - var event string - if len(storageAddr) == 0 { - event = "object:" + string(ethutil.Hex2Bytes(addr)) - app.lib.eth.Reactor().Subscribe(event, app.changeChan) - } else { - event = "storage:" + string(ethutil.Hex2Bytes(addr)) + ":" + string(ethutil.Hex2Bytes(storageAddr)) - app.lib.eth.Reactor().Subscribe(event, app.changeChan) +func (self *ExtApplication) Watch(filterOptions map[string]interface{}, identifier string) { + self.filters[identifier] = ethchain.NewFilterFromMap(filterOptions, self.eth) +} + +func (self *ExtApplication) GetMessages(object map[string]interface{}) string { + filter := ethchain.NewFilterFromMap(object, self.eth) + + messages := filter.Find() + var msgs []javascript.JSMessage + for _, m := range messages { + msgs = append(msgs, javascript.NewJSMessage(m)) } - app.registeredEvents = append(app.registeredEvents, event) + b, err := json.Marshal(msgs) + if err != nil { + return "{\"error\":" + err.Error() + "}" + } + + return string(b) } diff --git a/ethereal/gui.go b/ethereal/gui.go index df01cddda..f450acde6 100644 --- a/ethereal/gui.go +++ b/ethereal/gui.go @@ -2,31 +2,41 @@ package main import ( "bytes" + "encoding/json" "fmt" + "math/big" + "os" + "strconv" + "strings" + "time" + "github.com/ethereum/eth-go" "github.com/ethereum/eth-go/ethchain" "github.com/ethereum/eth-go/ethdb" "github.com/ethereum/eth-go/ethlog" "github.com/ethereum/eth-go/ethminer" - "github.com/ethereum/eth-go/ethpub" + "github.com/ethereum/eth-go/ethpipe" + "github.com/ethereum/eth-go/ethreact" "github.com/ethereum/eth-go/ethutil" "github.com/ethereum/eth-go/ethwire" "github.com/ethereum/go-ethereum/utils" - "github.com/go-qml/qml" - "math/big" - "strconv" - "strings" - "time" + "gopkg.in/qml.v1" ) var logger = ethlog.NewLogger("GUI") +type plugin struct { + Name string `json:"name"` + Path string `json:"path"` +} + type Gui struct { // The main application window win *qml.Window // QML Engine engine *qml.Engine component *qml.Common + qmlDone bool // The ethereum interface eth *eth.Ethereum @@ -35,14 +45,17 @@ type Gui struct { txDb *ethdb.LDBDatabase - pub *ethpub.PEthereum logLevel ethlog.LogLevel open bool + pipe *ethpipe.JSPipe + Session string clientIdentity *ethwire.SimpleClientIdentity config *ethutil.ConfigManager + plugins map[string]plugin + miner *ethminer.Miner } @@ -53,9 +66,17 @@ func NewWindow(ethereum *eth.Ethereum, config *ethutil.ConfigManager, clientIden panic(err) } - pub := ethpub.NewPEthereum(ethereum) + pipe := ethpipe.NewJSPipe(ethereum) + gui := &Gui{eth: ethereum, txDb: db, pipe: pipe, logLevel: ethlog.LogLevel(logLevel), Session: session, open: false, clientIdentity: clientIdentity, config: config, plugins: make(map[string]plugin)} + data, err := ethutil.ReadAllFile(ethutil.Config.ExecPath + "/plugins.json") + if err != nil { + fmt.Println(err) + } + fmt.Println("plugins:", string(data)) - return &Gui{eth: ethereum, txDb: db, pub: pub, logLevel: ethlog.LogLevel(logLevel), Session: session, open: false, clientIdentity: clientIdentity, config: config} + json.Unmarshal([]byte(data), &gui.plugins) + + return gui } func (gui *Gui) Start(assetPath string) { @@ -64,22 +85,20 @@ func (gui *Gui) Start(assetPath string) { // Register ethereum functions qml.RegisterTypes("Ethereum", 1, 0, []qml.TypeSpec{{ - Init: func(p *ethpub.PBlock, obj qml.Object) { p.Number = 0; p.Hash = "" }, + Init: func(p *ethpipe.JSBlock, obj qml.Object) { p.Number = 0; p.Hash = "" }, }, { - Init: func(p *ethpub.PTx, obj qml.Object) { p.Value = ""; p.Hash = ""; p.Address = "" }, + Init: func(p *ethpipe.JSTransaction, obj qml.Object) { p.Value = ""; p.Hash = ""; p.Address = "" }, }, { - Init: func(p *ethpub.KeyVal, obj qml.Object) { p.Key = ""; p.Value = "" }, + Init: func(p *ethpipe.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) // Expose the eth library and the ui library to QML - context.SetVar("eth", gui) - context.SetVar("pub", gui.pub) - gui.uiLib = NewUiLib(gui.engine, gui.eth, assetPath) - context.SetVar("ui", gui.uiLib) + context.SetVar("gui", gui) + context.SetVar("eth", gui.uiLib) // Load the main QML interface data, _ := ethutil.Config.Db.Get([]byte("KeyRing")) @@ -102,11 +121,13 @@ func (gui *Gui) Start(assetPath string) { logger.Infoln("Starting GUI") gui.open = true win.Show() + // only add the gui logger after window is shown otherwise slider wont be shown if addlog { ethlog.AddLogSystem(gui) } win.Wait() + // need to silence gui logger after window closed otherwise logsystem hangs (but do not save loglevel) gui.logLevel = ethlog.Silence gui.open = false @@ -118,6 +139,9 @@ func (gui *Gui) Stop() { gui.open = false gui.win.Hide() } + + gui.uiLib.jsEngine.Stop() + logger.Infoln("Stopped") } @@ -141,18 +165,54 @@ func (gui *Gui) showWallet(context *qml.Context) (*qml.Window, error) { return nil, err } - win := gui.createWindow(component) + gui.win = gui.createWindow(component) - go func() { - gui.setInitialBlockChain() - gui.loadAddressBook() - gui.readPreviousTransactions() - gui.setPeerInfo() - }() + gui.update() - go gui.update() + return gui.win, nil +} - return win, nil +func (self *Gui) DumpState(hash, path string) { + var stateDump []byte + + if len(hash) == 0 { + stateDump = self.eth.StateManager().CurrentState().Dump() + } else { + var block *ethchain.Block + if hash[0] == '#' { + i, _ := strconv.Atoi(hash[1:]) + block = self.eth.BlockChain().GetBlockByNumber(uint64(i)) + } else { + block = self.eth.BlockChain().GetBlock(ethutil.Hex2Bytes(hash)) + } + + if block == nil { + logger.Infof("block err: not found %s\n", hash) + return + } + + stateDump = block.State().Dump() + } + + file, err := os.OpenFile(path[7:], os.O_CREATE|os.O_RDWR, os.ModePerm) + if err != nil { + logger.Infoln("dump err: ", err) + return + } + defer file.Close() + + logger.Infof("dumped state (%s) to %s\n", hash, path) + + file.Write(stateDump) +} + +// The done handler will be called by QML when all views have been loaded +func (gui *Gui) Done() { + gui.qmlDone = true + +} + +func (gui *Gui) ImportKey(filePath string) { } func (gui *Gui) showKeyImport(context *qml.Context) (*qml.Window, error) { @@ -218,44 +278,84 @@ type address struct { } func (gui *Gui) loadAddressBook() { - gui.win.Root().Call("clearAddress") + view := gui.getObjectByName("infoView") + view.Call("clearAddress") - nameReg := ethpub.EthereumConfig(gui.eth.StateManager()).NameReg() + nameReg := gui.pipe.World().Config().Get("NameReg") if nameReg != nil { nameReg.EachStorage(func(name string, value *ethutil.Value) { if name[0] != 0 { value.Decode() - gui.win.Root().Call("addAddress", struct{ Name, Address string }{name, ethutil.Bytes2Hex(value.Bytes())}) + + view.Call("addAddress", struct{ Name, Address string }{name, ethutil.Bytes2Hex(value.Bytes())}) } }) } } +func (gui *Gui) insertTransaction(window string, tx *ethchain.Transaction) { + nameReg := ethpipe.New(gui.eth).World().Config().Get("NameReg") + addr := gui.address() + + var inout string + if bytes.Compare(tx.Sender(), addr) == 0 { + inout = "send" + } else { + inout = "recv" + } + + var ( + ptx = ethpipe.NewJSTx(tx) + send = nameReg.Storage(tx.Sender()) + rec = nameReg.Storage(tx.Recipient) + s, r string + ) + + if tx.CreatesContract() { + rec = nameReg.Storage(tx.CreationAddress()) + } + + if send.Len() != 0 { + s = strings.Trim(send.Str(), "\x00") + } else { + s = ethutil.Bytes2Hex(tx.Sender()) + } + if rec.Len() != 0 { + r = strings.Trim(rec.Str(), "\x00") + } else { + if tx.CreatesContract() { + r = ethutil.Bytes2Hex(tx.CreationAddress()) + } else { + r = ethutil.Bytes2Hex(tx.Recipient) + } + } + ptx.Sender = s + ptx.Address = r + + 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.Db().NewIterator(nil, nil) - addr := gui.address() for it.Next() { tx := ethchain.NewTransactionFromBytes(it.Value()) - var inout string - if bytes.Compare(tx.Sender(), addr) == 0 { - inout = "send" - } else { - inout = "recv" - } - - gui.win.Root().Call("addTx", ethpub.NewPTx(tx), inout) + gui.insertTransaction("post", tx) } it.Release() } func (gui *Gui) processBlock(block *ethchain.Block, initial bool) { - name := ethpub.FindNameInNameReg(gui.eth.StateManager(), block.Coinbase) - b := ethpub.NewPBlock(block) + name := strings.Trim(gui.pipe.World().Config().Get("NameReg").Storage(block.Coinbase).Str(), "\x00") + b := ethpipe.NewJSBlock(block) b.Name = name - gui.win.Root().Call("addBlock", b, initial) + gui.getObjectByName("chainView").Call("addBlock", b, initial) } func (gui *Gui) setWalletValue(amount, unconfirmedFunds *big.Int) { @@ -280,17 +380,115 @@ func (self *Gui) getObjectByName(objectName string) qml.Object { // Simple go routine function that updates the list of peers in the GUI func (gui *Gui) update() { - reactor := gui.eth.Reactor() + // We have to wait for qml to be done loading all the windows. + for !gui.qmlDone { + time.Sleep(500 * time.Millisecond) + } + + go func() { + go gui.setInitialBlockChain() + gui.loadAddressBook() + gui.setPeerInfo() + gui.readPreviousTransactions() + }() + + for _, plugin := range gui.plugins { + gui.win.Root().Call("addPlugin", plugin.Path, "") + } var ( - blockChan = make(chan ethutil.React, 1) - txChan = make(chan ethutil.React, 1) - objectChan = make(chan ethutil.React, 1) - peerChan = make(chan ethutil.React, 1) - chainSyncChan = make(chan ethutil.React, 1) - miningChan = make(chan ethutil.React, 1) + blockChan = make(chan ethreact.Event, 100) + txChan = make(chan ethreact.Event, 100) + objectChan = make(chan ethreact.Event, 100) + peerChan = make(chan ethreact.Event, 100) + chainSyncChan = make(chan ethreact.Event, 100) + miningChan = make(chan ethreact.Event, 100) ) + peerUpdateTicker := time.NewTicker(5 * time.Second) + generalUpdateTicker := time.NewTicker(1 * time.Second) + + state := gui.eth.StateManager().TransState() + + unconfirmedFunds := new(big.Int) + gui.win.Root().Call("setWalletValue", fmt.Sprintf("%v", ethutil.CurrencyToString(state.GetAccount(gui.address()).Balance))) + gui.getObjectByName("syncProgressIndicator").Set("visible", !gui.eth.IsUpToDate()) + + lastBlockLabel := gui.getObjectByName("lastBlockLabel") + + go func() { + for { + select { + case b := <-blockChan: + block := b.Resource.(*ethchain.Block) + gui.processBlock(block, false) + if bytes.Compare(block.Coinbase, gui.address()) == 0 { + gui.setWalletValue(gui.eth.StateManager().CurrentState().GetAccount(gui.address()).Balance, nil) + } + case txMsg := <-txChan: + tx := txMsg.Resource.(*ethchain.Transaction) + + if txMsg.Name == "newTx:pre" { + object := state.GetAccount(gui.address()) + + if bytes.Compare(tx.Sender(), gui.address()) == 0 { + unconfirmedFunds.Sub(unconfirmedFunds, tx.Value) + } else if bytes.Compare(tx.Recipient, gui.address()) == 0 { + unconfirmedFunds.Add(unconfirmedFunds, tx.Value) + } + + gui.setWalletValue(object.Balance, unconfirmedFunds) + + gui.insertTransaction("pre", tx) + } else { + object := state.GetAccount(gui.address()) + if bytes.Compare(tx.Sender(), gui.address()) == 0 { + object.SubAmount(tx.Value) + + gui.getObjectByName("transactionView").Call("addTx", ethpipe.NewJSTx(tx), "send") + gui.txDb.Put(tx.Hash(), tx.RlpEncode()) + } else if bytes.Compare(tx.Recipient, gui.address()) == 0 { + object.AddAmount(tx.Value) + + gui.getObjectByName("transactionView").Call("addTx", ethpipe.NewJSTx(tx), "recv") + gui.txDb.Put(tx.Hash(), tx.RlpEncode()) + } + + gui.setWalletValue(object.Balance, nil) + + state.UpdateStateObject(object) + } + case msg := <-chainSyncChan: + sync := msg.Resource.(bool) + gui.win.Root().ObjectByName("syncProgressIndicator").Set("visible", sync) + + case <-objectChan: + gui.loadAddressBook() + case <-peerChan: + gui.setPeerInfo() + case <-peerUpdateTicker.C: + gui.setPeerInfo() + case msg := <-miningChan: + if msg.Name == "miner:start" { + gui.miner = msg.Resource.(*ethminer.Miner) + } else { + gui.miner = nil + } + case <-generalUpdateTicker.C: + statusText := "#" + gui.eth.BlockChain().CurrentBlock.Number.String() + if gui.miner != nil { + pow := gui.miner.GetPow() + if pow.GetHashrate() != 0 { + statusText = "Mining @ " + strconv.FormatInt(pow.GetHashrate(), 10) + "Khash - " + statusText + } + } + lastBlockLabel.Set("text", statusText) + } + } + }() + + reactor := gui.eth.Reactor() + reactor.Subscribe("newBlock", blockChan) reactor.Subscribe("newTx:pre", txChan) reactor.Subscribe("newTx:post", txChan) @@ -298,98 +496,22 @@ func (gui *Gui) update() { reactor.Subscribe("miner:start", miningChan) reactor.Subscribe("miner:stop", miningChan) - nameReg := ethpub.EthereumConfig(gui.eth.StateManager()).NameReg() - if nameReg != nil { - reactor.Subscribe("object:"+string(nameReg.Address()), objectChan) - } + nameReg := gui.pipe.World().Config().Get("NameReg") + reactor.Subscribe("object:"+string(nameReg.Address()), objectChan) + reactor.Subscribe("peerList", peerChan) +} - peerUpdateTicker := time.NewTicker(5 * time.Second) - generalUpdateTicker := time.NewTicker(1 * time.Second) - - state := gui.eth.StateManager().TransState() - - unconfirmedFunds := new(big.Int) - gui.win.Root().Call("setWalletValue", fmt.Sprintf("%v", ethutil.CurrencyToString(state.GetAccount(gui.address()).Amount))) - gui.getObjectByName("syncProgressIndicator").Set("visible", !gui.eth.IsUpToDate()) - - lastBlockLabel := gui.getObjectByName("lastBlockLabel") - - for { - select { - case b := <-blockChan: - block := b.Resource.(*ethchain.Block) - gui.processBlock(block, false) - if bytes.Compare(block.Coinbase, gui.address()) == 0 { - gui.setWalletValue(gui.eth.StateManager().CurrentState().GetAccount(gui.address()).Amount, nil) - } - - case txMsg := <-txChan: - tx := txMsg.Resource.(*ethchain.Transaction) - - if txMsg.Event == "newTx:pre" { - object := state.GetAccount(gui.address()) - - if bytes.Compare(tx.Sender(), gui.address()) == 0 { - gui.win.Root().Call("addTx", ethpub.NewPTx(tx), "send") - gui.txDb.Put(tx.Hash(), tx.RlpEncode()) - - unconfirmedFunds.Sub(unconfirmedFunds, tx.Value) - } else if bytes.Compare(tx.Recipient, gui.address()) == 0 { - gui.win.Root().Call("addTx", ethpub.NewPTx(tx), "recv") - gui.txDb.Put(tx.Hash(), tx.RlpEncode()) - - unconfirmedFunds.Add(unconfirmedFunds, tx.Value) - } - - gui.setWalletValue(object.Amount, unconfirmedFunds) - } else { - object := state.GetAccount(gui.address()) - if bytes.Compare(tx.Sender(), gui.address()) == 0 { - object.SubAmount(tx.Value) - } else if bytes.Compare(tx.Recipient, gui.address()) == 0 { - object.AddAmount(tx.Value) - } - - gui.setWalletValue(object.Amount, nil) - - state.UpdateStateObject(object) - } - case msg := <-chainSyncChan: - sync := msg.Resource.(bool) - gui.win.Root().ObjectByName("syncProgressIndicator").Set("visible", sync) - - case <-objectChan: - gui.loadAddressBook() - case <-peerChan: - gui.setPeerInfo() - case <-peerUpdateTicker.C: - gui.setPeerInfo() - case msg := <-miningChan: - if msg.Event == "miner:start" { - gui.miner = msg.Resource.(*ethminer.Miner) - } else { - gui.miner = nil - } - - case <-generalUpdateTicker.C: - statusText := "#" + gui.eth.BlockChain().CurrentBlock.Number.String() - if gui.miner != nil { - pow := gui.miner.GetPow() - if pow.GetHashrate() != 0 { - statusText = "Mining @ " + strconv.FormatInt(pow.GetHashrate(), 10) + "Khash - " + statusText - } - } - lastBlockLabel.Set("text", statusText) - } - } +func (gui *Gui) CopyToClipboard(data string) { + //clipboard.WriteAll("test") + fmt.Println("COPY currently BUGGED. Here are the contents:\n", data) } func (gui *Gui) setPeerInfo() { gui.win.Root().Call("setPeers", fmt.Sprintf("%d / %d", gui.eth.PeerCount(), gui.eth.MaxPeers)) gui.win.Root().Call("resetPeers") - for _, peer := range gui.pub.GetPeers() { + for _, peer := range gui.pipe.Peers() { gui.win.Root().Call("addPeer", peer) } } @@ -402,18 +524,19 @@ func (gui *Gui) address() []byte { return gui.eth.KeyManager().Address() } -func (gui *Gui) RegisterName(name string) { - name = fmt.Sprintf("\"register\"\n\"%s\"", name) +func (gui *Gui) Transact(recipient, value, gas, gasPrice, d string) (*ethpipe.JSReceipt, error) { + var data string + if len(recipient) == 0 { + code, err := ethutil.Compile(d, false) + if err != nil { + return nil, err + } + data = ethutil.Bytes2Hex(code) + } else { + data = ethutil.Bytes2Hex(utils.FormatTransactionData(d)) + } - gui.pub.Transact(gui.privateKey(), "NameReg", "", "10000", "10000000000000", name) -} - -func (gui *Gui) Transact(recipient, value, gas, gasPrice, data string) (*ethpub.PReceipt, error) { - return gui.pub.Transact(gui.privateKey(), recipient, value, gas, gasPrice, data) -} - -func (gui *Gui) Create(recipient, value, gas, gasPrice, data string) (*ethpub.PReceipt, error) { - return gui.pub.Transact(gui.privateKey(), recipient, value, gas, gasPrice, data) + return gui.pipe.Transact(gui.privateKey(), recipient, value, gas, gasPrice, data) } func (gui *Gui) SetCustomIdentifier(customIdentifier string) { @@ -435,6 +558,20 @@ func (gui *Gui) GetLogLevel() ethlog.LogLevel { return gui.logLevel } +func (self *Gui) AddPlugin(pluginPath string) { + self.plugins[pluginPath] = plugin{Name: "SomeName", Path: pluginPath} + + json, _ := json.MarshalIndent(self.plugins, "", " ") + ethutil.WriteFile(ethutil.Config.ExecPath+"/plugins.json", json) +} + +func (self *Gui) RemovePlugin(pluginPath string) { + delete(self.plugins, pluginPath) + + json, _ := json.MarshalIndent(self.plugins, "", " ") + ethutil.WriteFile(ethutil.Config.ExecPath+"/plugins.json", json) +} + // this extra function needed to give int typecast value to gui widget // that sets initial loglevel to default func (gui *Gui) GetLogLevelInt() int { @@ -451,9 +588,13 @@ func (gui *Gui) Printf(format string, v ...interface{}) { // Print function that logs directly to the GUI func (gui *Gui) printLog(s string) { - str := strings.TrimRight(s, "\n") - lines := strings.Split(str, "\n") - for _, line := range lines { - gui.win.Root().Call("addLog", line) - } + /* + str := strings.TrimRight(s, "\n") + lines := strings.Split(str, "\n") + + view := gui.getObjectByName("infoView") + for _, line := range lines { + view.Call("addLog", line) + } + */ } diff --git a/ethereal/html_container.go b/ethereal/html_container.go index b00d3f78e..69edea570 100644 --- a/ethereal/html_container.go +++ b/ethereal/html_container.go @@ -1,18 +1,22 @@ package main import ( + "encoding/json" "errors" - "github.com/ethereum/eth-go/ethchain" - "github.com/ethereum/eth-go/ethpub" - "github.com/ethereum/eth-go/ethstate" - "github.com/ethereum/eth-go/ethutil" - "github.com/go-qml/qml" - "github.com/howeyc/fsnotify" + "fmt" "io/ioutil" "net/url" "os" "path" "path/filepath" + + "github.com/ethereum/eth-go/ethchain" + "github.com/ethereum/eth-go/ethpipe" + "github.com/ethereum/eth-go/ethstate" + "github.com/ethereum/eth-go/ethutil" + "github.com/ethereum/go-ethereum/javascript" + "github.com/howeyc/fsnotify" + "gopkg.in/qml.v1" ) type HtmlApplication struct { @@ -41,7 +45,7 @@ func (app *HtmlApplication) Create() error { return errors.New("Ethereum package not yet supported") // TODO - ethutil.OpenPackage(app.path) + //ethutil.OpenPackage(app.path) } win := component.CreateWindow(nil) @@ -118,18 +122,26 @@ func (app *HtmlApplication) Window() *qml.Window { } func (app *HtmlApplication) NewBlock(block *ethchain.Block) { - b := ðpub.PBlock{Number: int(block.BlockInfo().Number), Hash: ethutil.Bytes2Hex(block.Hash())} + b := ðpipe.JSBlock{Number: int(block.BlockInfo().Number), Hash: ethutil.Bytes2Hex(block.Hash())} app.webView.Call("onNewBlockCb", b) } -func (app *HtmlApplication) ObjectChanged(stateObject *ethstate.StateObject) { - app.webView.Call("onObjectChangeCb", ethpub.NewPStateObject(stateObject)) -} +func (self *HtmlApplication) Messages(messages ethstate.Messages, id string) { + var msgs []javascript.JSMessage + for _, m := range messages { + msgs = append(msgs, javascript.NewJSMessage(m)) + } -func (app *HtmlApplication) StorageChanged(storageObject *ethstate.StorageState) { - app.webView.Call("onStorageChangeCb", ethpub.NewPStorageState(storageObject)) + b, _ := json.Marshal(msgs) + + self.webView.Call("onWatchedCb", string(b), id) } func (app *HtmlApplication) Destroy() { app.engine.Destroy() } + +func (app *HtmlApplication) Post(data string, seed int) { + fmt.Println("about to call 'post'") + app.webView.Call("post", seed, data) +} diff --git a/ethereal/main.go b/ethereal/main.go index 0f99be886..4101efbca 100644 --- a/ethereal/main.go +++ b/ethereal/main.go @@ -1,30 +1,23 @@ package main import ( - "github.com/ethereum/eth-go/ethlog" - "github.com/ethereum/go-ethereum/utils" - "github.com/go-qml/qml" "os" "runtime" + + "github.com/ethereum/eth-go" + "github.com/ethereum/eth-go/ethlog" + "github.com/ethereum/go-ethereum/utils" + "gopkg.in/qml.v1" ) const ( ClientIdentifier = "Ethereal" - Version = "0.6.0" + Version = "0.6.3" ) -func main() { - runtime.GOMAXPROCS(runtime.NumCPU()) - - qml.Init(nil) - - var interrupted = false - utils.RegisterInterrupt(func(os.Signal) { - interrupted = true - }) - - utils.HandleInterrupt() +var ethereum *eth.Ethereum +func run() error { // precedence: code-internal flag default < config file < environment variables < command line Init() // parsing command line @@ -43,7 +36,7 @@ func main() { clientIdentity := utils.NewClientIdentity(ClientIdentifier, Version, Identifier) - ethereum := utils.NewEthereum(db, clientIdentity, keyManager, UseUPnP, OutboundPort, MaxPeer) + ethereum = utils.NewEthereum(db, clientIdentity, keyManager, UseUPnP, OutboundPort, MaxPeer) if ShowGenesis { utils.ShowGenesis(ethereum) @@ -61,6 +54,26 @@ func main() { utils.StartEthereum(ethereum, UseSeed) // gui blocks the main thread gui.Start(AssetPath) + + return nil +} + +func main() { + runtime.GOMAXPROCS(runtime.NumCPU()) + + // This is a bit of a cheat, but ey! + os.Setenv("QTWEBKIT_INSPECTOR_SERVER", "127.0.0.1:99999") + + //qml.Init(nil) + qml.Run(run) + + var interrupted = false + utils.RegisterInterrupt(func(os.Signal) { + interrupted = true + }) + + utils.HandleInterrupt() + // we need to run the interrupt callbacks in case gui is closed // this skips if we got here by actual interrupt stopping the GUI if !interrupted { diff --git a/ethereal/qml_container.go b/ethereal/qml_container.go index 1b420ee21..85bd7c699 100644 --- a/ethereal/qml_container.go +++ b/ethereal/qml_container.go @@ -1,12 +1,14 @@ package main import ( + "fmt" + "runtime" + "github.com/ethereum/eth-go/ethchain" - "github.com/ethereum/eth-go/ethpub" + "github.com/ethereum/eth-go/ethpipe" "github.com/ethereum/eth-go/ethstate" "github.com/ethereum/eth-go/ethutil" - "github.com/go-qml/qml" - "runtime" + "gopkg.in/qml.v1" ) type QmlApplication struct { @@ -25,7 +27,7 @@ func (app *QmlApplication) Create() error { path := string(app.path) // For some reason for windows we get /c:/path/to/something, windows doesn't like the first slash but is fine with the others so we are removing it - if string(app.path[0]) == "/" && runtime.GOOS == "windows" { + if app.path[0] == '/' && runtime.GOOS == "windows" { path = app.path[1:] } @@ -47,16 +49,12 @@ func (app *QmlApplication) NewWatcher(quitChan chan bool) { // Events func (app *QmlApplication) NewBlock(block *ethchain.Block) { - pblock := ðpub.PBlock{Number: int(block.BlockInfo().Number), Hash: ethutil.Bytes2Hex(block.Hash())} + pblock := ðpipe.JSBlock{Number: int(block.BlockInfo().Number), Hash: ethutil.Bytes2Hex(block.Hash())} app.win.Call("onNewBlockCb", pblock) } -func (app *QmlApplication) ObjectChanged(stateObject *ethstate.StateObject) { - app.win.Call("onObjectChangeCb", ethpub.NewPStateObject(stateObject)) -} - -func (app *QmlApplication) StorageChanged(storageObject *ethstate.StorageState) { - app.win.Call("onStorageChangeCb", ethpub.NewPStorageState(storageObject)) +func (self *QmlApplication) Messages(msgs ethstate.Messages, id string) { + fmt.Println("IMPLEMENT QML APPLICATION MESSAGES METHOD") } // Getters @@ -66,3 +64,5 @@ func (app *QmlApplication) Engine() *qml.Engine { func (app *QmlApplication) Window() *qml.Window { return app.win } + +func (app *QmlApplication) Post(data string, s int) {} diff --git a/ethereal/ui_lib.go b/ethereal/ui_lib.go index 6a62fa1df..4b8210da6 100644 --- a/ethereal/ui_lib.go +++ b/ethereal/ui_lib.go @@ -1,10 +1,20 @@ package main import ( - "github.com/ethereum/eth-go" - "github.com/ethereum/eth-go/ethutil" - "github.com/go-qml/qml" + "bytes" + "fmt" "path" + "strconv" + "strings" + + "github.com/ethereum/eth-go" + "github.com/ethereum/eth-go/ethchain" + "github.com/ethereum/eth-go/ethcrypto" + "github.com/ethereum/eth-go/ethpipe" + "github.com/ethereum/eth-go/ethstate" + "github.com/ethereum/eth-go/ethutil" + "github.com/ethereum/go-ethereum/javascript" + "gopkg.in/qml.v1" ) type memAddr struct { @@ -14,6 +24,7 @@ type memAddr struct { // UI Library that has some basic functionality exposed type UiLib struct { + *ethpipe.JSPipe engine *qml.Engine eth *eth.Ethereum connected bool @@ -22,10 +33,57 @@ type UiLib struct { win *qml.Window Db *Debugger DbWindow *DebuggerWindow + + jsEngine *javascript.JSRE + + filterCallbacks map[int][]int + filters map[int]*GuiFilter } func NewUiLib(engine *qml.Engine, eth *eth.Ethereum, assetPath string) *UiLib { - return &UiLib{engine: engine, eth: eth, assetPath: assetPath} + return &UiLib{JSPipe: ethpipe.NewJSPipe(eth), engine: engine, eth: eth, assetPath: assetPath, jsEngine: javascript.NewJSRE(eth), filterCallbacks: make(map[int][]int), filters: make(map[int]*GuiFilter)} +} + +func (self *UiLib) LookupDomain(domain string) string { + world := self.World() + + if len(domain) > 32 { + domain = string(ethcrypto.Sha3Bin([]byte(domain))) + } + data := world.Config().Get("DnsReg").StorageString(domain).Bytes() + + // Left padded = A record, Right padded = CNAME + if data[0] == 0 { + data = bytes.TrimLeft(data, "\x00") + var ipSlice []string + for _, d := range data { + ipSlice = append(ipSlice, strconv.Itoa(int(d))) + } + + return strings.Join(ipSlice, ".") + } else { + data = bytes.TrimRight(data, "\x00") + + return string(data) + } +} + +func (self *UiLib) ImportTx(rlpTx string) { + tx := ethchain.NewTransactionFromBytes(ethutil.Hex2Bytes(rlpTx)) + self.eth.TxPool().QueueTransaction(tx) +} + +func (self *UiLib) EvalJavascriptFile(path string) { + self.jsEngine.LoadExtFile(path[7:]) +} + +func (self *UiLib) EvalJavascriptString(str string) string { + value, err := self.jsEngine.Run(str) + if err != nil { + return err.Error() + } + + return fmt.Sprintf("%v", value) } func (ui *UiLib) OpenQml(path string) { @@ -42,6 +100,10 @@ func (ui *UiLib) OpenHtml(path string) { go app.run() } +func (ui *UiLib) OpenBrowser() { + ui.OpenHtml("file://" + ui.AssetPath("ext/home.html")) +} + func (ui *UiLib) Muted(content string) { component, err := ui.engine.LoadFile(ui.AssetPath("qml/muted.qml")) if err != nil { @@ -94,7 +156,96 @@ func (self *UiLib) StartDbWithCode(code string) { func (self *UiLib) StartDebugger() { dbWindow := NewDebuggerWindow(self) - //self.DbWindow = dbWindow dbWindow.Show() } + +func (self *UiLib) RegisterFilter(object map[string]interface{}, seed int) { + filter := &GuiFilter{ethpipe.NewJSFilterFromMap(object, self.eth), seed} + self.filters[seed] = filter + + filter.MessageCallback = func(messages ethstate.Messages) { + for _, callbackSeed := range self.filterCallbacks[seed] { + self.win.Root().Call("invokeFilterCallback", filter.MessagesToJson(messages), seed, callbackSeed) + } + } + +} + +func (self *UiLib) RegisterFilterString(typ string, seed int) { + filter := &GuiFilter{ethpipe.NewJSFilterFromMap(nil, self.eth), seed} + self.filters[seed] = filter + + if typ == "chain" { + filter.BlockCallback = func(block *ethchain.Block) { + for _, callbackSeed := range self.filterCallbacks[seed] { + self.win.Root().Call("invokeFilterCallback", "{}", seed, callbackSeed) + } + } + } +} + +func (self *UiLib) RegisterFilterCallback(seed, cbSeed int) { + self.filterCallbacks[seed] = append(self.filterCallbacks[seed], cbSeed) +} + +func (self *UiLib) UninstallFilter(seed int) { + filter := self.filters[seed] + if filter != nil { + filter.Uninstall() + delete(self.filters, seed) + } +} + +type GuiFilter struct { + *ethpipe.JSFilter + seed int +} + +func (self *UiLib) Transact(object map[string]interface{}) (*ethpipe.JSReceipt, error) { + // Default values + if object["from"] == nil { + object["from"] = "" + } + if object["to"] == nil { + object["to"] = "" + } + if object["value"] == nil { + object["value"] = "" + } + if object["gas"] == nil { + object["gas"] = "" + } + if object["gasPrice"] == nil { + object["gasPrice"] = "" + } + + var dataStr string + var data []string + if list, ok := object["data"].(*qml.List); ok { + list.Convert(&data) + } + + for _, str := range data { + if ethutil.IsHex(str) { + str = str[2:] + + if len(str) != 64 { + str = ethutil.LeftPadString(str, 64) + } + } else { + str = ethutil.Bytes2Hex(ethutil.LeftPadBytes(ethutil.Big(str).Bytes(), 32)) + } + + dataStr += str + } + + return self.JSPipe.Transact( + object["from"].(string), + object["to"].(string), + object["value"].(string), + object["gas"].(string), + object["gasPrice"].(string), + dataStr, + ) +} diff --git a/ethereum/cmd.go b/ethereum/cmd.go index ff2b8409c..5ddc91619 100644 --- a/ethereum/cmd.go +++ b/ethereum/cmd.go @@ -1,11 +1,13 @@ package main import ( - "github.com/ethereum/eth-go" - "github.com/ethereum/go-ethereum/ethereum/repl" - "github.com/ethereum/go-ethereum/utils" "io/ioutil" "os" + + "github.com/ethereum/eth-go" + "github.com/ethereum/go-ethereum/ethereum/repl" + "github.com/ethereum/go-ethereum/javascript" + "github.com/ethereum/go-ethereum/utils" ) func InitJsConsole(ethereum *eth.Ethereum) { @@ -25,7 +27,7 @@ func ExecJsFile(ethereum *eth.Ethereum, InputFile string) { if err != nil { logger.Fatalln(err) } - re := ethrepl.NewJSRE(ethereum) + re := javascript.NewJSRE(ethereum) utils.RegisterInterrupt(func(os.Signal) { re.Stop() }) diff --git a/ethereum/flags.go b/ethereum/flags.go index 4f59ddf06..5ed208411 100644 --- a/ethereum/flags.go +++ b/ethereum/flags.go @@ -3,10 +3,11 @@ package main import ( "flag" "fmt" - "github.com/ethereum/eth-go/ethlog" "os" "os/user" "path" + + "github.com/ethereum/eth-go/ethlog" ) var Identifier string @@ -31,6 +32,9 @@ var LogFile string var ConfigFile string var DebugFile string var LogLevel int +var Dump bool +var DumpHash string +var DumpNumber int // flags specific to cli client var StartMining bool @@ -71,6 +75,10 @@ func Init() { flag.BoolVar(&DiffTool, "difftool", false, "creates output for diff'ing. Sets LogLevel=0") flag.StringVar(&DiffType, "diff", "all", "sets the level of diff output [vm, all]. Has no effect if difftool=false") + flag.BoolVar(&Dump, "dump", false, "output the ethereum state in JSON format. Sub args [number, hash]") + flag.StringVar(&DumpHash, "hash", "", "specify arg in hex") + flag.IntVar(&DumpNumber, "number", -1, "specify arg in number") + flag.BoolVar(&StartMining, "mine", false, "start dagger mining") flag.BoolVar(&StartJsConsole, "js", false, "launches javascript console") diff --git a/ethereum/main.go b/ethereum/main.go index 217991074..a8e60dec7 100644 --- a/ethereum/main.go +++ b/ethereum/main.go @@ -1,15 +1,19 @@ package main import ( + "fmt" + "os" + "runtime" + + "github.com/ethereum/eth-go/ethchain" "github.com/ethereum/eth-go/ethlog" "github.com/ethereum/eth-go/ethutil" "github.com/ethereum/go-ethereum/utils" - "runtime" ) const ( ClientIdentifier = "Ethereum(G)" - Version = "0.6.0" + Version = "0.6.3" ) var logger = ethlog.NewLogger("CLI") @@ -23,7 +27,7 @@ func main() { Init() // parsing command line // If the difftool option is selected ignore all other log output - if DiffTool { + if DiffTool || Dump { LogLevel = 0 } @@ -46,6 +50,32 @@ func main() { ethereum := utils.NewEthereum(db, clientIdentity, keyManager, UseUPnP, OutboundPort, MaxPeer) + if Dump { + var block *ethchain.Block + + if len(DumpHash) == 0 && DumpNumber == -1 { + block = ethereum.BlockChain().CurrentBlock + } else if len(DumpHash) > 0 { + block = ethereum.BlockChain().GetBlock(ethutil.Hex2Bytes(DumpHash)) + } else { + block = ethereum.BlockChain().GetBlockByNumber(uint64(DumpNumber)) + } + + if block == nil { + fmt.Fprintln(os.Stderr, "block not found") + + // We want to output valid JSON + fmt.Println("{}") + + os.Exit(1) + } + + // Leave the Println. This needs clean output for piping + fmt.Printf("%s\n", block.State().Dump()) + + os.Exit(0) + } + if ShowGenesis { utils.ShowGenesis(ethereum) } diff --git a/ethereum/repl/repl.go b/ethereum/repl/repl.go index 92d4ad86a..d08feb7b4 100644 --- a/ethereum/repl/repl.go +++ b/ethereum/repl/repl.go @@ -3,12 +3,14 @@ package ethrepl import ( "bufio" "fmt" - "github.com/ethereum/eth-go" - "github.com/ethereum/eth-go/ethlog" - "github.com/ethereum/eth-go/ethutil" "io" "os" "path" + + "github.com/ethereum/eth-go" + "github.com/ethereum/eth-go/ethlog" + "github.com/ethereum/eth-go/ethutil" + "github.com/ethereum/go-ethereum/javascript" ) var logger = ethlog.NewLogger("REPL") @@ -19,7 +21,7 @@ type Repl interface { } type JSRepl struct { - re *JSRE + re *javascript.JSRE prompt string @@ -34,7 +36,7 @@ func NewJSRepl(ethereum *eth.Ethereum) *JSRepl { panic(err) } - return &JSRepl{re: NewJSRE(ethereum), prompt: "> ", history: hist} + return &JSRepl{re: javascript.NewJSRE(ethereum), prompt: "> ", history: hist} } func (self *JSRepl) Start() { diff --git a/ethereum/repl/repl_darwin.go b/ethereum/repl/repl_darwin.go index 3a91b0d44..4c07280f7 100644 --- a/ethereum/repl/repl_darwin.go +++ b/ethereum/repl/repl_darwin.go @@ -115,8 +115,8 @@ L: } func (self *JSRepl) PrintValue(v interface{}) { - method, _ := self.re.vm.Get("prettyPrint") - v, err := self.re.vm.ToValue(v) + method, _ := self.re.Vm.Get("prettyPrint") + v, err := self.re.Vm.ToValue(v) if err == nil { method.Call(method, v) } diff --git a/ethereum/repl/types.go b/ethereum/repl/types.go deleted file mode 100644 index 16a18e6e5..000000000 --- a/ethereum/repl/types.go +++ /dev/null @@ -1,95 +0,0 @@ -package ethrepl - -import ( - "fmt" - "github.com/ethereum/eth-go/ethpub" - "github.com/ethereum/eth-go/ethutil" - "github.com/obscuren/otto" -) - -type JSStateObject struct { - *ethpub.PStateObject - eth *JSEthereum -} - -func (self *JSStateObject) EachStorage(call otto.FunctionCall) otto.Value { - cb := call.Argument(0) - self.PStateObject.EachStorage(func(key string, value *ethutil.Value) { - value.Decode() - - cb.Call(self.eth.toVal(self), self.eth.toVal(key), self.eth.toVal(ethutil.Bytes2Hex(value.Bytes()))) - }) - - return otto.UndefinedValue() -} - -// The JSEthereum object attempts to wrap the PEthereum object and returns -// meaningful javascript objects -type JSBlock struct { - *ethpub.PBlock - eth *JSEthereum -} - -func (self *JSBlock) GetTransaction(hash string) otto.Value { - return self.eth.toVal(self.PBlock.GetTransaction(hash)) -} - -type JSEthereum struct { - *ethpub.PEthereum - vm *otto.Otto -} - -func (self *JSEthereum) GetBlock(hash string) otto.Value { - return self.toVal(&JSBlock{self.PEthereum.GetBlock(hash), self}) -} - -func (self *JSEthereum) GetPeers() otto.Value { - return self.toVal(self.PEthereum.GetPeers()) -} - -func (self *JSEthereum) GetKey() otto.Value { - return self.toVal(self.PEthereum.GetKey()) -} - -func (self *JSEthereum) GetStateObject(addr string) otto.Value { - return self.toVal(&JSStateObject{self.PEthereum.GetStateObject(addr), self}) -} - -func (self *JSEthereum) GetStateKeyVals(addr string) otto.Value { - return self.toVal(self.PEthereum.GetStateObject(addr).StateKeyVal(false)) -} - -func (self *JSEthereum) Transact(key, recipient, valueStr, gasStr, gasPriceStr, dataStr string) otto.Value { - r, err := self.PEthereum.Transact(key, recipient, valueStr, gasStr, gasPriceStr, dataStr) - if err != nil { - fmt.Println(err) - - return otto.UndefinedValue() - } - - return self.toVal(r) -} - -func (self *JSEthereum) Create(key, valueStr, gasStr, gasPriceStr, scriptStr string) otto.Value { - r, err := self.PEthereum.Create(key, valueStr, gasStr, gasPriceStr, scriptStr) - - if err != nil { - fmt.Println(err) - - return otto.UndefinedValue() - } - - return self.toVal(r) -} - -func (self *JSEthereum) toVal(v interface{}) otto.Value { - result, err := self.vm.ToValue(v) - - if err != nil { - fmt.Println("Value unknown:", err) - - return otto.UndefinedValue() - } - - return result -} diff --git a/ethereum/repl/javascript_runtime.go b/javascript/javascript_runtime.go similarity index 72% rename from ethereum/repl/javascript_runtime.go rename to javascript/javascript_runtime.go index 41b6216d4..c794c32a8 100644 --- a/ethereum/repl/javascript_runtime.go +++ b/javascript/javascript_runtime.go @@ -1,30 +1,32 @@ -package ethrepl +package javascript import ( "fmt" - "github.com/ethereum/eth-go" - "github.com/ethereum/eth-go/ethchain" - "github.com/ethereum/eth-go/ethlog" - "github.com/ethereum/eth-go/ethpub" - "github.com/ethereum/eth-go/ethstate" - "github.com/ethereum/eth-go/ethutil" - "github.com/ethereum/go-ethereum/utils" - "github.com/obscuren/otto" "io/ioutil" "os" "path" "path/filepath" + + "github.com/ethereum/eth-go" + "github.com/ethereum/eth-go/ethchain" + "github.com/ethereum/eth-go/ethlog" + "github.com/ethereum/eth-go/ethpipe" + "github.com/ethereum/eth-go/ethreact" + "github.com/ethereum/eth-go/ethstate" + "github.com/ethereum/eth-go/ethutil" + "github.com/ethereum/go-ethereum/utils" + "github.com/obscuren/otto" ) var jsrelogger = ethlog.NewLogger("JSRE") type JSRE struct { ethereum *eth.Ethereum - vm *otto.Otto - lib *ethpub.PEthereum + Vm *otto.Otto + pipe *ethpipe.JSPipe - blockChan chan ethutil.React - changeChan chan ethutil.React + blockChan chan ethreact.Event + changeChan chan ethreact.Event quitChan chan bool objectCb map[string][]otto.Value @@ -33,9 +35,9 @@ type JSRE struct { func (jsre *JSRE) LoadExtFile(path string) { result, err := ioutil.ReadFile(path) if err == nil { - jsre.vm.Run(result) + jsre.Vm.Run(result) } else { - jsrelogger.Debugln("Could not load file:", path) + jsrelogger.Infoln("Could not load file:", path) } } @@ -48,15 +50,15 @@ func NewJSRE(ethereum *eth.Ethereum) *JSRE { re := &JSRE{ ethereum, otto.New(), - ethpub.NewPEthereum(ethereum), - make(chan ethutil.React, 1), - make(chan ethutil.React, 1), + ethpipe.NewJSPipe(ethereum), + make(chan ethreact.Event, 10), + make(chan ethreact.Event, 10), make(chan bool), make(map[string][]otto.Value), } // Init the JS lib - re.vm.Run(jsLib) + re.Vm.Run(jsLib) // Load extra javascript files re.LoadIntFile("string.js") @@ -65,7 +67,11 @@ func NewJSRE(ethereum *eth.Ethereum) *JSRE { // We have to make sure that, whoever calls this, calls "Stop" go re.mainLoop() - re.Bind("eth", &JSEthereum{re.lib, re.vm}) + // Subscribe to events + reactor := ethereum.Reactor() + reactor.Subscribe("newBlock", re.blockChan) + + re.Bind("eth", &JSEthereum{re.pipe, re.Vm, ethereum}) re.initStdFuncs() @@ -75,11 +81,11 @@ func NewJSRE(ethereum *eth.Ethereum) *JSRE { } func (self *JSRE) Bind(name string, v interface{}) { - self.vm.Set(name, v) + self.Vm.Set(name, v) } func (self *JSRE) Run(code string) (otto.Value, error) { - return self.vm.Run(code) + return self.Vm.Run(code) } func (self *JSRE) Require(file string) error { @@ -109,10 +115,6 @@ func (self *JSRE) Stop() { } func (self *JSRE) mainLoop() { - // Subscribe to events - reactor := self.ethereum.Reactor() - reactor.Subscribe("newBlock", self.blockChan) - out: for { select { @@ -121,24 +123,12 @@ out: case block := <-self.blockChan: if _, ok := block.Resource.(*ethchain.Block); ok { } - case object := <-self.changeChan: - if stateObject, ok := object.Resource.(*ethstate.StateObject); ok { - for _, cb := range self.objectCb[ethutil.Bytes2Hex(stateObject.Address())] { - val, _ := self.vm.ToValue(ethpub.NewPStateObject(stateObject)) - cb.Call(cb, val) - } - } else if storageObject, ok := object.Resource.(*ethstate.StorageState); ok { - for _, cb := range self.objectCb[ethutil.Bytes2Hex(storageObject.StateAddress)+ethutil.Bytes2Hex(storageObject.Address)] { - val, _ := self.vm.ToValue(ethpub.NewPStorageState(storageObject)) - cb.Call(cb, val) - } - } } } } func (self *JSRE) initStdFuncs() { - t, _ := self.vm.Get("eth") + t, _ := self.Vm.Get("eth") eth := t.Object() eth.Set("watch", self.watch) eth.Set("addPeer", self.addPeer) @@ -146,19 +136,51 @@ func (self *JSRE) initStdFuncs() { eth.Set("stopMining", self.stopMining) eth.Set("startMining", self.startMining) eth.Set("execBlock", self.execBlock) + eth.Set("dump", self.dump) } /* * The following methods are natively implemented javascript functions */ +func (self *JSRE) dump(call otto.FunctionCall) otto.Value { + var state *ethstate.State + + if len(call.ArgumentList) > 0 { + var block *ethchain.Block + if call.Argument(0).IsNumber() { + num, _ := call.Argument(0).ToInteger() + block = self.ethereum.BlockChain().GetBlockByNumber(uint64(num)) + } else if call.Argument(0).IsString() { + hash, _ := call.Argument(0).ToString() + block = self.ethereum.BlockChain().GetBlock(ethutil.Hex2Bytes(hash)) + } else { + fmt.Println("invalid argument for dump. Either hex string or number") + } + + if block == nil { + fmt.Println("block not found") + + return otto.UndefinedValue() + } + + state = block.State() + } else { + state = self.ethereum.StateManager().CurrentState() + } + + v, _ := self.Vm.ToValue(state.Dump()) + + return v +} + func (self *JSRE) stopMining(call otto.FunctionCall) otto.Value { - v, _ := self.vm.ToValue(utils.StopMining(self.ethereum)) + v, _ := self.Vm.ToValue(utils.StopMining(self.ethereum)) return v } func (self *JSRE) startMining(call otto.FunctionCall) otto.Value { - v, _ := self.vm.ToValue(utils.StartMining(self.ethereum)) + v, _ := self.Vm.ToValue(utils.StartMining(self.ethereum)) return v } @@ -211,7 +233,7 @@ func (self *JSRE) require(call otto.FunctionCall) otto.Value { return otto.UndefinedValue() } - t, _ := self.vm.Get("exports") + t, _ := self.Vm.Get("exports") return t } diff --git a/ethereum/repl/js_lib.go b/javascript/js_lib.go similarity index 98% rename from ethereum/repl/js_lib.go rename to javascript/js_lib.go index c781c43d0..a3e9b8a5b 100644 --- a/ethereum/repl/js_lib.go +++ b/javascript/js_lib.go @@ -1,4 +1,4 @@ -package ethrepl +package javascript const jsLib = ` function pp(object) { diff --git a/javascript/types.go b/javascript/types.go new file mode 100644 index 000000000..afa0a41c6 --- /dev/null +++ b/javascript/types.go @@ -0,0 +1,138 @@ +package javascript + +import ( + "fmt" + + "github.com/ethereum/eth-go" + "github.com/ethereum/eth-go/ethchain" + "github.com/ethereum/eth-go/ethpipe" + "github.com/ethereum/eth-go/ethstate" + "github.com/ethereum/eth-go/ethutil" + "github.com/obscuren/otto" +) + +type JSStateObject struct { + *ethpipe.JSObject + eth *JSEthereum +} + +func (self *JSStateObject) EachStorage(call otto.FunctionCall) otto.Value { + cb := call.Argument(0) + self.JSObject.EachStorage(func(key string, value *ethutil.Value) { + value.Decode() + + cb.Call(self.eth.toVal(self), self.eth.toVal(key), self.eth.toVal(ethutil.Bytes2Hex(value.Bytes()))) + }) + + return otto.UndefinedValue() +} + +// The JSEthereum object attempts to wrap the PEthereum object and returns +// meaningful javascript objects +type JSBlock struct { + *ethpipe.JSBlock + eth *JSEthereum +} + +func (self *JSBlock) GetTransaction(hash string) otto.Value { + return self.eth.toVal(self.JSBlock.GetTransaction(hash)) +} + +type JSMessage struct { + To string `json:"to"` + From string `json:"from"` + Input string `json:"input"` + Output string `json:"output"` + Path int `json:"path"` + Origin string `json:"origin"` + Timestamp int32 `json:"timestamp"` + Coinbase string `json:"coinbase"` + Block string `json:"block"` + Number int32 `json:"number"` +} + +func NewJSMessage(message *ethstate.Message) JSMessage { + return JSMessage{ + To: ethutil.Bytes2Hex(message.To), + From: ethutil.Bytes2Hex(message.From), + Input: ethutil.Bytes2Hex(message.Input), + Output: ethutil.Bytes2Hex(message.Output), + Path: message.Path, + Origin: ethutil.Bytes2Hex(message.Origin), + Timestamp: int32(message.Timestamp), + Coinbase: ethutil.Bytes2Hex(message.Origin), + Block: ethutil.Bytes2Hex(message.Block), + Number: int32(message.Number.Int64()), + } +} + +type JSEthereum struct { + *ethpipe.JSPipe + vm *otto.Otto + ethereum *eth.Ethereum +} + +func (self *JSEthereum) GetBlock(hash string) otto.Value { + return self.toVal(&JSBlock{self.JSPipe.BlockByHash(hash), self}) +} + +func (self *JSEthereum) GetPeers() otto.Value { + return self.toVal(self.JSPipe.Peers()) +} + +func (self *JSEthereum) GetKey() otto.Value { + return self.toVal(self.JSPipe.Key()) +} + +func (self *JSEthereum) GetStateObject(addr string) otto.Value { + return self.toVal(&JSStateObject{ethpipe.NewJSObject(self.JSPipe.World().SafeGet(ethutil.Hex2Bytes(addr))), self}) +} + +func (self *JSEthereum) Transact(key, recipient, valueStr, gasStr, gasPriceStr, dataStr string) otto.Value { + r, err := self.JSPipe.Transact(key, recipient, valueStr, gasStr, gasPriceStr, dataStr) + if err != nil { + fmt.Println(err) + + return otto.UndefinedValue() + } + + return self.toVal(r) +} + +func (self *JSEthereum) Create(key, valueStr, gasStr, gasPriceStr, scriptStr string) otto.Value { + r, err := self.JSPipe.Transact(key, "", valueStr, gasStr, gasPriceStr, scriptStr) + + if err != nil { + fmt.Println(err) + + return otto.UndefinedValue() + } + + return self.toVal(r) +} + +func (self *JSEthereum) toVal(v interface{}) otto.Value { + result, err := self.vm.ToValue(v) + + if err != nil { + fmt.Println("Value unknown:", err) + + return otto.UndefinedValue() + } + + return result +} + +func (self *JSEthereum) Messages(object map[string]interface{}) otto.Value { + filter := ethchain.NewFilterFromMap(object, self.ethereum) + + messages := filter.Find() + var msgs []JSMessage + for _, m := range messages { + msgs = append(msgs, NewJSMessage(m)) + } + + v, _ := self.vm.ToValue(msgs) + + return v +} diff --git a/utils/cmd.go b/utils/cmd.go index 5d0b3463c..cda735c27 100644 --- a/utils/cmd.go +++ b/utils/cmd.go @@ -1,25 +1,27 @@ package utils import ( - "bitbucket.org/kardianos/osext" "fmt" - "github.com/ethereum/eth-go" - "github.com/ethereum/eth-go/ethcrypto" - "github.com/ethereum/eth-go/ethdb" - "github.com/ethereum/eth-go/ethlog" - "github.com/ethereum/eth-go/ethminer" - "github.com/ethereum/eth-go/ethpub" - "github.com/ethereum/eth-go/ethrpc" - "github.com/ethereum/eth-go/ethutil" - "github.com/ethereum/eth-go/ethwire" "io" "log" "os" "os/signal" "path" "path/filepath" + "regexp" "runtime" "time" + + "bitbucket.org/kardianos/osext" + "github.com/ethereum/eth-go" + "github.com/ethereum/eth-go/ethcrypto" + "github.com/ethereum/eth-go/ethdb" + "github.com/ethereum/eth-go/ethlog" + "github.com/ethereum/eth-go/ethminer" + "github.com/ethereum/eth-go/ethpipe" + "github.com/ethereum/eth-go/ethrpc" + "github.com/ethereum/eth-go/ethutil" + "github.com/ethereum/eth-go/ethwire" ) var logger = ethlog.NewLogger("CLI") @@ -127,6 +129,7 @@ func NewDatabase() ethutil.Database { } func NewClientIdentity(clientIdentifier, version, customIdentifier string) *ethwire.SimpleClientIdentity { + logger.Infoln("identity created") return ethwire.NewSimpleClientIdentity(clientIdentifier, version, customIdentifier) } @@ -193,7 +196,6 @@ func DefaultAssetPath() string { } func KeyTasks(keyManager *ethcrypto.KeyManager, KeyRing string, GenAddr bool, SecretFile string, ExportDir string, NonInteractive bool) { - ethcrypto.InitWords(DefaultAssetPath()) // Init mnemonic word list var err error switch { @@ -226,7 +228,7 @@ func KeyTasks(keyManager *ethcrypto.KeyManager, KeyRing string, GenAddr bool, Se func StartRpc(ethereum *eth.Ethereum, RpcPort int) { var err error - ethereum.RpcServer, err = ethrpc.NewJsonRpcServer(ethpub.NewPEthereum(ethereum), RpcPort) + ethereum.RpcServer, err = ethrpc.NewJsonRpcServer(ethpipe.NewJSPipe(ethereum), RpcPort) if err != nil { logger.Errorf("Could not start RPC interface (port %v): %v", RpcPort, err) } else { @@ -243,21 +245,18 @@ func GetMiner() *ethminer.Miner { func StartMining(ethereum *eth.Ethereum) bool { if !ethereum.Mining { ethereum.Mining = true - addr := ethereum.KeyManager().Address() go func() { + logger.Infoln("Start mining") if miner == nil { miner = ethminer.NewDefaultMiner(addr, ethereum) } - // Give it some time to connect with peers time.Sleep(3 * time.Second) for !ethereum.IsUpToDate() { time.Sleep(5 * time.Second) } - - logger.Infoln("Miner started") miner.Start() }() RegisterInterrupt(func(os.Signal) { @@ -268,12 +267,23 @@ func StartMining(ethereum *eth.Ethereum) bool { return false } +func FormatTransactionData(data string) []byte { + d := ethutil.StringToByteFunc(data, func(s string) (ret []byte) { + slice := regexp.MustCompile("\\n|\\s").Split(s, 1000000000) + for _, dataItem := range slice { + d := ethutil.FormatData(dataItem) + ret = append(ret, d...) + } + return + }) + + return d +} + func StopMining(ethereum *eth.Ethereum) bool { if ethereum.Mining && miner != nil { miner.Stop() - - logger.Infoln("Miner stopped") - + logger.Infoln("Stopped mining") ethereum.Mining = false return true diff --git a/utils/vm_env.go b/utils/vm_env.go index 2c40dd7b8..30568c421 100644 --- a/utils/vm_env.go +++ b/utils/vm_env.go @@ -1,9 +1,10 @@ package utils import ( + "math/big" + "github.com/ethereum/eth-go/ethchain" "github.com/ethereum/eth-go/ethstate" - "math/big" ) type VMEnv struct { @@ -29,5 +30,6 @@ func (self *VMEnv) PrevHash() []byte { return self.block.PrevHash } func (self *VMEnv) Coinbase() []byte { return self.block.Coinbase } func (self *VMEnv) Time() int64 { return self.block.Time } func (self *VMEnv) Difficulty() *big.Int { return self.block.Difficulty } +func (self *VMEnv) BlockHash() []byte { return self.block.Hash() } func (self *VMEnv) Value() *big.Int { return self.value } func (self *VMEnv) State() *ethstate.State { return self.state }