diff --git a/mix/CodeModel.cpp b/mix/CodeModel.cpp index a9cfcc336..aae9dac86 100644 --- a/mix/CodeModel.cpp +++ b/mix/CodeModel.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include "QContractDefinition.h" #include "QFunctionDefinition.h" #include "QVariableDeclaration.h" @@ -61,7 +62,6 @@ CompilationResult::CompilationResult(const dev::solidity::CompilerStack& _compil auto const& contractDefinition = _compiler.getContractDefinition(std::string()); m_contract.reset(new QContractDefinition(&contractDefinition)); m_bytes = _compiler.getBytecode(); - m_assemblyCode = QString::fromStdString(dev::eth::disassemble(m_bytes)); dev::solidity::InterfaceHandler interfaceHandler; m_contractInterface = QString::fromStdString(*interfaceHandler.getABIInterface(contractDefinition)); if (m_contractInterface.isEmpty()) @@ -78,11 +78,15 @@ CompilationResult::CompilationResult(CompilationResult const& _prev, QString con m_contract(_prev.m_contract), m_compilerMessage(_compilerMessage), m_bytes(_prev.m_bytes), - m_assemblyCode(_prev.m_assemblyCode), m_contractInterface(_prev.m_contractInterface), m_codeHighlighter(_prev.m_codeHighlighter) {} +QString CompilationResult::codeHex() const +{ + return QString::fromStdString(toJS(m_bytes)); +} + CodeModel::CodeModel(QObject* _parent): QObject(_parent), m_compiling(false), diff --git a/mix/CodeModel.h b/mix/CodeModel.h index 5f2add874..0262aa094 100644 --- a/mix/CodeModel.h +++ b/mix/CodeModel.h @@ -69,6 +69,7 @@ class CompilationResult: public QObject Q_PROPERTY(QString compilerMessage READ compilerMessage CONSTANT) Q_PROPERTY(bool successful READ successful CONSTANT) Q_PROPERTY(QString contractInterface READ contractInterface CONSTANT) + Q_PROPERTY(QString codeHex READ codeHex CONSTANT) public: /// Empty compilation result constructor @@ -88,8 +89,8 @@ public: QString compilerMessage() const { return m_compilerMessage; } /// @returns contract bytecode dev::bytes const& bytes() const { return m_bytes; } - /// @returns contract bytecode in human-readable form - QString assemblyCode() const { return m_assemblyCode; } + /// @returns contract bytecode as hex string + QString codeHex() const; /// @returns contract definition in JSON format QString contractInterface() const { return m_contractInterface; } /// Get code highlighter @@ -101,7 +102,6 @@ private: std::shared_ptr m_contract; QString m_compilerMessage; ///< @todo: use some structure here dev::bytes m_bytes; - QString m_assemblyCode; QString m_contractInterface; std::shared_ptr m_codeHighlighter; diff --git a/mix/FileIo.cpp b/mix/FileIo.cpp index 52fd57902..818d8c887 100644 --- a/mix/FileIo.cpp +++ b/mix/FileIo.cpp @@ -70,6 +70,12 @@ void FileIo::writeFile(QString const& _url, QString const& _data) void FileIo::copyFile(QString const& _sourceUrl, QString const& _destUrl) { + if (QUrl(_sourceUrl).scheme() == "qrc") + { + writeFile(_destUrl, readFile(_sourceUrl)); + return; + } + QUrl sourceUrl(_sourceUrl); QUrl destUrl(_destUrl); if (!QFile::copy(sourceUrl.path(), destUrl.path())) diff --git a/mix/HttpServer.cpp b/mix/HttpServer.cpp index cfe5c37f4..bf210444b 100644 --- a/mix/HttpServer.cpp +++ b/mix/HttpServer.cpp @@ -132,23 +132,25 @@ void HttpServer::readClient() if (socket->canReadLine()) { QString hdr = QString(socket->readLine()); - if (hdr.startsWith("POST")) + if (hdr.startsWith("POST") || hdr.startsWith("GET")) { + QUrl url(hdr.split(' ')[1]); QString l; do l = socket->readLine(); while (!(l.isEmpty() || l == "\r" || l == "\r\n")); QString content = socket->readAll(); - QUrl url; std::unique_ptr request(new HttpRequest(this, url, content)); clientConnected(request.get()); QTextStream os(socket); os.setAutoDetectUnicode(true); + QString q; ///@todo: allow setting response content-type, charset, etc - os << "HTTP/1.0 200 Ok\r\n" - "Content-Type: text/plain; charset=\"utf-8\"\r\n" - "\r\n"; + os << "HTTP/1.0 200 Ok\r\n"; + if (!request->m_responseContentType.isEmpty()) + os << "Content-Type: " << request->m_responseContentType << "; "; + os << "charset=\"utf-8\"\r\n\r\n"; os << request->m_response; } } diff --git a/mix/HttpServer.h b/mix/HttpServer.h index 00d63a073..add83238b 100644 --- a/mix/HttpServer.h +++ b/mix/HttpServer.h @@ -51,11 +51,15 @@ public: /// Set response for a request /// @param _response Response body. If no response is set, server returns status 200 with empty body Q_INVOKABLE void setResponse(QString const& _response) { m_response = _response; } + /// Set response content type + /// @param _contentType Response content type string. text/plain by default + Q_INVOKABLE void setResponseContentType(QString const& _contentType) { m_responseContentType = _contentType ; } private: QUrl m_url; QString m_content; QString m_response; + QString m_responseContentType; friend class HttpServer; }; diff --git a/mix/qml/CodeEditorView.qml b/mix/qml/CodeEditorView.qml index 36fc586b3..4d54994fe 100644 --- a/mix/qml/CodeEditorView.qml +++ b/mix/qml/CodeEditorView.qml @@ -4,18 +4,26 @@ import QtQuick.Layouts 1.0 import QtQuick.Controls 1.0 Item { - + id: codeEditorView property string currentDocumentId: "" + signal documentEdit(string documentId) function getDocumentText(documentId) { - for (i = 0; i < editorListModel.count; i++) { + for (var i = 0; i < editorListModel.count; i++) { if (editorListModel.get(i).documentId === documentId) { - return editors.itemAt(i).getText(); + return editors.itemAt(i).item.getText(); } } return ""; } + function isDocumentOpen(documentId) { + for (var i = 0; i < editorListModel.count; i++) + if (editorListModel.get(i).documentId === documentId) + return true; + return false; + } + function openDocument(document) { loadDocument(document); currentDocumentId = document.documentId; @@ -31,13 +39,16 @@ Item { function doLoadDocument(editor, document) { var data = fileIo.readFile(document.path); - if (document.isContract) - editor.onEditorTextChanged.connect(function() { + editor.onEditorTextChanged.connect(function() { + documentEdit(document.documentId); + if (document.isContract) codeModel.registerCodeChange(editor.getText()); - }); + }); editor.setText(data, document.syntaxMode); } + Component.onCompleted: projectModel.codeEditor = codeEditorView; + Connections { target: projectModel onDocumentOpened: { diff --git a/mix/qml/ProjectModel.qml b/mix/qml/ProjectModel.qml index 10dde5b41..e74be7a9b 100644 --- a/mix/qml/ProjectModel.qml +++ b/mix/qml/ProjectModel.qml @@ -20,6 +20,9 @@ Item { signal projectSaved() signal newProject(var projectData) signal documentSaved(var documentId) + signal deploymentStarted() + signal deploymentComplete() + signal deploymentError(string error) property bool isEmpty: (projectPath === "") readonly property string projectFileName: ".mix" @@ -28,8 +31,10 @@ Item { property string projectPath: "" property string projectTitle: "" property string currentDocumentId: "" + property string deploymentAddress: "" property var listModel: projectListModel property var stateListModel: projectStateListModel.model + property CodeEditorView codeEditor: null //interface function saveAll() { ProjectModelCode.saveAll(); } @@ -48,7 +53,8 @@ Item { function removeDocument(documentId) { ProjectModelCode.removeDocument(documentId); } function getDocument(documentId) { return ProjectModelCode.getDocument(documentId); } function getDocumentIndex(documentId) { return ProjectModelCode.getDocumentIndex(documentId); } - function doAddExistingFiles(paths) { ProjectModelCode.doAddExistingFiles(paths); } + function addExistingFiles(paths) { ProjectModelCode.doAddExistingFiles(paths); } + function deployProject() { ProjectModelCode.deployProject(false); } Connections { target: appContext @@ -83,6 +89,17 @@ Item { } } + MessageDialog { + id: deployWarningDialog + title: qsTr("Project") + text: qsTr("This project has been already deployed to the network. Do you want to re-deploy it?") + standardButtons: StandardButton.Ok | StandardButton.Cancel + icon: StandardIcon.Question + onAccepted: { + ProjectModelCode.deployProject(true); + } + } + ListModel { id: projectListModel } diff --git a/mix/qml/StatusPane.qml b/mix/qml/StatusPane.qml index 956d3f2ec..ddadb9953 100644 --- a/mix/qml/StatusPane.qml +++ b/mix/qml/StatusPane.qml @@ -38,11 +38,17 @@ Rectangle { Connections { target:clientModel - onRunStarted: infoMessage(qsTr("Running transactions..")); + onRunStarted: infoMessage(qsTr("Running transactions...")); onRunFailed: infoMessage(qsTr("Error running transactions")); onRunComplete: infoMessage(qsTr("Run complete")); onNewBlock: infoMessage(qsTr("New block created")); } + Connections { + target:projectModel + onDeploymentStarted: infoMessage(qsTr("Running deployment...")); + onDeploymentError: infoMessage(error); + onDeploymentComplete: infoMessage(qsTr("Deployment complete")); + } color: "transparent" anchors.fill: parent diff --git a/mix/qml/WebPreview.qml b/mix/qml/WebPreview.qml index f258c2e21..08f25b3df 100644 --- a/mix/qml/WebPreview.qml +++ b/mix/qml/WebPreview.qml @@ -43,8 +43,8 @@ Item { } function changePage() { - if (pageCombo.currentIndex >=0 && pageCombo.currentIndex < pageListModel.count) { - setPreviewUrl(pageListModel.get(pageCombo.currentIndex).path); + if (pageCombo.currentIndex >= 0 && pageCombo.currentIndex < pageListModel.count) { + setPreviewUrl(httpServer.url + "/" + pageListModel.get(pageCombo.currentIndex).documentId); } else { setPreviewUrl(""); } @@ -54,7 +54,7 @@ Item { onAppLoaded: { //We need to load the container using file scheme so that web security would allow loading local files in iframe var containerPage = fileIo.readFile("qrc:///qml/html/WebContainer.html"); - webView.loadHtml(containerPage, "file:///WebContainer.html") + webView.loadHtml(containerPage, httpServer.url + "/WebContainer.html") } } @@ -112,16 +112,35 @@ Item { accept: true port: 8893 onClientConnected: { - //filter polling spam - //TODO: do it properly - //var log = _request.content.indexOf("eth_changed") < 0; - var log = true; - if (log) - console.log(_request.content); - var response = clientModel.apiCall(_request.content); - if (log) - console.log(response); - _request.setResponse(response); + var urlPath = _request.url.toString(); + if (urlPath.indexOf("/rpc/") === 0) + { + //jsonrpc request + //filter polling requests //TODO: do it properly + var log = _request.content.indexOf("eth_changed") < 0; + if (log) + console.log(_request.content); + var response = clientModel.apiCall(_request.content); + if (log) + console.log(response); + _request.setResponse(response); + } + else + { + //document request + var documentId = urlPath.substr(urlPath.lastIndexOf("/") + 1); + var content = ""; + if (projectModel.codeEditor.isDocumentOpen(documentId)) + content = projectModel.codeEditor.getDocumentText(documentId); + else + content = fileIo.readFile(projectModel.getDocument(documentId).path); + if (documentId === pageListModel.get(pageCombo.currentIndex).documentId) { + //root page, inject deployment script + content = "\n" + content; + _request.setResponseContentType("text/html"); + } + _request.setResponse(content); + } } } @@ -163,7 +182,7 @@ Item { onLoadingChanged: { if (!loading) { initialized = true; - webView.runJavaScript("init(\"" + httpServer.url + "\")"); + webView.runJavaScript("init(\"" + httpServer.url + "/rpc/\")"); if (pendingPageUrl) setPreviewUrl(pendingPageUrl); } diff --git a/mix/qml/html/WebContainer.html b/mix/qml/html/WebContainer.html index 04ba8ab73..e48668041 100644 --- a/mix/qml/html/WebContainer.html +++ b/mix/qml/html/WebContainer.html @@ -21,13 +21,18 @@ updateContract = function(address, contractFace) { window.contractAddress = address; window.contractInterface = contractFace; window.contract = window.web3.eth.contract(address, contractFace); + window.deploy = { + contractAddress: address, + contractInterface: contractFace, + contract: window.contract + }; } }; init = function(url) { web3 = require('web3'); - web3.setProvider(new web3.providers.HttpSyncProvider(url)); window.web3 = web3; + web3.setProvider(new web3.providers.HttpSyncProvider(url)); }; diff --git a/mix/qml/js/ProjectModel.js b/mix/qml/js/ProjectModel.js index 7c7a8ae4c..fb6872bd4 100644 --- a/mix/qml/js/ProjectModel.js +++ b/mix/qml/js/ProjectModel.js @@ -39,7 +39,11 @@ function closeProject() { function saveProject() { if (!isEmpty) { - var projectData = { files: [] }; + var projectData = { + files: [], + title: projectTitle, + deploymentAddress: deploymentAddress + }; for (var i = 0; i < projectListModel.count; i++) projectData.files.push(projectListModel.get(i).fileName) projectSaving(projectData); @@ -60,6 +64,7 @@ function loadProject(path) { var parts = path.split("/"); projectData.title = parts[parts.length - 2]; } + deploymentAddress = projectData.deploymentAddress ? projectData.deploymentAddress : ""; projectTitle = projectData.title; projectPath = path; if (!projectData.files) @@ -246,3 +251,96 @@ function generateFileName(name, extension) { return fileName } + +var jsonRpcRequestId = 1; +function deployProject(force) { + + saveAll(); //TODO: ask user + + if (!force && deploymentAddress !== "") { + deployWarningDialog.visible = true; + return; + } + + var date = new Date(); + var deploymentId = date.toLocaleString(Qt.locale(), "ddMMyyHHmmsszzz"); + var jsonRpcUrl = "http://localhost:8080"; + console.log("Deploying " + deploymentId + " to " + jsonRpcUrl); + deploymentStarted(); + var code = codeModel.codeHex + var rpcRequest = JSON.stringify({ + jsonrpc: "2.0", + method: "eth_transact", + params: [ { + "code": code + } ], + id: jsonRpcRequestId++ + }); + var httpRequest = new XMLHttpRequest() + httpRequest.open("POST", jsonRpcUrl, true); + httpRequest.setRequestHeader("Content-type", "application/json"); + httpRequest.setRequestHeader("Content-length", rpcRequest.length); + httpRequest.setRequestHeader("Connection", "close"); + httpRequest.onreadystatechange = function() { + if (httpRequest.readyState === XMLHttpRequest.DONE) { + if (httpRequest.status === 200) { + var rpcResponse = JSON.parse(httpRequest.responseText); + var address = rpcResponse.result; + console.log("Created contract, address: " + address); + finalizeDeployment(deploymentId, address); + } else { + var errorText = qsTr("Deployment error: RPC server HTTP status ") + httpRequest.status; + console.log(errorText); + deploymentError(errorText); + } + } + } + httpRequest.send(rpcRequest); +} + +function finalizeDeployment(deploymentId, address) { + //create a dir for frontend files and copy them + var deploymentDir = projectPath + deploymentId + "/"; + fileIo.makeDir(deploymentDir); + for (var i = 0; i < projectListModel.count; i++) { + var doc = projectListModel.get(i); + if (doc.isContract) + continue; + if (doc.isHtml) { + //inject the script to access contract API + //TODO: use a template + var html = fileIo.readFile(doc.path); + var insertAt = html.indexOf("") + if (insertAt < 0) + insertAt = 0; + else + insertAt += 6; + html = html.substr(0, insertAt) + + "" + + "" + + "" + + html.substr(insertAt); + fileIo.writeFile(deploymentDir + doc.fileName, html); + } + else + fileIo.copyFile(doc.path, deploymentDir + doc.fileName); + } + //write deployment js + var deploymentJs = + "// Autogenerated by Mix\n" + + "var web3 = require(\"web3\");\n" + + "var contractInterface = " + codeModel.code.contractInterface + ";\n" + + "deploy = {\n" + + "\tweb3: web3,\n" + + "\tcontractAddress: \"" + address + "\",\n" + + "\tcontractInterface: contractInterface,\n" + + "};\n" + + "deploy.contract = web3.eth.contract(deploy.contractAddress, deploy.contractInterface);\n"; + fileIo.writeFile(deploymentDir + "deployment.js", deploymentJs); + //copy scripts + fileIo.copyFile("qrc:///js/bignumber.min.js", deploymentDir + "bignumber.min.js"); + fileIo.copyFile("qrc:///js/webthree.js", deploymentDir + "ethereum.js"); + deploymentAddress = address; + saveProject(); + deploymentComplete(); +} diff --git a/mix/qml/main.qml b/mix/qml/main.qml index 79430eb59..71d8c24bf 100644 --- a/mix/qml/main.qml +++ b/mix/qml/main.qml @@ -42,6 +42,8 @@ ApplicationWindow { MenuSeparator {} MenuItem { action: editStatesAction } MenuSeparator {} + MenuItem { action: deployViaRpcAction } + MenuSeparator {} MenuItem { action: toggleRunOnLoadAction } } Menu { @@ -265,7 +267,7 @@ ApplicationWindow { selectFolder: false onAccepted: { var paths = addExistingFileDialog.fileUrls; - projectModel.doAddExistingFiles(paths); + projectModel.addExistingFiles(paths); } } @@ -301,4 +303,11 @@ ApplicationWindow { onTriggered: projectModel.openPrevDocument(); } + Action { + id: deployViaRpcAction + text: qsTr("Deploy to Network") + shortcut: "Ctrl+Shift+D" + enabled: !projectModel.isEmpty && codeModel.hasContract + onTriggered: projectModel.deployProject(); + } }