system files

pull/1/head
yann300 8 years ago
parent d3537b3196
commit 2aed87988b
  1. 66
      assets/css/browser-solidity.css
  2. 14
      index.html
  3. 337
      src/app.js
  4. 14
      src/app/editor.js
  5. 94
      src/app/file-explorer.js
  6. 97
      src/app/file-panel.js
  7. 70
      src/app/files/browser-files.js
  8. 2
      src/app/files/storage.js
  9. 201
      src/app/files/system-files.js
  10. 52
      src/app/modaldialog.js
  11. 113
      src/lib/remixd.js
  12. 8
      src/universal-dapp.js
  13. 4
      test-browser/tests/ballot.js
  14. 4
      test-browser/tests/compiling.js
  15. 22
      test-browser/tests/fileExplorer.js
  16. 4
      test-browser/tests/simpleContract.js
  17. 8
      test-browser/tests/staticanalysis.js

@ -461,3 +461,69 @@ input[type="file"] {
.ace_gutter-cell.ace_breakpoint{
background-color: #F77E79;
}
/* The Modal (background) */
.modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 6; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
overflow: auto; /* Enable scroll if needed */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
/* Modal Header */
.modal-header {
padding: 2px 16px;
background-color: orange;
color: white;
}
/* Modal Body */
.modal-body {padding: 2px 16px;}
/* Modal Footer */
.modal-footer {
padding: 2px 16px;
background-color: orange;
color: white;
}
/* Modal Content */
.modal-content {
position: relative;
background-color: #fefefe;
margin: auto;
padding: 0;
border: 1px solid #888;
width: 50%;
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);
-webkit-animation-name: animatetop;
-webkit-animation-duration: 0.4s;
animation-name: animatetop;
animation-duration: 0.4s
}
#modal-footer-cancel {
margin-left: 1em;
cursor: pointer;
}
#modal-footer-ok {
cursor: pointer;
}
/* Add Animation */
@-webkit-keyframes animatetop {
from {top: -300px; opacity: 0}
to {top: 0; opacity: 1}
}
@keyframes animatetop {
from {top: -300px; opacity: 0}
to {top: 0; opacity: 1}
}

@ -53,6 +53,18 @@
</div>
<script src="build/app.js"></script>
<div id="modaldialog" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Localhost connection</h2>
</div>
<div class="modal-body">
</div>
<div class="modal-footer">
<span id="modal-footer-ok">OK</span><span id="modal-footer-cancel">Cancel</span>
</div>
</div>
</div>
</body>
</html>

@ -12,8 +12,10 @@ var queryParams = new QueryParams()
var GistHandler = require('./app/gist-handler')
var gistHandler = new GistHandler()
var Storage = require('./app/storage')
var Files = require('./app/files')
var Remixd = require('./lib/remixd')
var Storage = require('./app/files/storage')
var Browserfiles = require('./app/files/browser-files')
var Systemfiles = require('./app/files/system-files')
var Config = require('./app/config')
var Editor = require('./app/editor')
var Renderer = require('./app/renderer')
@ -42,22 +44,31 @@ var run = function () {
var self = this
this.event = new EventManager()
var fileStorage = new Storage('sol:')
var files = new Files(fileStorage)
var config = new Config(fileStorage)
var remixd = new Remixd()
var filesProviders = {}
filesProviders['browser'] = new Browserfiles(fileStorage)
filesProviders['localhost'] = new Systemfiles(remixd)
// return all the files, except the temporary/readonly ones
function packageFiles () {
var tabbedFiles = {} // list of files displayed in the tabs bar
// return all the files, except the temporary/readonly ones.. package only files from the browser storage.
function packageFiles (cb) {
var ret = {}
Object.keys(files.list())
.filter(function (path) { if (!files.isReadOnly(path)) { return path } })
.map(function (path) { ret[path] = { content: files.get(path) } })
return ret
var files = filesProviders['browser']
var filtered = Object.keys(files.list()).filter(function (path) { if (!files.isReadOnly(path)) { return path } })
async.eachSeries(filtered, function (path, cb) {
ret[path] = { content: files.get(path) }
cb()
}, () => {
cb(ret)
})
}
function createNonClashingName (path) {
var counter = ''
if (path.endsWith('.sol')) path = path.substring(0, path.lastIndexOf('.sol'))
while (files.exists(path + counter + '.sol')) {
while (filesProviders['browser'].exists(path + counter + '.sol')) {
counter = (counter | 0) + 1
}
return path + counter + '.sol'
@ -66,7 +77,7 @@ var run = function () {
// Add files received from remote instance (i.e. another browser-solidity)
function loadFiles (filesSet) {
for (var f in filesSet) {
files.set(createNonClashingName(f), filesSet[f].content)
filesProviders['browser'].set(createNonClashingName(f), filesSet[f].content)
}
switchToNextFile()
}
@ -101,8 +112,8 @@ var run = function () {
})
// insert ballot contract if there are no files available
if (!loadingFromGist && Object.keys(files.list()).length === 0) {
if (!files.set(examples.ballot.name, examples.ballot.content)) {
if (!loadingFromGist && Object.keys(filesProviders['browser'].list()).length === 0) {
if (!filesProviders['browser'].set(examples.ballot.name, examples.ballot.content)) {
alert('Failed to store example contract in browser. Remix will not work properly. Please ensure Remix has access to LocalStorage. Safari in Private mode is known not to work.')
}
}
@ -123,11 +134,16 @@ var run = function () {
console.log('comparing to cloud', key, resp)
if (typeof resp[key] !== 'undefined' && obj[key] !== resp[key] && confirm('Overwrite "' + key + '"? Click Ok to overwrite local file with file from cloud. Cancel will push your local file to the cloud.')) {
console.log('Overwriting', key)
files.set(key, resp[key])
refreshTabs()
filesProviders['browser'].set(key, resp[key])
} else {
console.log('add to obj', obj, key)
obj[key] = files.get(key)
filesProviders['browser'].get(key, (error, content) => {
if (error) {
console.log(error)
} else {
obj[key] = content
}
})
}
done++
if (done >= count) {
@ -138,11 +154,17 @@ var run = function () {
})
}
for (var y in files.list()) {
for (var y in filesProviders['browser'].list()) {
console.log('checking', y)
obj[y] = files.get(y)
count++
check(y)
filesProviders['browser'].get(y, (error, content) => {
if (error) {
console.log(error)
} else {
obj[y] = content
count++
check(y)
}
})
}
}
@ -167,9 +189,18 @@ var run = function () {
event: this.event,
editorFontSize: function (incr) {
editor.editorFontSize(incr)
},
currentFile: function () {
return config.get('currentFile')
},
currentContent: function () {
return editor.get(config.get('currentFile'))
},
setText: function (text) {
editor.setText(text)
}
}
var filePanel = new FilePanel(FilePanelAPI, files)
var filePanel = new FilePanel(FilePanelAPI, filesProviders)
// TODO this should happen inside file-panel.js
filepanelContainer.appendChild(filePanel)
@ -194,45 +225,81 @@ var run = function () {
window['filepanel'].style.width = width + 'px'
window['tabs-bar'].style.left = width + 'px'
})
files.event.register('fileRenamed', function (oldName, newName) {
function fileRenamedEvent (oldName, newName, isFolder) {
// TODO please never use 'window' when it is possible to use a variable
// that references the DOM node
[...window.files.querySelectorAll('.file .name')].forEach(function (span) {
if (span.innerText === oldName) span.innerText = newName
})
})
files.event.register('fileRemoved', function (path) {
if (!isFolder) {
config.set('currentFile', '')
editor.discard(oldName)
if (tabbedFiles[oldName]) {
delete tabbedFiles[oldName]
tabbedFiles[newName] = newName
}
switchToFile(newName)
} else {
var newFocus
for (var k in tabbedFiles) {
if (k.indexOf(oldName + '/') === 0) {
var newAbsolutePath = k.replace(oldName, newName)
tabbedFiles[newAbsolutePath] = newAbsolutePath
delete tabbedFiles[k]
if (config.get('currentFile') === k) {
newFocus = newAbsolutePath
}
}
}
if (newFocus) {
switchToFile(newFocus)
}
}
refreshTabs()
}
filesProviders['browser'].event.register('fileRenamed', fileRenamedEvent)
filesProviders['localhost'].event.register('fileRenamed', fileRenamedEvent)
function fileRemovedEvent (path) {
if (path === config.get('currentFile')) {
config.set('currentFile', '')
switchToNextFile()
}
editor.discard(path)
delete tabbedFiles[path]
refreshTabs()
})
files.event.register('fileAdded', function (path) {
refreshTabs()
})
}
filesProviders['browser'].event.register('fileRemoved', fileRemovedEvent)
filesProviders['localhost'].event.register('fileRemoved', fileRemovedEvent)
// ------------------ gist publish --------------
$('#gist').click(function () {
if (confirm('Are you sure you want to publish all your files anonymously as a public gist on github.com?')) {
var files = packageFiles()
var description = 'Created using browser-solidity: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://ethereum.github.io/browser-solidity/#version=' + queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&gist='
$.ajax({
url: 'https://api.github.com/gists',
type: 'POST',
data: JSON.stringify({
description: description,
public: true,
files: files
})
}).done(function (response) {
if (response.html_url && confirm('Created a gist at ' + response.html_url + ' Would you like to open it in a new window?')) {
window.open(response.html_url, '_blank')
packageFiles((error, packaged) => {
if (error) {
console.log(error)
} else {
var description = 'Created using browser-solidity: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://ethereum.github.io/browser-solidity/#version=' + queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&gist='
$.ajax({
url: 'https://api.github.com/gists',
type: 'POST',
data: JSON.stringify({
description: description,
public: true,
files: packaged
})
}).done(function (response) {
if (response.html_url && confirm('Created a gist at ' + response.html_url + ' Would you like to open it in a new window?')) {
window.open(response.html_url, '_blank')
}
}).fail(function (xhr, text, err) {
alert('Failed to create gist: ' + (err || 'Unknown transport error'))
})
}
}).fail(function (xhr, text, err) {
alert('Failed to create gist: ' + (err || 'Unknown transport error'))
})
}
})
@ -245,12 +312,17 @@ var run = function () {
if (target === null) {
return
}
var files = packageFiles()
$('<iframe/>', {
src: target,
style: 'display:none;',
load: function () { this.contentWindow.postMessage(['loadFiles', files], '*') }
}).appendTo('body')
packageFiles((error, packaged) => {
if (error) {
console.log(error)
} else {
$('<iframe/>', {
src: target,
style: 'display:none;',
load: function () { this.contentWindow.postMessage(['loadFiles', packaged], '*') }
}).appendTo('body')
}
})
})
// ----------------- editor ----------------------
@ -267,97 +339,77 @@ var run = function () {
return false
})
// Edit name of current tab
$filesEl.on('click', '.file.active', function (ev) {
var $fileTabEl = $(this)
var originalName = $fileTabEl.find('.name').text()
ev.preventDefault()
if ($(this).find('input').length > 0) return false
var $fileNameInputEl = $('<input value="' + originalName + '"/>')
$fileTabEl.html($fileNameInputEl)
$fileNameInputEl.focus()
$fileNameInputEl.select()
$fileNameInputEl.on('blur', handleRename)
$fileNameInputEl.keyup(handleRename)
function handleRename (ev) {
ev.preventDefault()
if (ev.which && ev.which !== 13) return false
var newName = ev.target.value
$fileNameInputEl.off('blur')
$fileNameInputEl.off('keyup')
if (newName !== originalName && confirm(
files.exists(newName)
? 'Are you sure you want to overwrite: ' + newName + ' with ' + originalName + '?'
: 'Are you sure you want to rename: ' + originalName + ' to ' + newName + '?')) {
if (!files.rename(originalName, newName)) {
alert('Error while renaming file')
} else {
config.set('currentFile', '')
switchToFile(newName)
editor.discard(originalName)
}
}
return false
}
return false
})
// Remove current tab
$filesEl.on('click', '.file .remove', function (ev) {
ev.preventDefault()
var name = $(this).parent().find('.name').text()
if (confirm('Are you sure you want to remove: ' + name + ' from local storage?')) {
if (!files.remove(name)) {
alert('Error while removing file')
}
delete tabbedFiles[name]
refreshTabs()
if (Object.keys(tabbedFiles).length) {
switchToFile(Object.keys(tabbedFiles)[0])
} else {
editor.displayEmptyReadOnlySession()
}
return false
})
editor.event.register('sessionSwitched', refreshTabs)
function switchToFile (file) {
editorSyncFile()
config.set('currentFile', file)
if (files.isReadOnly(file)) {
editor.openReadOnly(file, files.get(file))
} else {
editor.open(file, files.get(file))
}
self.event.trigger('currentFileChanged', [file])
refreshTabs(file)
fileProviderOf(file).get(file, (error, content) => {
if (error) {
console.log(error)
} else {
if (fileProviderOf(file).isReadOnly(file)) {
editor.openReadOnly(file, content)
} else {
editor.open(file, content)
}
self.event.trigger('currentFileChanged', [file, fileProviderOf(file)])
}
})
}
function switchToNextFile () {
var fileList = Object.keys(files.list())
var fileList = Object.keys(filesProviders['browser'].list())
if (fileList.length) {
switchToFile(fileList[0])
}
}
var previouslyOpenedFile = config.get('currentFile')
if (previouslyOpenedFile && files.get(previouslyOpenedFile)) {
switchToFile(previouslyOpenedFile)
if (previouslyOpenedFile) {
filesProviders['browser'].get(previouslyOpenedFile, (error, content) => {
if (!error && content) {
switchToFile(previouslyOpenedFile)
} else {
switchToNextFile()
}
})
} else {
switchToNextFile()
}
// Synchronise tab list with file names known to the editor
function refreshTabs () {
var $filesEl = $('#files')
var fileNames = Object.keys(files.list())
function fileProviderOf (file) {
var provider = file.match(/[^/]*/)
if (provider !== null) {
return filesProviders[provider[0]]
}
return null
}
// Display files that have already been selected
function refreshTabs (newfile) {
if (newfile) {
tabbedFiles[newfile] = newfile
}
var $filesEl = $('#files')
$filesEl.find('.file').remove()
for (var f in fileNames) {
var name = fileNames[f]
$filesEl.append($('<li class="file"><span class="name">' + name + '</span><span class="remove"><i class="fa fa-close"></i></span></li>'))
for (var file in tabbedFiles) {
$filesEl.append($('<li class="file"><span class="name">' + file + '</span><span class="remove"><i class="fa fa-close"></i></span></li>'))
}
var currentFileOpen = !!config.get('currentFile')
@ -447,7 +499,7 @@ var run = function () {
}
},
errorClick: (errFile, errLine, errCol) => {
if (errFile !== config.get('currentFile') && files.exists(errFile)) {
if (errFile !== config.get('currentFile') && (filesProviders['browser'].exists(errFile) || filesProviders['localhost'].exists(errFile))) {
switchToFile(errFile)
}
editor.gotoLine(errLine, errCol)
@ -524,7 +576,7 @@ var run = function () {
return cb('No metadata')
}
Object.keys(metadata.sources).forEach(function (fileName) {
async.eachSeries(Object.keys(metadata.sources), function (fileName, cb) {
// find hash
var hash
try {
@ -533,16 +585,23 @@ var run = function () {
return cb('Metadata inconsistency')
}
sources.push({
content: files.get(fileName),
hash: hash
fileProviderOf(fileName).get(fileName, (error, content) => {
if (error) {
console.log(error)
} else {
sources.push({
content: content,
hash: hash
})
}
cb()
})
}, function () {
// publish the list of sources in order, fail if any failed
async.eachSeries(sources, function (item, cb) {
swarmVerifiedPublish(item.content, item.hash, cb)
}, cb)
})
// publish the list of sources in order, fail if any failed
async.eachSeries(sources, function (item, cb) {
swarmVerifiedPublish(item.content, item.hash, cb)
}, cb)
}
udapp.event.register('publishContract', this, function (contract) {
@ -635,9 +694,9 @@ var run = function () {
}
function handleImportCall (url, cb) {
if (files.exists(url)) {
cb(null, files.get(url))
return
var provider = fileProviderOf(url)
if (provider && provider.exists(url)) {
return provider.get(url, cb)
}
var handlers = [
@ -664,7 +723,7 @@ var run = function () {
}
// FIXME: at some point we should invalidate the cache
files.addReadOnly(url, content)
filesProviders['browser'].addReadOnly(url, content)
cb(null, content)
})
}
@ -754,8 +813,19 @@ var run = function () {
if (currentFile) {
var target = currentFile
var sources = {}
sources[target] = files.get(target)
compiler.compile(sources, target)
var provider = fileProviderOf(currentFile)
if (provider) {
provider.get(target, (error, content) => {
if (error) {
console.log(error)
} else {
sources[target] = content
compiler.compile(sources, target)
}
})
} else {
console.log('cannot compile ' + currentFile + '. Does not belong to any explorer')
}
}
}
@ -763,7 +833,12 @@ var run = function () {
var currentFile = config.get('currentFile')
if (currentFile && editor.current()) {
var input = editor.get(currentFile)
files.set(currentFile, input)
var provider = fileProviderOf(currentFile)
if (provider) {
provider.set(currentFile, input)
} else {
console.log('cannot save ' + currentFile + '. Does not belong to any explorer')
}
}
}

@ -59,6 +59,12 @@ function Editor (editorElement) {
e.stop()
})
this.displayEmptyReadOnlySession = function () {
currentSession = null
editor.setSession(emptySession)
editor.setReadOnly(true)
}
this.setBreakpoint = function (row, css) {
editor.session.setBreakpoint(row, css)
}
@ -67,6 +73,12 @@ function Editor (editorElement) {
editor.setFontSize(editor.getFontSize() + incr)
}
this.setText = function (text) {
if (currentSession && sessions[currentSession]) {
sessions[currentSession].setValue(text)
}
}
function createSession (content) {
var s = new ace.EditSession(content, 'ace/mode/javascript')
s.setUndoManager(new ace.UndoManager())
@ -87,6 +99,8 @@ function Editor (editorElement) {
var session = createSession(content)
sessions[path] = session
readOnlySessions[path] = false
} else if (sessions[path].getValue() !== content) {
sessions[path].setValue(content)
}
switchSession(path)
}

@ -33,6 +33,17 @@ var css = csjs`
module.exports = fileExplorer
function fileExplorer (appAPI, files) {
this.files = files
this.files.event.register('fileExternallyChanged', (path, content) => {
if (appAPI.currentFile() === path && appAPI.currentContent() !== content) {
if (confirm('This file has been changed outside of the remix.\nDo you want to replace the content displayed in remix by the new one?')) {
appAPI.setText(content)
}
}
})
var self = this
var fileEvents = files.event
var treeView = new Treeview({
extractData: function (value, tree, key) {
@ -55,8 +66,11 @@ function fileExplorer (appAPI, files) {
}
},
formatSelf: function (key, data) {
return yo`<label class=${data.children ? css.folder : css.file}
var isRoot = data.path.indexOf('/') === -1
return yo`<label class="${data.children ? css.folder : css.file}"
data-path="${data.path}"
style="${isRoot ? 'font-weight:bold;' : ''}"
isfolder=${data.children !== undefined}
onload=${function (el) { adaptEnvironment(el, focus, hover) }}
onunload=${function (el) { unadaptEnvironment(el, focus, hover) }}
onclick=${editModeOn}
@ -66,30 +80,35 @@ function fileExplorer (appAPI, files) {
}
})
this.treeView = treeView
var deleteButton = yo`
<span class=${css.remove} onclick=${deletePath}>
<i class="fa fa-trash" aria-hidden="true"></i>
</span>
`
appAPI.event.register('currentFileChanged', (newFile) => {
fileFocus(newFile)
appAPI.event.register('currentFileChanged', (newFile, explorer) => {
if (explorer === files) {
fileFocus(newFile)
} else {
unfocus(focusElement)
}
})
fileEvents.register('fileRemoved', fileRemoved)
fileEvents.register('fileRenamed', fileRenamed)
fileEvents.register('fileRenamedError', fileRenamedError)
fileEvents.register('fileAdded', fileAdded)
var filepath = null
var focusElement = null
var textUnderEdit = null
var element = treeView.render(files.listAsTree())
element.className = css.fileexplorer
var events = new EventManager()
this.events = events
var api = {}
api.addFile = function addFile (file) {
var name = file.name
var name = files.type + '/' + file.name
if (!files.exists(name) || confirm('The file ' + name + ' already exists! Would you like to overwrite it?')) {
var fileReader = new FileReader()
fileReader.onload = function (event) {
@ -100,12 +119,13 @@ function fileExplorer (appAPI, files) {
fileReader.readAsText(file)
}
}
this.api = api
function focus (event) {
event.cancelBubble = true
var li = this
if (focusElement === li) return
if (focusElement) focusElement.classList.toggle(css.hasFocus)
unfocus(focusElement)
focusElement = li
focusElement.classList.toggle(css.hasFocus)
var label = getLabelFrom(li)
@ -114,6 +134,11 @@ function fileExplorer (appAPI, files) {
if (isFile) events.trigger('focus', [filepath])
}
function unfocus (el) {
if (focusElement) focusElement.classList.toggle(css.hasFocus)
focusElement = null
}
function hover (event) {
if (event.type === 'mouseout') {
var exitedTo = event.toElement || event.relatedTarget
@ -128,7 +153,7 @@ function fileExplorer (appAPI, files) {
}
function getElement (path) {
var label = element.querySelector(`label[data-path="${path}"]`)
var label = self.element.querySelector(`label[data-path="${path}"]`)
if (label) return getLiFrom(label)
}
@ -160,12 +185,26 @@ function fileExplorer (appAPI, files) {
function editModeOff (event) {
var label = this
if (event.type === 'blur' || event.which === 27 || event.which === 13) {
if (event.which === 13) event.preventDefault()
if ((event.type === 'blur' || event.which === 27 || event.which === 13) && label.getAttribute('contenteditable')) {
var isFolder = label.getAttribute('isfolder') === 'true'
var save = textUnderEdit !== label.innerText
if (event.which === 13) event.preventDefault()
if (save && event.which !== 13) save = confirm('Do you want to rename?')
if (save) renameSubtree(label)
else label.innerText = textUnderEdit
if (save) {
var newPath = label.dataset.path
newPath = newPath.split('/')
newPath[newPath.length - 1] = label.innerText
newPath = newPath.join('/')
if (label.innerText.match(/(\/|:|\*|\?|"|<|>|\\|\||')/) !== null) {
alert('special characters are not allowsed')
label.innerText = textUnderEdit
} else if (!files.exists(newPath)) {
files.rename(label.dataset.path, newPath, isFolder)
} else {
alert('File already exists.')
label.innerText = textUnderEdit
}
} else label.innerText = textUnderEdit
label.removeAttribute('contenteditable')
label.classList.remove(css.rename)
}
@ -205,8 +244,6 @@ function fileExplorer (appAPI, files) {
var path = label.dataset.path
var newName = path.replace(oldPath, newPath)
label.dataset.path = newName
var isFile = label.className.indexOf('file') === 0
if (isFile) files.rename(path, newName)
var ul = li.lastChild
if (ul.tagName === 'UL') {
updateAllLabels([...ul.children], oldPath, newPath)
@ -227,7 +264,7 @@ function fileExplorer (appAPI, files) {
if (li) li.parentElement.removeChild(li)
}
function fileRenamed (oldName, newName) {
function fileRenamed (oldName, newName, isFolder) {
var li = getElement(oldName)
if (li) {
oldName = oldName.split('/')
@ -244,16 +281,16 @@ function fileExplorer (appAPI, files) {
}
}
function fileRenamedError (error) {
alert(error)
}
function fileAdded (filepath) {
var el = treeView.render(files.listAsTree())
el.className = css.fileexplorer
element.parentElement.replaceChild(el, element)
element = el
self.element.parentElement.replaceChild(el, self.element)
self.element = el
}
element.events = events
element.api = api
return element
}
/******************************************************************************
HELPER FUNCTIONS
@ -312,3 +349,16 @@ function expandPathTo (li) {
if (caret.classList.contains('fa-caret-right')) caret.click() // expand
}
}
fileExplorer.prototype.init = function () {
var files = this.files.listAsTree()
if (!Object.keys(files).length) {
files[this.files.type] = {} // default
}
var element = this.treeView.render(files)
element.className = css.fileexplorer
element.events = this.events
element.api = this.api
this.element = element
return element
}

@ -4,6 +4,7 @@ var yo = require('yo-yo')
var EventManager = require('ethereum-remix').lib.EventManager
var FileExplorer = require('./file-explorer')
var modalDialog = require('./modaldialog')
module.exports = filepanel
@ -28,6 +29,9 @@ var css = csjs`
.newFile {
padding : 10px;
}
.connectToLocalhost {
padding : 10px;
}
.uploadFile {
padding : 10px;
}
@ -46,11 +50,10 @@ var css = csjs`
}
.isHidden {
position : absolute;
height : 99%
height : 99%;
left : -101%;
}
.treeview {
height : 100%;
background-color : white;
}
.dragbar {
@ -83,25 +86,39 @@ var limit = 60
var canUpload = window.File || window.FileReader || window.FileList || window.Blob
var ghostbar = yo`<div class=${css.ghostbar}></div>`
function filepanel (appAPI, files) {
var fileExplorer = new FileExplorer(appAPI, files)
function filepanel (appAPI, filesProvider) {
var fileExplorer = new FileExplorer(appAPI, filesProvider['browser'])
var fileSystemExplorer = new FileExplorer(appAPI, filesProvider['localhost'])
var dragbar = yo`<div onmousedown=${mousedown} class=${css.dragbar}></div>`
function remixdDialog () {
return yo`<div><div>This feature allows to interact with your file system from Remix. Once the connection is made the shared folder will be available in the file explorer under <i>localhost</i></div>
<div><i>Remixd</i> first has to be run in your local computer. See <a href="http://remix.readthedocs.io/en/latest/tutorial_mist.html">http://remix.readthedocs.io/en/latest/remixd.html</a> for more details.</div>
<div>Accepting this dialog will start a session between <i>${window.location.href}</i> and your local file system <i>ws://127.0.0.1:65520</i></div>
<div>Please be sure your system is secured enough (port 65520 neither opened nor forwarded).</div>
<div><i class="fa fa-link"></i> will update the connection status.</div>
<div>This feature is still alpha, we recommend to keep a copy of the shared folder.</div>
</div>`
}
function template () {
return yo`
<div class=${css.container}>
<div class=${css.fileexplorer}>
<div class=${css.menu}>
<span onclick=${createNewFile} class="newFile ${css.newFile}" title="New File">
<span onclick=${createNewFile} class="newFile ${css.newFile}" title="Create New File in the Browser Storage Explorer">
<i class="fa fa-plus-circle"></i>
</span>
</span>
${canUpload ? yo`
<span class=${css.uploadFile} title="Open local file">
<span class=${css.uploadFile} title="Add Local file to the Browser Storage Explorer">
<label class="fa fa-folder-open">
<input type="file" onchange=${uploadFile} multiple />
</label>
</span>
` : ''}
<span onclick=${connectToLocalhost} class="${css.connectToLocalhost}">
<i class="websocketconn fa fa-link" title="Connect to Localhost"></i>
</span>
<span class=${css.changeeditorfontsize} >
<i class="increditorsize fa fa-plus" aria-hidden="true" title="increase editor font size"></i>
<i class="decreditorsize fa fa-minus" aria-hidden="true" title="decrease editor font size"></i>
@ -110,7 +127,8 @@ function filepanel (appAPI, files) {
<i class="fa fa-angle-double-left"></i>
</span>
</div>
<div class=${css.treeview}>${fileExplorer}</div>
<div class=${css.treeview}>${fileExplorer.init()}</div>
<div class="filesystemexplorer ${css.treeview}"></div>
</div>
${dragbar}
</div>
@ -121,6 +139,33 @@ function filepanel (appAPI, files) {
var element = template()
element.querySelector('.increditorsize').addEventListener('click', () => { appAPI.editorFontSize(1) })
element.querySelector('.decreditorsize').addEventListener('click', () => { appAPI.editorFontSize(-1) })
var containerFileSystem = element.querySelector('.filesystemexplorer')
var websocketconn = element.querySelector('.websocketconn')
filesProvider['localhost'].remixd.event.register('connecting', (event) => {
websocketconn.style.color = 'orange'
websocketconn.setAttribute('title', 'Connecting to localhost. ' + JSON.stringify(event))
})
filesProvider['localhost'].remixd.event.register('connected', (event) => {
websocketconn.style.color = 'green'
websocketconn.setAttribute('title', 'Connected to localhost. ' + JSON.stringify(event))
})
filesProvider['localhost'].remixd.event.register('errored', (event) => {
websocketconn.style.color = 'red'
websocketconn.setAttribute('title', 'localhost connection errored. ' + JSON.stringify(event))
if (fileSystemExplorer.element && containerFileSystem.children.length > 0) {
containerFileSystem.removeChild(fileSystemExplorer.element)
}
})
filesProvider['localhost'].remixd.event.register('closed', (event) => {
websocketconn.style.color = '#111111'
websocketconn.setAttribute('title', 'localhost connection closed. ' + JSON.stringify(event))
if (fileSystemExplorer.element && containerFileSystem.children.length > 0) {
containerFileSystem.removeChild(fileSystemExplorer.element)
}
})
// TODO please do not add custom javascript objects, which have no
// relation to the DOM to DOM nodes
element.events = events
@ -128,6 +173,10 @@ function filepanel (appAPI, files) {
appAPI.switchToFile(path)
})
fileSystemExplorer.events.register('focus', function (path) {
appAPI.switchToFile(path)
})
return element
function toggle (event) {
@ -182,11 +231,39 @@ function filepanel (appAPI, files) {
}
function createNewFile () {
var newName = appAPI.createName('Untitled')
if (!files.set(newName, '')) {
var newName = filesProvider['browser'].type + '/' + appAPI.createName('Untitled.sol')
if (!filesProvider['browser'].set(newName, '')) {
alert('Failed to create file ' + newName)
} else {
appAPI.switchToFile(newName)
}
}
/**
* connect to localhost if no connection and render the explorer
* disconnect from localhost if connected and remove the explorer
*
* @param {String} txHash - hash of the transaction
*/
function connectToLocalhost () {
var container = document.querySelector('.filesystemexplorer')
if (filesProvider['localhost'].files !== null) {
filesProvider['localhost'].close((error) => {
if (error) console.log(error)
})
} else {
modalDialog('Connection to Localhost', remixdDialog(), () => {
filesProvider['localhost'].init((error) => {
if (error) {
console.log(error)
} else {
if (fileSystemExplorer.element && container.children.length > 0) {
container.removeChild(fileSystemExplorer.element)
}
container.appendChild(fileSystemExplorer.init())
}
})
})
}
}
}

@ -6,40 +6,52 @@ function Files (storage) {
var event = new EventManager()
this.event = event
var readonly = {}
this.type = 'browser'
this.exists = function (path) {
var unprefixedpath = this.removePrefix(path)
// NOTE: ignore the config file
if (path === '.remix.config') {
return false
}
return this.isReadOnly(path) || storage.exists(path)
return this.isReadOnly(unprefixedpath) || storage.exists(unprefixedpath)
}
this.get = function (path) {
this.init = function (cb) {
cb()
}
this.get = function (path, cb) {
var unprefixedpath = this.removePrefix(path)
// NOTE: ignore the config file
if (path === '.remix.config') {
return null
}
return readonly[path] || storage.get(path)
var content = readonly[unprefixedpath] || storage.get(unprefixedpath)
if (cb) {
cb(null, content)
}
return content
}
this.set = function (path, content) {
var unprefixedpath = this.removePrefix(path)
// NOTE: ignore the config file
if (path === '.remix.config') {
return false
}
if (!this.isReadOnly(path)) {
var exists = storage.exists(path)
if (!storage.set(path, content)) {
if (!this.isReadOnly(unprefixedpath)) {
var exists = storage.exists(unprefixedpath)
if (!storage.set(unprefixedpath, content)) {
return false
}
if (!exists) {
event.trigger('fileAdded', [path, false])
event.trigger('fileAdded', [this.type + '/' + unprefixedpath, false])
} else {
event.trigger('fileChanged', [path])
event.trigger('fileChanged', [this.type + '/' + unprefixedpath])
}
return true
}
@ -48,9 +60,10 @@ function Files (storage) {
}
this.addReadOnly = function (path, content) {
if (!storage.exists(path)) {
readonly[path] = content
event.trigger('fileAdded', [path, true])
var unprefixedpath = this.removePrefix(path)
if (!storage.exists(unprefixedpath)) {
readonly[unprefixedpath] = content
event.trigger('fileAdded', [this.type + '/' + unprefixedpath, true])
return true
}
@ -58,31 +71,35 @@ function Files (storage) {
}
this.isReadOnly = function (path) {
path = this.removePrefix(path)
return readonly[path] !== undefined
}
this.remove = function (path) {
if (!this.exists(path)) {
var unprefixedpath = this.removePrefix(path)
if (!this.exists(unprefixedpath)) {
return false
}
if (this.isReadOnly(path)) {
readonly[path] = undefined
if (this.isReadOnly(unprefixedpath)) {
readonly[unprefixedpath] = undefined
} else {
if (!storage.remove(path)) {
if (!storage.remove(unprefixedpath)) {
return false
}
}
event.trigger('fileRemoved', [path])
event.trigger('fileRemoved', [this.type + '/' + unprefixedpath])
return true
}
this.rename = function (oldPath, newPath) {
if (!this.isReadOnly(oldPath) && storage.exists(oldPath)) {
if (!storage.rename(oldPath, newPath)) {
this.rename = function (oldPath, newPath, isFolder) {
var unprefixedoldPath = this.removePrefix(oldPath)
var unprefixednewPath = this.removePrefix(newPath)
if (!this.isReadOnly(unprefixedoldPath) && storage.exists(unprefixedoldPath)) {
if (!storage.rename(unprefixedoldPath, unprefixednewPath)) {
return false
}
event.trigger('fileRenamed', [oldPath, newPath])
event.trigger('fileRenamed', [this.type + '/' + unprefixedoldPath, this.type + '/' + unprefixednewPath, isFolder])
return true
}
return false
@ -92,21 +109,25 @@ function Files (storage) {
var files = {}
// add r/w files to the list
storage.keys().forEach(function (path) {
storage.keys().forEach((path) => {
// NOTE: as a temporary measure do not show the config file
if (path !== '.remix.config') {
files[path] = false
files[this.type + '/' + path] = false
}
})
// add r/o files to the list
Object.keys(readonly).forEach(function (path) {
files[path] = true
Object.keys(readonly).forEach((path) => {
files[this.type + '/' + path] = true
})
return files
}
this.removePrefix = function (path) {
return path.indexOf(this.type + '/') === 0 ? path.replace(this.type + '/', '') : path
}
//
// Tree model for files
// {
@ -146,7 +167,6 @@ function Files (storage) {
'/content': self.get(path)
})
})
return tree
}

@ -41,7 +41,7 @@ function Storage (prefix) {
return safeKeys()
// filter any names not including the prefix
.filter(function (item) { return item.indexOf(prefix, 0) === 0 })
// remove prefix from filename
// remove prefix from filename and add the 'browser' path
.map(function (item) { return item.substr(prefix.length) })
}

@ -0,0 +1,201 @@
'use strict'
var async = require('async')
var EventManager = require('ethereum-remix').lib.EventManager
class SystemFiles {
constructor (remixd) {
this.event = new EventManager()
this.remixd = remixd
this.files = null
this.filesContent = {}
this.filesTree = null
this.type = 'localhost'
this.error = {
'EEXIST': 'File already exists'
}
this.remixd.event.register('notified', (data) => {
if (data.scope === 'systemfiles') {
if (data.name === 'created') {
this.init(() => {
this.event.trigger('fileAdded', [this.type + '/' + data.value.path, data.value.isReadOnly, data.value.isFolder])
})
} else if (data.name === 'removed') {
this.init(() => {
this.event.trigger('fileRemoved', [this.type + '/' + data.value.path])
})
} else if (data.name === 'changed') {
this.remixd.call('systemfiles', 'get', {path: data.value}, (error, content) => {
if (error) {
console.log(error)
} else {
var path = this.type + '/' + data.value
this.filesContent[path] = content
this.event.trigger('fileExternallyChanged', [path, content])
}
})
}
}
})
}
close (cb) {
this.remixd.close()
this.files = null
this.filesTree = null
cb()
}
init (cb) {
this.remixd.call('systemfiles', 'list', {}, (error, filesList) => {
if (error) {
cb(error)
} else {
this.files = {}
for (var k in filesList) {
this.files[this.type + '/' + k] = filesList[k]
}
listAsTree(this, this.files, (error, tree) => {
this.filesTree = tree
cb(error)
})
}
})
}
exists (path) {
if (!this.files) return false
return this.files[path] !== undefined
}
get (path, cb) {
var unprefixedpath = this.removePrefix(path)
this.remixd.call('systemfiles', 'get', {path: unprefixedpath}, (error, content) => {
if (!error) {
this.filesContent[path] = content
cb(error, content)
} else {
// display the last known content.
// TODO should perhaps better warn the user that the file is not synced.
cb(null, this.filesContent[path])
}
})
}
set (path, content, cb) {
var unprefixedpath = this.removePrefix(path)
this.remixd.call('systemfiles', 'set', {path: unprefixedpath, content: content}, (error, result) => {
if (cb) cb(error, result)
var path = this.type + '/' + unprefixedpath
this.filesContent[path]
this.event.trigger('fileChanged', [path])
})
return true
}
addReadOnly (path, content) {
return false
}
isReadOnly (path) {
if (this.files) return this.files[path]
return true
}
remove (path) {
var unprefixedpath = this.removePrefix(path)
this.remixd.call('systemfiles', 'remove', {path: unprefixedpath}, (error, result) => {
if (error) console.log(error)
var path = this.type + '/' + unprefixedpath
delete this.filesContent[path]
this.init(() => {
this.event.trigger('fileRemoved', [path])
})
})
}
rename (oldPath, newPath, isFolder) {
var unprefixedoldPath = this.removePrefix(oldPath)
var unprefixednewPath = this.removePrefix(newPath)
this.remixd.call('systemfiles', 'rename', {oldPath: unprefixedoldPath, newPath: unprefixednewPath}, (error, result) => {
if (error) {
console.log(error)
if (this.error[error.code]) error = this.error[error.code]
this.event.trigger('fileRenamedError', [this.error[error.code]])
} else {
var newPath = this.type + '/' + unprefixednewPath
var oldPath = this.type + '/' + unprefixedoldPath
this.filesContent[newPath] = this.filesContent[oldPath]
delete this.filesContent[oldPath]
this.init(() => {
this.event.trigger('fileRenamed', [oldPath, newPath, isFolder])
})
}
})
return true
}
list () {
return this.files
}
listAsTree () {
return this.filesTree
}
removePrefix (path) {
return path.indexOf(this.type + '/') === 0 ? path.replace(this.type + '/', '') : path
}
}
//
// Tree model for files
// {
// 'a': { }, // empty directory 'a'
// 'b': {
// 'c': {}, // empty directory 'b/c'
// 'd': { '/readonly': true, '/content': 'Hello World' } // files 'b/c/d'
// 'e': { '/readonly': false, '/path': 'b/c/d' } // symlink to 'b/c/d'
// 'f': { '/readonly': false, '/content': '<executable>', '/mode': 0755 }
// }
// }
//
function listAsTree (self, filesList, callback) {
function hashmapize (obj, path, val) {
var nodes = path.split('/')
var i = 0
for (; i < nodes.length - 1; i++) {
var node = nodes[i]
if (obj[node] === undefined) {
obj[node] = {}
}
obj = obj[node]
}
obj[nodes[i]] = val
}
var tree = {}
// This does not include '.remix.config', because it is filtered
// inside list().
async.eachSeries(Object.keys(filesList), function (path, cb) {
self.get(path, (error, content) => {
if (error) {
console.log(error)
cb(error)
} else {
self.filesContent[path] = content
hashmapize(tree, path, {
'/readonly': filesList[path],
'/content': content
})
cb()
}
})
}, (error) => {
callback(error, tree)
})
}
module.exports = SystemFiles

@ -0,0 +1,52 @@
module.exports = (title, content, okFn, cancelFn) => {
var modal = document.querySelector('.modal-body')
var modaltitle = document.querySelector('.modal-header h2')
modaltitle.innerHTML = ' - '
if (title) modaltitle.innerHTML = title
modal.innerHTML = ''
if (content) modal.appendChild(content)
var container = document.querySelector('.modal')
container.style.display = container.style.display === 'block' ? hide() : show()
function ok () {
hide()
if (okFn) okFn()
removeEventListener()
}
function cancel () {
hide()
if (cancelFn) cancelFn()
removeEventListener()
}
function blur (event) {
if (event.target === container) {
cancel()
}
}
window.onclick = (event) => {
console.log('clicj windo')
blur(event)
}
function hide () {
container.style.display = 'none'
}
function show () {
container.style.display = 'block'
}
function removeEventListener () {
document.getElementById('modal-footer-ok').removeEventListener('click', ok)
document.getElementById('modal-footer-cancel').removeEventListener('click', cancel)
}
document.getElementById('modal-footer-ok').addEventListener('click', ok)
document.getElementById('modal-footer-cancel').addEventListener('click', cancel)
}

@ -0,0 +1,113 @@
'use strict'
var EventManager = require('ethereum-remix').lib.EventManager
class Remixd {
constructor () {
this.event = new EventManager()
this.callbacks = {}
this.callid = 0
this.socket = null
this.connected = false
}
online () {
return this.socket !== null
}
close () {
if (this.socket) {
this.socket.close()
this.socket = null
}
}
start (cb) {
if (this.socket) {
try {
this.socket.close()
} catch (e) {}
}
this.event.trigger('connecting', [])
this.socket = new WebSocket('ws://localhost:65520', 'echo-protocol') // eslint-disable-line
this.socket.addEventListener('open', (event) => {
this.connected = true
this.event.trigger('connected', [event])
cb()
})
this.socket.addEventListener('message', (event) => {
var data = JSON.parse(event.data)
if (data.type === 'reply') {
if (this.callbacks[data.id]) {
this.callbacks[data.id](data.error, data.result)
delete this.callbacks[data.id]
}
this.event.trigger('replied', [data])
} else if (data.type === 'notification') {
this.event.trigger('notified', [data])
}
})
this.socket.addEventListener('error', (event) => {
this.errored(event)
cb(event)
})
this.socket.addEventListener('close', (event) => {
if (event.wasClean) {
this.connected = false
this.event.trigger('closed', [event])
} else {
this.errored(event)
}
this.socket = null
})
}
errored (event) {
if (this.connected) {
alert('connection to Remixd lost!.') // eslint-disable-line
}
this.connected = false
this.socket = null
this.event.trigger('errored', [event])
}
call (service, fn, args, callback) {
this.ensureSocket((error) => {
if (error) return callback(error)
if (this.socket && this.socket.readyState === this.socket.OPEN) {
var data = this.format(service, fn, args)
this.callbacks[data.id] = callback
this.socket.send(JSON.stringify(data))
} else {
callback('Socket not ready. state:' + this.socket.readyState)
}
})
}
ensureSocket (cb) {
if (this.socket) return cb(null, this.socket)
this.start((error) => {
if (error) {
cb(error)
} else {
cb(null, this.socket)
}
})
}
format (service, fn, args) {
var data = {
id: this.callid,
service: service,
fn: fn,
args: args
}
this.callid++
return data
}
}
module.exports = Remixd

@ -280,13 +280,13 @@ UniversalDApp.prototype.getContractByName = function (contractName) {
}
UniversalDApp.prototype.getCreateInterface = function ($container, contract) {
function remove () {
self.$el.remove()
}
var self = this
var createInterface = yo`<div class="create"></div>`
if (self.options.removable) {
var close = yo`<div class="udapp-close" onclick=${remove}></div>`
function remove () {
self.$el.remove()
}
createInterface.appendChild(close)
}
@ -348,10 +348,10 @@ UniversalDApp.prototype.getInstanceInterface = function (contract, address, $tar
var appendFunctions = function (address, $el) {
if ($el) $el = $el.get(0)
function remove () { $instance.remove() }
var $instance = $(`<div class="instance ${cssInstance.instance}"/>`)
if (self.options.removable_instances) {
var close = yo`<div class="udapp-close" onclick=${remove}></div>`
function remove () { $instance.remove() }
$instance.get(0).appendChild(close)
}
var context = self.executionContext.isVM() ? 'memory' : 'blockchain'

@ -6,7 +6,7 @@ var sauce = require('./sauce')
var sources = {
'sources': {
'Untitled.sol': examples.ballot.content
'browser/Untitled.sol': examples.ballot.content
}
}
@ -27,7 +27,7 @@ function runTests (browser, testData) {
browser
.waitForElementVisible('.newFile', 10000)
.click('.envView')
contractHelper.testContracts(browser, sources.sources['Untitled.sol'], ['Untitled.sol:Ballot'], function () {
contractHelper.testContracts(browser, sources.sources['browser/Untitled.sol'], ['browser/Untitled.sol:Ballot'], function () {
browser.end()
})
}

@ -5,7 +5,7 @@ var sauce = require('./sauce')
var sources = {
'sources': {
'Untitled.sol': `pragma solidity ^0.4.0;
'browser/Untitled.sol': `pragma solidity ^0.4.0;
contract TestContract { function f() returns (uint) { return 8; } }`
}
}
@ -27,7 +27,7 @@ function runTests (browser) {
browser
.waitForElementVisible('.newFile', 10000)
.click('.envView')
contractHelper.testContracts(browser, sources.sources['Untitled.sol'], ['Untitled.sol:TestContract'], function () {
contractHelper.testContracts(browser, sources.sources['browser/Untitled.sol'], ['browser/Untitled.sol:TestContract'], function () {
browser.click('.create .constructor .call')
.waitForElementPresent('.instance .call[title="f"]')
.click('.instance .call[title="f"]')

@ -6,17 +6,17 @@ var sauce = require('./sauce')
var sources = {
'sources': {
'ballot.sol': examples.ballot.content,
'test/client/credit.sol': '',
'src/voting.sol': '',
'src/leasing.sol': '',
'src/gmbh/contract.sol': false,
'src/gmbh/test.sol': false,
'src/gmbh/company.sol': false,
'src/gmbh/node_modules/ballot.sol': false,
'src/ug/finance.sol': false,
'app/solidity/mode.sol': true,
'app/ethereum/constitution.sol': true
'browser/ballot.sol': examples.ballot.content,
'browser/test/client/credit.sol': '',
'browser/src/voting.sol': '',
'browser/src/leasing.sol': '',
'browser/src/gmbh/contract.sol': false,
'browser/src/gmbh/test.sol': false,
'browser/src/gmbh/company.sol': false,
'browser/src/gmbh/node_modules/ballot.sol': false,
'browser/src/ug/finance.sol': false,
'browser/app/solidity/mode.sol': true,
'browser/app/ethereum/constitution.sol': true
}
}

@ -5,7 +5,7 @@ var sauce = require('./sauce')
var sources = {
'sources': {
'Untitled.sol': 'contract test1 {} contract test2 {}'
'browser/Untitled.sol': 'contract test1 {} contract test2 {}'
}
}
@ -26,7 +26,7 @@ function runTests (browser) {
browser
.waitForElementVisible('.newFile', 10000)
.click('.envView')
contractHelper.testContracts(browser, sources.sources['Untitled.sol'], ['Untitled.sol:test1', 'Untitled.sol:test2'], function () {
contractHelper.testContracts(browser, sources.sources['browser/Untitled.sol'], ['browser/Untitled.sol:test1', 'browser/Untitled.sol:test2'], function () {
browser.end()
})
}

@ -6,7 +6,7 @@ var dom = require('../helpers/dom')
var sources = {
'sources': {
'Untitled.sol': `
'browser/Untitled.sol': `
contract test1 { address test = tx.origin; }
contract test2 {}
contract TooMuchGas {
@ -33,13 +33,13 @@ function runTests (browser) {
browser
.waitForElementVisible('.newFile', 10000)
.click('.envView')
contractHelper.testContracts(browser, sources.sources['Untitled.sol'], ['Untitled.sol:TooMuchGas', 'Untitled.sol:test1', 'Untitled.sol:test2'], function () {
contractHelper.testContracts(browser, sources.sources['browser/Untitled.sol'], ['browser/Untitled.sol:TooMuchGas', 'browser/Untitled.sol:test1', 'browser/Untitled.sol:test2'], function () {
browser
.click('.staticanalysisView')
.click('#staticanalysisView button')
.waitForElementPresent('#staticanalysisresult .warning', 2000, true, function () {
dom.listSelectorContains(['Untitled.sol:2:33: Use of tx.origin',
'Fallback function of contract Untitled.sol:TooMuchGas requires too much gas'],
dom.listSelectorContains(['browser/Untitled.sol:2:33: Use of tx.origin',
'Fallback function of contract browser/Untitled.sol:TooMuchGas requires too much gas'],
'#staticanalysisresult .warning span',
browser, function () {
browser.end()

Loading…
Cancel
Save