Browse Source

Merge pull request #1028 from arkpar/mix_deploy

Mix: deploy to network
cl-refactor
Gav Wood 10 years ago
parent
commit
4cbac11518
  1. 8
      mix/CodeModel.cpp
  2. 6
      mix/CodeModel.h
  3. 6
      mix/FileIo.cpp
  4. 12
      mix/HttpServer.cpp
  5. 4
      mix/HttpServer.h
  6. 23
      mix/qml/CodeEditorView.qml
  7. 19
      mix/qml/ProjectModel.qml
  8. 8
      mix/qml/StatusPane.qml
  9. 47
      mix/qml/WebPreview.qml
  10. 7
      mix/qml/html/WebContainer.html
  11. 100
      mix/qml/js/ProjectModel.js
  12. 11
      mix/qml/main.qml

8
mix/CodeModel.cpp

@ -28,6 +28,7 @@
#include <libsolidity/SourceReferenceFormatter.h>
#include <libsolidity/InterfaceHandler.h>
#include <libevmcore/Instruction.h>
#include <libethcore/CommonJS.h>
#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),

6
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<QContractDefinition> m_contract;
QString m_compilerMessage; ///< @todo: use some structure here
dev::bytes m_bytes;
QString m_assemblyCode;
QString m_contractInterface;
std::shared_ptr<CodeHighlighter> m_codeHighlighter;

6
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()))

12
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<HttpRequest> 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;
}
}

4
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;
};

23
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: {

19
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
}

8
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

47
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 = "<script>deploy=parent.deploy</script>\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);
}

7
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));
};
</script>

100
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("<head>")
if (insertAt < 0)
insertAt = 0;
else
insertAt += 6;
html = html.substr(0, insertAt) +
"<script src=\"bignumber.min.js\"></script>" +
"<script src=\"ethereum.js\"></script>" +
"<script src=\"deployment.js\"></script>" +
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();
}

11
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();
}
}

Loading…
Cancel
Save