diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000000..0b07bbd8b7 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,44 @@ +# Javascript Node CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-javascript/ for more details +# +version: 2 +jobs: + remix-lib: + docker: + - image: circleci/node:7.10 + environment: + working_directory: ~/repo + steps: + - checkout + - run: npm install && npm run bootstrap + - run: cd remix-lib && npm test + + remix-debug: + docker: + - image: circleci/node:7.10 + environment: + working_directory: ~/repo + steps: + - checkout + - run: npm install && npm run bootstrap + - run: cd remix-debug && npm test + + remix-analyzer: + docker: + - image: circleci/node:7.10 + environment: + working_directory: ~/repo + steps: + - checkout + - run: npm install && npm run bootstrap + - run: cd remix-analyzer && npm test + +workflows: + version: 2 + build_all: + jobs: + - remix-lib + - remix-debug + - remix-analyzer + diff --git a/.gitignore b/.gitignore index 920bf3aade..68f8dbe9a6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ npm-debug.log lint.xml .vscode test-browser/reports/* +babelify-src +docs/_build +package-lock.json +.DS_Store +.tern-port diff --git a/.travis.yml b/.travis.yml index eb6e4e4d37..5fd298c5b5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,18 @@ language: node_js node_js: - stable +env: + - TEST_DIR=remix-lib + - TEST_DIR=remix-solidity + - TEST_DIR=remix-debug script: - - npm run test - - ./ci/browser_tests.sh + - cd $TEST_DIR && npm install && npm test deploy: provider: script - script: ci/deploy_from_travis.sh + script: remix-debugger/ci/deploy_from_travis.sh skip_cleanup: true on: branch: master + condition: $TEST_DIR = remix-debugger +cache: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..ae1309a1dd --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,22 @@ +# Contributing + +Everyone is very welcome to contribute on the codebase of Remix. Please reach us in [Gitter](https://gitter.im/ethereum/remix)! + +The easiest way to work on `remix-debugger` or `remix-solidity` is to pull the `remix-ide` (Remix IDE) repository (https://github.com/ethereum/remix-ide) which has a strong Remix integration. Then, in `remix-ide`, execute `npm install`, `npm run pullremix`, `npm run linkremixsolidity`. +then `npm run build && npm run serve` to start a new Remix IDE instance (you can browse `127.0.0.1:8080`). + +To interact with the Remix code: + +1. For static analysis: compile a new contact, click on the `Analysis`, the content of this tab is provided by the `remix-solidity/analysis` library + +2. For Debugging: compile a new contact, deploy it (`Create` button in the `run` tab), in the remix IDE terminal a transaction should appear, click on `Debug`. The Debugger tab get the focus, the content if this tab is provided by the `remix-debugger` library. + +3. For Decoding local and state variables: follow 2), then expand (is not already) the `Solidity State` and `Solidity Locals` panels, the content of thoses panel are provided by the `remix-solidity/decoder` library. + +**Reminder**: the Remix repository contains tools used to debug transactions, one of these tools being a debugger. The [`remix-ide` repository](https://github.com/ethereum/remix-ide) contains the Remix IDE (online version remix.ethereum.org), which make use of the Remix repository for the debugging features. + +## Coding style + +Remix makes use of the `npm` [coding style](https://docs.npmjs.com/misc/coding-style). Please make sure your code is compliant with this coding standard before sending a PR. You'll find in the link above tools to integrate this coding style with multiple developer tools (Emacs, Atom...). You can also make use of the `npm run test` which will check your local repository against the coding style. + + diff --git a/README.md b/README.md index 1007029706..88bff7cbc2 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,52 @@ -# REMIX +# Remix [![Join the chat at https://gitter.im/ethereum/remix](https://badges.gitter.im/ethereum/remix.svg)](https://gitter.im/ethereum/remix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Ethereum IDE and tools for the web -## REMIX WEBSITE: +Ethereum tools for the web. -Remix is avalaible at http://ethereum.github.io/remix. -You can use it either inside Mist or by connecting to geth or eth. -Note that connecting to Geth does not work through https. +*Are you looking for the Remix IDE? Follow [this link](https://github.com/ethereum/remix-ide)!* -You'll have to run your own node using the following parameters: ++ [What is Remix?](#what-is-remix) ++ [How to use Remix?](#how-to-use) ++ [Modules](#modules) ++ [Contributing guidelines](#contributing) -Using Geth: +## What is Remix? - geth --rpc --rpcapi 'web3,eth,debug' --rpcport 8545 --rpccorsdomain '*' - -Using Eth: +**Remix** is a suite of tools to interact with the Ethereum blockchain in order to debug transactions, stored in this Git repository. A Remix transaction Web debugger is available [here](http://remix.ethereum.org), and its source code is part of this repository. - eth -j --rpccorsdomain '*' +The **Remix IDE** is an IDE for Solidity dApp developers, powered by Remix. The Remix IDE repository **is available [here](https://github.com/ethereum/remix-ide)**, and an online version is available at https://remix.ethereum.org. -geth will run the rpc server on http://localhost:8545, remix uses by default this url. +## How to use Remix -Remix will use this instance of Geth to retrieve the transaction and the associated trace. -This instance should **only** be used for debugging purposes. Never use features that could have an impact on your keys (do not unlock any keys, do not use this instance together with the wallet, do not activate the admin web3 API). +### Prerequisites -## INSTALLATION: +To use Remix tools, you'll need to connect to an Ethereum node. You can do that using [the Mist browser](https://github.com/ethereum/mist), or by connecting to your local Ethereum node (`geth`, or `eth`). *Note: connecting to `geth` does not work through https.* -Brief instructions to build for linux(Todo add other platforms) we will add detailed instructions later ++ Using `geth`: `geth --rpc --rpcapi 'web3,eth,debug' --rpcport 8545 --rpccorsdomain '*'`. -Install eth or geth, npm and node.js (see https://docs.npmjs.com/getting-started/installing-node), then do: ++ Using `eth`: `eth -j --rpccorsdomain '*'` - git clone https://github.com/ethereum/remix - cd remix - npm install && npm run build && npm run start_node +**DO NOT DO EXECUTE THESE COMMANDS IF `geth`/`eth` STORES YOUR PRIVATE KEYS**, as any external system might be able to access your node's RPC server! -open remix/index.html in your browser. +Those commands will run the RPC server on `localhost:8545`, which is the default URL that Remix will connect to. This instance should **only** be used for debugging purposes. Never use features that could have an impact on your keys: do not unlock any keys, do not use this instance together with the wallet, do not activate the admin `web3` API. -## REMIX First Step: +### Run the debugger -Once remix is connected to a node, you will be able to debug transactions. -There's two way of doing that: - - using a block number and a transaction index. - - using a transaction hash. - -When loading the transaction succeed, the hash, from and to field will show up. -Then the vm trace is loaded. +See [here](remix-debugger/README.md) how to install, run and use the debugger locally. The debugger itself contains several controls that allow stepping over the trace and seeing the current state of a selected step. -#### Slider and Stepping action: - -The slider allows to move quickly from a state to another. -Stepping actions are: -- Step Into Back -- Step Over Back -- Step Over Forward -- Step Into Forward -- Jump Next Call (this will select the next state that refers to a context changes - CALL, CALLCODE, DELEGATECALL, CREATE) - -#### State Viewer: - -The upper right panel contains basic informations about the current step: -- VMTraceStep: the index in the trace of the current step. -- Step -- Add memory -- Gas: gas used by this step -- Remaining gas: gas left -- Loaded address: the current code loaded, refers to the executing code. - -The other 6 panels describe the current selected state: - - Instructions list: list of all the instruction that defines the current executing code. - - Stack - - Storage Changes - - Memory - - Call Data$ - - Call Stack - -## CODING STYLE: - -Remix uses npm coding style: https://docs.npmjs.com/misc/coding-style -Please be sure your code is compliant with this coding standard before sending PR. -There's on the above page a bunch of links that propose integration with developer tools (Emacs, Atom, ...). -You can also run 'npm run test' to check your local repository against the coding style. +## Remix Modules + +Remix is built out of 3 different modules: + ++ [`remix-solidity`](remix-solidity/README.md) provides Solidity analysis and decoding functions. ++ [`remix-lib`](remix-lib/README.md) ++ [`remix-debug`](remix-debugger/README.md) contains the debugger. + +## Contributing + +Everyone is very welcome to contribute on the codebase of Remix. Please reach us in [Gitter](https://gitter.im/ethereum/remix). + +For more information on the contributing procedure, see [CONTRIBUTING.md](CONTRIBUTING.md). For more information on running and developing the Remix debugger, see [the debugger README.md](remix-debugging/README.md). diff --git a/ci/browser_tests.sh b/ci/browser_tests.sh deleted file mode 100755 index 05feb26930..0000000000 --- a/ci/browser_tests.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash - - -SAUCECONNECT_URL="https://saucelabs.com/downloads/sc-4.3.16-linux.tar.gz" -SAUCECONNECT_USERNAME="yanneth" -SAUCECONNECT_ACCESSKEY="1f5a4560-b02b-41aa-b52b-f033aad30870" -SAUCECONNECT_JOBIDENTIFIER="remix_tests_${TRAVIS_JOB_NUMBER}" -SAUCECONNECT_READYFILE="sc.ready" -TEST_EXITCODE=0 - -npm run build -npm run serve & - -wget $SAUCECONNECT_URL -tar -zxvf sc-4.3.16-linux.tar.gz -./sc-4.3.16-linux/bin/sc -u $SAUCECONNECT_USERNAME -k $SAUCECONNECT_ACCESSKEY -i $SAUCECONNECT_JOBIDENTIFIER --readyfile $SAUCECONNECT_READYFILE & -while [ ! -f $SAUCECONNECT_READYFILE ]; do - sleep .5 -done - -npm run nightwatch_remote_firefox || TEST_EXITCODE=1 -npm run nightwatch_remote_chrome || TEST_EXITCODE=1 -npm run nightwatch_remote_safari || TEST_EXITCODE=1 -npm run nightwatch_remote_ie || TEST_EXITCODE=1 - -node ci/sauceDisconnect.js $SAUCECONNECT_USERNAME $SAUCECONNECT_ACCESSKEY $SAUCECONNECT_JOBIDENTIFIER - -echo $TEST_EXITCODE -if [ $TEST_EXITCODE -eq 1 ] -then - exit 1 -fi diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000000..a04587262d --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,216 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Remix.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Remix.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Remix" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Remix" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/analysis_tab.md b/docs/analysis_tab.md new file mode 100644 index 0000000000..8c82d0e5f1 --- /dev/null +++ b/docs/analysis_tab.md @@ -0,0 +1,33 @@ +Analysis +======== + +This section gives information about the last compilation. By default, a +new analysis is run at each compilation. + +The analysis tab gives detailed information about the contract code. It +can help you avoid code mistakes and to enforce best practices. + +![image](images/remix_analysistab.png) + +Here is the list of analyzers: + +Security: +- Transaction origin: Warns if tx.origin is used +- Check effects: Avoid potential reentrancy bugs +- Inline assembly: Use of Inline Assembly +- Block timestamp: Semantics maybe unclear +- Low level calls: Semantics maybe unclear +- Block.blockhash usage: Semantics maybe unclear + + +Gas & Economy: +- Gas costs: Warns if the gas requirements of the functions + are too high +- This on local calls: Invocation of local functions via + this + +Miscellaneous: +- Constant functions: Checks for potentially constant + functions +- Similar variable names: Checks if variable names are too + similar diff --git a/docs/code_contribution_guide.md b/docs/code_contribution_guide.md new file mode 100644 index 0000000000..ad647df40e --- /dev/null +++ b/docs/code_contribution_guide.md @@ -0,0 +1,12 @@ +Code contribution guide +======================= + +Remix is an open source tool and we encourage anyone to help us improve our tool. +You can do that by opening issues, giving feedback or by contributing a pull request +to our codebase. + +Remix application is built with Javascript and it doesn't use any framework, we only +rely on selected set of NPM modules, like `yo-yo`, `csjs-inject` and others. Check out +package.json file to learn more about the stack. + +To learn more, please visit our [Github page](https://github.com/ethereum/remix-ide). diff --git a/docs/community.md b/docs/community.md new file mode 100644 index 0000000000..9ffd1553af --- /dev/null +++ b/docs/community.md @@ -0,0 +1,14 @@ +Community +======================= + +We know that blockchain ecosystem is very new and that lots of information is scattered around the web. +That is why we created a community support channel where we and other users try to answer your questions if +you get stuck using Remix. Please, join [the community](https://gitter.im/ethereum/remix) and ask community for help. + +For anyone who is interested in developing a custom plugin for Remix or who wants to contribute to the codebase, +we opened [contributors' channel](https://gitter.im/ethereum/remix-dev) specially for developers working on Remix tool. + +We would kindly ask you to respect the space and to use it for +getting help with your work and the developers' channel for discussions related to working on Remix codebase. If you have +ideas for collaborations or you want to promote your project, try to find some more appropriate channels to do so. Or you can contact +main contributors directly on Gitter or Twitter. diff --git a/docs/compile_tab.md b/docs/compile_tab.md new file mode 100644 index 0000000000..55b7452da9 --- /dev/null +++ b/docs/compile_tab.md @@ -0,0 +1,28 @@ +Compiling contracts +=================== + +By default Remix triggers a compilation each time the current file is +changed or another file is selected. If the contract has a lot of +dependencies and takes a long time to compile, it is possible to disable +the autocompilation. + +![image](images/remix_compiletab.png) + +After each compilation, a list is updated with all the newly compiled +contracts. + +Details modal dialog displays detailed information about the current +selected contract. + +From this tab, you can also publish your contract to Swarm (only non +abstract contracts can be published). + +Published data notably contains the `abi` and solidity source code. + +After a contract is published, you can find its metadata information +using the bzz URL located in the details modal dialog `SWARM LOCATION`. + +Compilation Errors and Warning are displayed below the contract section. +At each compilation, the static analysis tab builds a report. It is very +valuable when addressing reported issues even if the compiler doesn't +complain. ([see more](http://remix.readthedocs.io/en/latest/analysis_tab.html)) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000000..4e5b322071 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,305 @@ +# -*- coding: utf-8 -*- +# +# Remix documentation build configuration file, created by +# sphinx-quickstart on Mon Feb 20 12:16:16 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + + + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] + + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Remix, Ethereum-IDE' +copyright = u'2018, Remix' +author = u'Remix team' + +github_doc_root = 'https://github.com/ethereum/remix/tree/master/docs/' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'1' +# The full version, including alpha/beta/rc tags. +release = u'1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +highlight_language = 'JavaScript' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'sphinx_rtd_theme' + + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Remixdoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'Remix.tex', u'Remix Documentation', + u'yann300', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'remix', u'Remix Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Remix', u'Remix Documentation', + author, 'Remix', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + +from recommonmark.parser import CommonMarkParser +source_suffix = ['.rst', '.md'] +source_parsers = {'.md': CommonMarkParser} + + +# app setup hook +# def setup(app): +# app.add_config_value('recommonmark_config', { +# 'url_resolver': lambda url: github_doc_root + url, +# 'enable_auto_toc_tree': True, +# 'enable_eval_rst': True, +# 'enable_auto_doc_ref': True, +# }, True) +# app.add_transform(AutoStructify) diff --git a/docs/debugger_tab.md b/docs/debugger_tab.md new file mode 100644 index 0000000000..004941baf1 --- /dev/null +++ b/docs/debugger_tab.md @@ -0,0 +1,12 @@ +Debugging +========= + +This module allows you to debug the transaction. It can be used to +deploy transactions created from Remix and already mined transactions. +(debugging works only if the current environment provides the necessary +features). + +![image](images/remix_debuggertab.png) + +For more information about debugging, see the [Tutorial on debugging transactions with Remix +](http://remix.readthedocs.io/en/latest/tutorial_debug.html) diff --git a/docs/file_explorer.md b/docs/file_explorer.md new file mode 100644 index 0000000000..cabf680e5d --- /dev/null +++ b/docs/file_explorer.md @@ -0,0 +1,52 @@ +File Explorer +============= + +The file explorer lists by default all the files stored in your browser. +You can see them in the browser folder. You can always rename, remove or +add new files to the file explorer. + +![image](images/remix_file_explorer_browser.png) + +Note that clearing the browser storage will permanently delete all the +solidity files you wrote. To avoid this, you can use Remixd, which +enables you to store and sync files in the browser with your local +computer (for more information see ../tutorial\_remixd\_filesystem) + +![image](images/remix_file_explorer_menu.png) + +We will start by reviewing at the icons at the top left - from left to +the right: + +Create new File +--------------- + +Creates a new `untitled.sol` file in Remix. + +Add Local File +-------------- + +Allows you to select files from the local file system and import them to +the Remix browser storage. + +Publish to Gist +--------------- + +Publishes all files from the browser folder to a gist. +Gist API has changed in 2018 and it unfortunately requires users to be authenticated to be able to publish a gist. + +Click [this link](https://github.com/settings/tokens) to Github tokens setup and select Generate new token. +Then check only Create gists checkbox and generate a new token. + +Then paste it in Remix (right panel/Settings tab) and click Save. Now you should be able to use the feature. + +Copy to another Remix instance +------------------------ + +Enables you to copy files from the browser storage to another instance +(URL) of Remix. + +Connect your filesystem to Remix +-------------------- + +Allows to sync between Remix and your local file system (see +[more about RemixD](http://remix.readthedocs.io/en/latest/tutorial_remixd_filesystem.html)). diff --git a/docs/images/remix_analysistab.png b/docs/images/remix_analysistab.png new file mode 100755 index 0000000000..e43143962c Binary files /dev/null and b/docs/images/remix_analysistab.png differ diff --git a/docs/images/remix_compiletab.png b/docs/images/remix_compiletab.png new file mode 100755 index 0000000000..cd51b11574 Binary files /dev/null and b/docs/images/remix_compiletab.png differ diff --git a/docs/images/remix_debuggertab.png b/docs/images/remix_debuggertab.png new file mode 100755 index 0000000000..104d9744b2 Binary files /dev/null and b/docs/images/remix_debuggertab.png differ diff --git a/docs/images/remix_editor.png b/docs/images/remix_editor.png new file mode 100755 index 0000000000..9260f05fe4 Binary files /dev/null and b/docs/images/remix_editor.png differ diff --git a/docs/images/remix_file_explorer_browser.png b/docs/images/remix_file_explorer_browser.png new file mode 100755 index 0000000000..3f09c0c752 Binary files /dev/null and b/docs/images/remix_file_explorer_browser.png differ diff --git a/docs/images/remix_file_explorer_menu.png b/docs/images/remix_file_explorer_menu.png new file mode 100755 index 0000000000..2140e9b36c Binary files /dev/null and b/docs/images/remix_file_explorer_menu.png differ diff --git a/docs/images/remix_quickstart_javascriptvm_callinginstance.png b/docs/images/remix_quickstart_javascriptvm_callinginstance.png new file mode 100644 index 0000000000..fe11a2cb06 Binary files /dev/null and b/docs/images/remix_quickstart_javascriptvm_callinginstance.png differ diff --git a/docs/images/remix_quickstart_javascriptvm_creation.png b/docs/images/remix_quickstart_javascriptvm_creation.png new file mode 100644 index 0000000000..fc7598aea7 Binary files /dev/null and b/docs/images/remix_quickstart_javascriptvm_creation.png differ diff --git a/docs/images/remix_quickstart_javascriptvm_creationTransaction.png b/docs/images/remix_quickstart_javascriptvm_creationTransaction.png new file mode 100644 index 0000000000..0b27bc105f Binary files /dev/null and b/docs/images/remix_quickstart_javascriptvm_creationTransaction.png differ diff --git a/docs/images/remix_recorder.png b/docs/images/remix_recorder.png new file mode 100644 index 0000000000..1f7abc7363 Binary files /dev/null and b/docs/images/remix_recorder.png differ diff --git a/docs/images/remix_runtab.png b/docs/images/remix_runtab.png new file mode 100755 index 0000000000..c636298493 Binary files /dev/null and b/docs/images/remix_runtab.png differ diff --git a/docs/images/remix_runtab_example.png b/docs/images/remix_runtab_example.png new file mode 100755 index 0000000000..78fba7e994 Binary files /dev/null and b/docs/images/remix_runtab_example.png differ diff --git a/docs/images/remix_settingstab.png b/docs/images/remix_settingstab.png new file mode 100755 index 0000000000..bfd50332e1 Binary files /dev/null and b/docs/images/remix_settingstab.png differ diff --git a/docs/images/remix_supporttab.png b/docs/images/remix_supporttab.png new file mode 100755 index 0000000000..b62ead496b Binary files /dev/null and b/docs/images/remix_supporttab.png differ diff --git a/docs/images/remix_terminal.png b/docs/images/remix_terminal.png new file mode 100755 index 0000000000..3f096a2490 Binary files /dev/null and b/docs/images/remix_terminal.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000000..10e3b25db9 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,72 @@ +Welcome to Remix documentation! +=============================== + +Remix is a powerful, open source tool that helps you write Solidity contracts straight from the browser. +Written in Javascript, Remix supports both usage in the browser or locally. + +Remix also supports testing, debugging and deploying of smart contracts and much more. + +Our Remix project with all its features is available +at `remix.ethereum.org `__ and more information can be found in these +docs. Our tool is available at `our GitHub repository +`__. + +This set of documents covers instructions on how to use Remix and some tutorials to help you get started. + +Userful links: + +- `Solidity documentation `__ + +- `Remix alpha `__ - version where we test new Remix release (not stable!) + +- `Ethereum Stackexchange for Remix `__ + +- `Community support channel `__ + +- `Dapp Developer resources (Ethereum wiki) `__ + + +.. toctree:: + :maxdepth: 2 + :caption: Quick start + + packages + solidity_editor + compile_tab + quickstart_javascript_vm + settings_tab + + +.. toctree:: + :maxdepth: 2 + :caption: Deploy and test + + run_tab + udapp + +.. toctree:: + :maxdepth: 2 + :caption: Other Remix features + + file_explorer + debugger_tab + analysis_tab + terminal + +.. toctree:: + :maxdepth: 2 + :caption: Tutorials and workshops + + workshop_Building_smart_contracts_with_Remix + tutorial_remixd_filesystem + tutorial_debug + tutorial_import + tutorial_mist + +.. toctree:: + :maxdepth: 2 + :caption: Code contribution guide + + code_contribution_guide + Community + support_tab diff --git a/docs/make.bat b/docs/make.bat new file mode 100755 index 0000000000..963594cac0 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,263 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 1>NUL 2>NUL +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Remix.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Remix.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/docs/mist1.png b/docs/mist1.png new file mode 100644 index 0000000000..cd2991e2fd Binary files /dev/null and b/docs/mist1.png differ diff --git a/docs/packages.md b/docs/packages.md new file mode 100644 index 0000000000..ba8440c424 --- /dev/null +++ b/docs/packages.md @@ -0,0 +1,13 @@ +Packages +======== + +This part focuses on using `Remix IDE`, which is a browser based smart contract IDE. We will basically answer to the question: +Where can I use / download `Remix IDE`, and what is the difference between packages? + +- An online version is available at [https://remix.ethereum.org](https://remix.ethereum.org). This version is stable and is updated at almost every release. +- An alpha online version is available at [https://remix-alpha.ethereum.org](https://remix-alpha.ethereum.org). This is not a stable version. +- npm `remix-ide` package `npm install remix-ide -g`. `remix-ide` create a new instance of `Remix IDE` available at [http://127.0.0.1:8080](http://127.0.0.1:8080) and make the current folder available to Remix IDE by automatically starting `remixd`. +see [Connection to `remixd`](http://remix.readthedocs.io/en/latest/tutorial_remixd_filesystem.html) for more information about sharing local file with `Remix IDE`. +- Github release: [https://github.com/ethereum/remix-ide/releases](https://github.com/ethereum/remix-ide/releases) . The source code is packaged at every release but still need to be built using `npm run build`. +- Mist: `Remix IDE` can be started and use the local geth node from `Mist` [https://github.com/ethereum/mist/releases](https://github.com/ethereum/mist/releases) +- Electron: `Remix IDE` wrapped as an Electron app is available at [https://github.com/horizon-games/remix-app](https://github.com/horizon-games/remix-app) \ No newline at end of file diff --git a/docs/quickstart_javascript_vm.md b/docs/quickstart_javascript_vm.md new file mode 100644 index 0000000000..a9abe8ee1f --- /dev/null +++ b/docs/quickstart_javascript_vm.md @@ -0,0 +1,104 @@ +Quick Start using the JavaScript VM +=================================== + +There are 3 type of environments Remix can be plugged to: +`Javascript VM`, `Injected provider`, or `Web3 provider`. (for details see [Running transactions](http://remix.readthedocs.io/en/latest/run_tab.html)) + +Both `Web3 provider` and `Injected provider` require the use of an +external tool. + +The external tool for `Web3 provider` is an Ethereum node the tools for +`Injected provider` are Mist or Metamask. + +The `JavaScript VM` mode is convenient because each execution runs in +your browser. Thus reloading the page will restart Remix with an empty +state. + +So for performance purposes, it might also be better to use an external +node. + +Selecting the VM mode +--------------------- + +Make sure the VM mode is selected. All accounts displayed in `Accounts` +should have 100 ether. + +Sample contract +--------------- + +``` {.sourceCode .none} +pragma solidity ^0.4.16; + +contract testContract { + + uint value; + function testContract(uint _p) { + value = _p; + } + + function setP(uint _n) payable { + value = _n; + } + + function setNP(uint _n) { + value = _n; + } + + function get () constant returns (uint) { + return value; + } +} +``` + +This contract is very basic. The goal is to quickly start to create and +to interact with a sample contract. + +Deploying an instance +--------------------- + +The `Compile tab` displays information related to the current contract +(note that there can be more than one) (see ../compile\_tab). + +Moving on, in the `Run tab` select, `JavaScript VM` to specify that you +are going to deploy an instance of the contract in the `JavaScript VM` +state. + +![image](images/remix_quickstart_javascriptvm_creation.png) + +The constructor of `testContract` needs a parameter (of type `uint`). +Give any value and click on `Create`. + +The transaction which deploys the instance of `testContract` is created. + +In a "normal" blockchain, it can take several seconds to execute. This +is the time for the transaction to be mined. However, because we are +using the `JavaScript VM`, our execution is immediate. + +The terminal will inform you about the transaction. You can see details +there and start debugging. + +The newly created instance is displayed in the `run tab`. + +![image](images/remix_quickstart_javascriptvm_creationTransaction.png) + +Interacting with an instance +---------------------------- + +This new instance contains 3 actions which corresponds to the 3 +functions (`setP`, `setPN`, `get`). Clicking on `SetP` or `SetPN` will +create a new transaction. + +Note that `SetP` is `payable` (red action) : it is possible to send +value (Ether) to the contract. + +`SetPN` is not payable (light red action) : it is not possible to send +value (Ether) to the contract. + +Clicking on `get` will not execute a transaction (blue action). It is +not necessary to do so because `get` does not modify the state (variable +`value`) of this instance. + +As `get` is `constant` you can see the return value just below the +action. + +![image](images/remix_quickstart_javascriptvm_callinginstance.png) diff --git a/docs/remix1.png b/docs/remix1.png new file mode 100644 index 0000000000..7575d34f6c Binary files /dev/null and b/docs/remix1.png differ diff --git a/docs/remix2.png b/docs/remix2.png new file mode 100644 index 0000000000..17a53affd9 Binary files /dev/null and b/docs/remix2.png differ diff --git a/docs/remix3.png b/docs/remix3.png new file mode 100644 index 0000000000..060352a755 Binary files /dev/null and b/docs/remix3.png differ diff --git a/docs/remix4.png b/docs/remix4.png new file mode 100644 index 0000000000..404ed50572 Binary files /dev/null and b/docs/remix4.png differ diff --git a/docs/remix5.png b/docs/remix5.png new file mode 100644 index 0000000000..4297d58adc Binary files /dev/null and b/docs/remix5.png differ diff --git a/docs/remix_breakpoint.png b/docs/remix_breakpoint.png new file mode 100644 index 0000000000..c755f61348 Binary files /dev/null and b/docs/remix_breakpoint.png differ diff --git a/docs/remix_debuginstructions.png b/docs/remix_debuginstructions.png new file mode 100644 index 0000000000..bb36decbaf Binary files /dev/null and b/docs/remix_debuginstructions.png differ diff --git a/docs/remix_debugtransactioninfo.png b/docs/remix_debugtransactioninfo.png new file mode 100644 index 0000000000..92d2e33100 Binary files /dev/null and b/docs/remix_debugtransactioninfo.png differ diff --git a/docs/remix_enterdebugsession.png b/docs/remix_enterdebugsession.png new file mode 100644 index 0000000000..77acc520ee Binary files /dev/null and b/docs/remix_enterdebugsession.png differ diff --git a/docs/remix_executionexception.png b/docs/remix_executionexception.png new file mode 100644 index 0000000000..9f28396b8a Binary files /dev/null and b/docs/remix_executionexception.png differ diff --git a/docs/remix_navigation.png b/docs/remix_navigation.png new file mode 100644 index 0000000000..d4187a462a Binary files /dev/null and b/docs/remix_navigation.png differ diff --git a/docs/remix_soliditylocals.png b/docs/remix_soliditylocals.png new file mode 100644 index 0000000000..bfa627d1a4 Binary files /dev/null and b/docs/remix_soliditylocals.png differ diff --git a/docs/remix_soliditystate.png b/docs/remix_soliditystate.png new file mode 100644 index 0000000000..789df079e2 Binary files /dev/null and b/docs/remix_soliditystate.png differ diff --git a/docs/remix_startdebugging.png b/docs/remix_startdebugging.png new file mode 100644 index 0000000000..bf5287a5c3 Binary files /dev/null and b/docs/remix_startdebugging.png differ diff --git a/docs/remix_stepdetail.png b/docs/remix_stepdetail.png new file mode 100644 index 0000000000..efde21304e Binary files /dev/null and b/docs/remix_stepdetail.png differ diff --git a/docs/remix_valueinput.png b/docs/remix_valueinput.png new file mode 100644 index 0000000000..381ebfc86a Binary files /dev/null and b/docs/remix_valueinput.png differ diff --git a/docs/remixd_alert.png b/docs/remixd_alert.png new file mode 100644 index 0000000000..d64e6f7214 Binary files /dev/null and b/docs/remixd_alert.png differ diff --git a/docs/remixd_connectionok.png b/docs/remixd_connectionok.png new file mode 100644 index 0000000000..e8381f55df Binary files /dev/null and b/docs/remixd_connectionok.png differ diff --git a/docs/remixd_noconnection.png b/docs/remixd_noconnection.png new file mode 100644 index 0000000000..b6cd6f5b40 Binary files /dev/null and b/docs/remixd_noconnection.png differ diff --git a/docs/run_tab.md b/docs/run_tab.md new file mode 100644 index 0000000000..f13fb6591f --- /dev/null +++ b/docs/run_tab.md @@ -0,0 +1,278 @@ +Running transactions +==================== + +The Run tab is an important section of Remix. It allows you to send +transactions to the current environment. + +![image](images/remix_runtab.png) + +Run Setup +--------- + +The following settings allow you to directly influence the transaction +execution: + +Environment: + +- `JavaScript VM`: All the transactions will be executed in + a sandbox blockchain in the browser. This means nothing + will be persisted and a page reload will restart a new + blockchain from scratch, the old one will not be saved. + +- `Injected Provider`: Remix will connect to an injected + web3 provider. `Mist` and `Metamask` are example of + providers that inject web3, thus can be used with this + option. + +- `Web3 Provider`: Remix will connect to a remote node. You + will need to provide the URL address to the selected + provider: geth, parity or any Ethereum client. + +- Account: the list of accounts associated with the current + environment (and their associated balances). + +- Gas Limit: the maximum amount of gas that can be set for all the + transactions created in Remix. + +- Value: the amount of value for the next created transaction (this + value is always reset to 0 after each transaction execution). + + ![image](images/remix_runtab_example.png) + +Initiate Instance +----------------- + +This section contains the list of compiled contracts and 2 actions: + +- `At Address` assumes the given address is an instance of the + selected contract. It is then possible to interact with an already + deployed contract. There's no check at this point, so be careful + when using this feature, and be sure you trust the contract at that + address. + +- `Create` send a transaction that deploys the selected contract. When + the transaction is mined, the newly created instance will be added + (this might take several seconds). Note that if the `constructor` + has parameters, you need to specify them. + +Pending Instances +----------------- + +Validating a transaction take several seconds. During this time, the GUI +shows it in a pending mode. When transaction is mined the number of +pending transactions is updated and the transaction is added to the log +(see ../terminal) + +USING ABI +--------- + +Using `Deploy` or `At Address` is a classic use case of Remix. It is +possible though to interact with a contract by using its ABI. The ABI is +a JSON array which describe its interface. + +To interact with a contract using the ABI, create a new file in Remix +with extension `*.abi` and copy the ABI content to it. Then in the input +next to `At Address`, put the Address of the contract you want to +interact with. Click on `At Address`, a new "connection" with the +contract will popup below. + +USING THE RECORDER +------------------ + +The Recorder allows to save a bunch of transactions in a JSON file and +rerun them later either in the same environment or in another. + +Saving to JSON allows to easily check the transaction list, tweak input +parameters, change linked library, etc... + +We can find many use cases for the recorder, for instance: +: - After having coded and tested contracts in a constrained + environment (like the JavaScript VM), it could be interesting to + redeploy them easily in a more persisted environment (like a + Geth node) in order to check whether everything behaves normally + in a classic environment. + - Deploying contract does often require more than creating one + transaction. + - Working in a dev environment does often require to setup the + state in a first place. + +![image](images/remix_recorder.png) + +Saving a record ends up with the creation of this type of content (see +below): + +In that specific record, 3 transactions are executed: + +The first corresponds to the deployment of the lib `testLib`. + +The second corresponds to the deployment of the contract `test`, the +first parameter of the constructor is set to 11. That contract depends +on a library. The linkage is done using the property `linkReferences`. +In that case we use the addres of the previously created library : +`created{1512830014773}`. the number is the id (timestamp) of the +transaction that leads to the creation of the library. + +The third parameter corresponds to the call to the function `set` of the +contract `test` (the property to is set to: `created{1512830015080}`) . +Input parameters are `1` and +`0xca35b7d915458ef540ade6068dfe2f44e8fa733c` + +all these transactions are created using the value of the accounts +`account{0}`. + +``` {.sourceCode .none} +{ +"accounts": { + "account{0}": "0xca35b7d915458ef540ade6068dfe2f44e8fa733c" +}, +"linkReferences": { + "testLib": "created{1512830014773}" +}, +"transactions": [ + { + "timestamp": 1512830014773, + "record": { + "value": "0", + "parameters": [], + "abi": "0xbc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a", + "contractName": "testLib", + "bytecode": "60606040523415600e57600080fd5b60968061001c6000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680636d4ce63c146044575b600080fd5b604a6060565b6040518082815260200191505060405180910390f35b6000610d809050905600a165627a7a7230582022d123b15248b8176151f8d45c2dc132063bcc9bb8d5cd652aea7efae362c8050029", + "linkReferences": {}, + "type": "constructor", + "from": "account{0}" + } + }, + { + "timestamp": 1512830015080, + "record": { + "value": "100", + "parameters": [ + 11 + ], + "abi": "0xc41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec", + "contractName": "test", + "bytecode": "60606040526040516020806102b183398101604052808051906020019091905050806000819055505061027a806100376000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632f30c6f61461006757806338cc48311461009e57806362738998146100f357806387cc10e11461011c575b600080fd5b61009c600480803590602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610145565b005b34156100a957600080fd5b6100b1610191565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34156100fe57600080fd5b6101066101bb565b6040518082815260200191505060405180910390f35b341561012757600080fd5b61012f6101c4565b6040518082815260200191505060405180910390f35b8160008190555080600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505050565b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b60008054905090565b600073__browser/ballot.sol:testLib____________636d4ce63c6000604051602001526040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160206040518083038186803b151561022e57600080fd5b6102c65a03f4151561023f57600080fd5b505050604051805190509050905600a165627a7a72305820e0b2510bb2890a0334bfe5613d96db3e72442e63b514cdeaee8fc2c6bbd19d3a0029", + "linkReferences": { + "browser/ballot.sol": { + "testLib": [ + { + "length": 20, + "start": 511 + } + ] + } + }, + "name": "", + "type": "constructor", + "from": "account{0}" + } + }, + { + "timestamp": 1512830034180, + "record": { + "value": "1000000000000000000", + "parameters": [ + 1, + "0xca35b7d915458ef540ade6068dfe2f44e8fa733c" + ], + "to": "created{1512830015080}", + "abi": "0xc41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec", + "name": "set", + "type": "function", + "from": "account{0}" + } + } +], +"abis": { + "0xbc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a": [ + { + "constant": true, + "inputs": [], + "name": "get", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } + ], + "0xc41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec": [ + { + "constant": true, + "inputs": [], + "name": "getInt", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getFromLib", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getAddress", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_t", + "type": "uint256" + }, + { + "name": "_add", + "type": "address" + } + ], + "name": "set", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "name": "_r", + "type": "uint256" + } + ], + "payable": true, + "stateMutability": "payable", + "type": "constructor" + } + ] +} +} +``` diff --git a/docs/settings_tab.md b/docs/settings_tab.md new file mode 100644 index 0000000000..328b9da44f --- /dev/null +++ b/docs/settings_tab.md @@ -0,0 +1,12 @@ +Settings +======== + +This section displays the current compiler version and allows one to change to another version. + +![image](images/remix_settingstab.png) + +Another important settings: + +- Text wrap: controls if the text in the editor should be wrapped. + +- Enable optimization: defines if the compiler should enable optimization during compilation. Enabling this option saves execution gas. It is useful to enable optimization for contracts ready to be deployed in production but could lead to some inconsistencies when debugging such a contract. diff --git a/docs/solidity_editor.md b/docs/solidity_editor.md new file mode 100644 index 0000000000..df6219afda --- /dev/null +++ b/docs/solidity_editor.md @@ -0,0 +1,18 @@ +Solidity Editor +=============== + +The Remix editor recompiles the code each time the current file is +changed or another file is selected. It also provides syntax +highlighting mapped to solidity keywords. + +![image](images/remix_editor.png) + +Here's the list of some important features: + +- It display opened files as tabs. +- Compilation Warning and Error are displayed in the gutter +- Remix saves the current file continuously (5s after the last + changes) +- +/- on the top left corner enable you to increase/decrease the font + size of the editor + diff --git a/docs/support.md b/docs/support.md new file mode 100644 index 0000000000..6bdd96968a --- /dev/null +++ b/docs/support.md @@ -0,0 +1,9 @@ +Support chat +======================= + +We know that blockchain ecosystem is very new and that lots of information is scattered around the web. +That is why we created a community support chat where we and other users try to answer your questions if +you get stuck using Remix. Please, join [the Remix channel](https://gitter.im/ethereum/remix) and ask community for help. + +For anyone who is interested in developing a custom plugin for Remix or who wants to contribute to the codebase, +we opened [another channel](https://gitter.im/ethereum/remix-dev) specially for developers working on Remix tool. diff --git a/docs/support_tab.md b/docs/support_tab.md new file mode 100644 index 0000000000..90ed7a47b2 --- /dev/null +++ b/docs/support_tab.md @@ -0,0 +1,9 @@ +Support tab in Remix +====================== + +This section provides a link to Remix Issues where users can report a +bug or suggest a feature, as well as providing other useful links. It +also displays a [Remix support +channel](http://gitter.im/ethereum/remix) + +![image](images/remix_supporttab.png) diff --git a/docs/terminal.md b/docs/terminal.md new file mode 100644 index 0000000000..9c1708996c --- /dev/null +++ b/docs/terminal.md @@ -0,0 +1,20 @@ +Terminal +======== + +![image](images/remix_terminal.png) + +Features, available in the terminal: + +- It integrates a JavaScript interpreter and the `web3` object. It + enables the execution of the JavaScript script which interacts with + the current context. (note that `web3` is only available if the + `web provider` or `injected provider` mode is selected). +- It displays important actions made while interacting with the Remix + IDE (i.e. sending a new transaction). +- It displays transactions that are mined in the current context. You + can choose to display all transactions or only transactions that + refers to the contracts Remix knows (e.g transaction created from + the Remix IDE). +- It allows searching for the data and clearing the logs from the + terminal. + diff --git a/docs/tuto_basicimport.png b/docs/tuto_basicimport.png new file mode 100644 index 0000000000..81a8009336 Binary files /dev/null and b/docs/tuto_basicimport.png differ diff --git a/docs/tuto_importgit.png b/docs/tuto_importgit.png new file mode 100644 index 0000000000..022eb4b011 Binary files /dev/null and b/docs/tuto_importgit.png differ diff --git a/docs/tuto_importswarm.png b/docs/tuto_importswarm.png new file mode 100644 index 0000000000..01fba3e646 Binary files /dev/null and b/docs/tuto_importswarm.png differ diff --git a/docs/tutorial_debug.md b/docs/tutorial_debug.md new file mode 100644 index 0000000000..31b33fa96e --- /dev/null +++ b/docs/tutorial_debug.md @@ -0,0 +1,222 @@ +Tutorial on debugging transactions with Remix +=============================================== + +The goal of this tutorial is to explain how to debug transaction using +Remix. + +Start debugging +--------------- + +There are two different ways to start debugging, each way correspond to +a different use case. + +### From the Transaction GUI + +We will not explain in detail here how to write or deploy contract. Let +us start with a basic contract (replace this one by your's): + +``` {.sourceCode .none} +contract Donation { + address owner; + event fundMoved(address _to, uint _amount); + modifier onlyowner { if (msg.sender == owner) _; } + address[] _giver; + uint[] _values; + + function Donation() { + owner = msg.sender; + } + + function donate() payable { + addGiver(msg.value); + } + + function moveFund(address _to, uint _amount) onlyowner { + uint balance = this.balance; + uint amount = _amount; + if (_amount <= this.balance) { + if (_to.send(this.balance)) { + fundMoved(_to, _amount); + } else { + throw; + } + } else { + throw; + } + } + + function addGiver(uint _amount) internal { + _giver.push(msg.sender); + _values.push(_amount); + } +} +``` + +For the purpose of this tutorial, we will run the `JavaScript VM` +(that's the default mode when you don't use Remix with Mist or +Metamask). This simulates a custom blockchain. You could do the same +using a proper backend node. + +Now, let's deploy the contract: + +Right panel / Red button `Create` + +![image](remix1.png) + +Then we should call the `Donate` function (that will send Ether to the +contract). + +Let's set the amount of Ether: + +Right panel / second tab from the left - fill in the ´´value´´ input (´1 +ether´ for instance) + +![image](remix_valueinput.png) + +Then click on `Donate`. As we are using the `JavaScript VM`, everything +goes almost instantly. + +Remix displays also some information related to each transaction result. +In the terminal, the transaction is logged and you can start debugging +it. + +![image](remix_startdebugging.png) + +### From the Debugger + +The debugger can be found in the right panel / 5th tab from the left. + +You can start a debug session by providing either a `transaction hash` +or a `block number` and `transaction index`. + +![image](remix3.png) + +Click the `play` button to start debugging. + +Using the debugger +------------------ + +The debugger allows one to see detailed informations about the +transaction's execution. It uses the editor (left panel) to display the +location in the source code where the current execution is. + +The transaction panel displays basic information about the current +transaction. + +![image](remix_debugtransactioninfo.png) + +The navigation part contains a slider and buttons that can be used to +step through the transaction execution. + +From the left to the right: + +step over back, step into back, step into forward, step over forward, +jump out (jump out of the current call), jump to the previous +breakpoint, jump to the next breakpoint. + +![image](remix_navigation.png) + +11 panels give detailed information about the execution: + +### Instructions + +![image](remix_debuginstructions.png) + +The Instructions panel displays the bytecode of the current executing +contract- with the current step highlighted. + +Important note: When this panel is hidden, the slider will have a +courser granularity and only stop at expression boundaries, even if they +are compiled into multiple EVM instructions. When the panel is +displayed, it will be possible to step over every instruction, even +those that refers to the same expression. + +### Solidity Locals + +![image](remix_soliditylocals.png) + +The Solidity Locals panel displays local variables associated with the +current context. + +### Solidity State + +![image](remix_soliditystate.png) + +The Solidity State panel displays state variables of the current +executing contract. + +### Low level panels + +These panels display low level informations about the execution: + +> - Stack +> - Storages Changes +> - Memory +> - Call Data +> - Call Stack +> - Return Value (only if the current step is a RETURN opcode) +> - Full Storages Changes (only at the end of the execution - display +> every storage change of every modified contract) + +### Reverted Transaction + +A transaction could be reverted (either because of out of gas exception, +Solidity `throw` or low level exception). + +In that case it is important to be aware of the exception and to locate +where the exception is in the source code. + +Remix will warn you when the execution throws an exception. The +`warning` button will jump to the last opcode before the exception +happened. + +![image](remix_executionexception.png) + +### Breakpoints + +The two last buttons from the navigation area are used to jump either +back to the previous breakpoint or forward to the next breakpoint. + +Breakpoints can be added and removed by clicking on the line number. + +![image](remix_breakpoint.png) + +When a debug session is started, the execution will jump to the first +encountered breakpoint. + +Important note: If you add a breakpoint to a line that declares a +variable, it might be triggered twice: Once for initializing the +variable to zero and second time for assigning the actual value. As an +example, assume you are debugging the following contract: + +``` {.sourceCode .none} +contract ctr { + function hid () { + uint p = 45; + uint m; + m = 89; + uint l = 34; + } +} +``` + +And let's says that breakpoints are set for the lines + +`uint p = 45;` + +`m = 89;` + +`uint l = 34;` + +then clicking on `Jump to next breakpoint` will stop at the following +lines in the given order: + +> `uint p = 45;` (declaration of p) +> +> `uint l = 34;` (declaration of l) +> +> `uint p = 45;` (45 assigned to p) +> +> `m = 89;` (89 assigned to m) +> +> `uint l = 34;` (34 assigned to l) diff --git a/docs/tutorial_import.md b/docs/tutorial_import.md new file mode 100644 index 0000000000..18fc416030 --- /dev/null +++ b/docs/tutorial_import.md @@ -0,0 +1,34 @@ +Importing Source Files in Solidity +================================== + +This tutorial will show you how to import local and external files. + +The compilation result will also contain contracts implemented in the +imported files. + +For a detailed explanation of the `import` keyword see the +[Solidity documentation](https://solidity.readthedocs.io/en/develop/layout-of-source-files.html?highlight=import#importing-other-source-files) + +Importing a local file +---------------------- + +Other files in Remix can be imported just by specifying their path. +Please use ./ for relative paths to increase portability. + +![image](tuto_basicimport.png) + +Importing from Github +--------------------- + +It is possible to import files directly from github with URLs like +`https://github.com///`. + +![image](tuto_importgit.png) + +Importing from Swarm +-------------------- + +Files can be imported using all URLs supported by swarm. If you do not +have a swarm node, swarm-gateways.net will be used instead. + +![image](tuto_importswarm.png) diff --git a/docs/tutorial_mist.md b/docs/tutorial_mist.md new file mode 100644 index 0000000000..568cf34b46 --- /dev/null +++ b/docs/tutorial_mist.md @@ -0,0 +1,309 @@ +Debugging a Dapp using Remix - Mist - Geth +========================================== + +The ultimate goal of this tutorial is to debug transactions that have +been created by a dapp front end. + +It is easy in Remix to debug a transaction created from its own GUI. +However, setting up an environment that allows you to debug transactions +created outside of Remix, require a bit more of complexity. + +We will need four tools for that : + +> - Geth - this is the center piece and provides the blockchain +> environment. We will basically run geth in a dev mode. +> - Mist - this is the Ethereum dapp browser. We will use it to browse +> our front end. +> - Remix - this is the Ethereum IDE. We will use it to develop our +> Solidity contract. +> - Any code editor you want - in order to write your front end :) + +Install the environment +----------------------- + +### Install Mist + +Mist is the Ethereum browser and the entry point of a Dapp. + +Please download [the latest +version](http://github.com/ethereum/mist/releases) (at least 0.8.9). + +Basically we will always run our front end in Mist (note that it is also +possible to use [Metamask](http://metamask.io)). + +### Install Geth + +[Geth](http://github.com/ethereum/go-ethereum/releases) is the official +Ethereum client. + +Running the environment +----------------------- + +### Run Geth + +We will run a test node. This node will have a new empty state and will +not be synced to the main or ropsten network. + + geth --ipcpath /geth.ipc --datadir --dev console + +`` is the folder where keys and chain data will be +stored. + +`--ipcpath` defines the end point that other apps (like Mist) use to +talk to geth. + +`--datadir` specifies the data directory. + +`--dev` sets the node into private chain mode and adds some debugging +flags. + +Then we need to create accounts and mine a bit to generate some Ether: + + // from the geth console : + personal.newAccount() // You can execute this command several time if you need more than one account. + miner.start() // generate some Ether. + miner.stop() // stop mining after 30s-60s - we could also keep mining. + +Next time we run Geth, we will only need to mine transactions (no need +to recreate account). + +### Run Mist + +If we run Mist without any argument, its internal Geth node will run. As +we have our own we need to specify the ipc path of the node installed +above. + + mist --rpc /geth.ipc + +(yes the option is --rpc) + +Once Mist is started, verify that it is connected to the test node +(that's very important!!). + +On the bottom left, check that the network is `Private-net` and that the +block number is the same as reported by the test node we are currently +running. Run the following command in the Geth Console to check: +web3.eth.blockNumber. + +![image](mist1.png) + +Clicking on Wallet will allow you to send transactions and check account +balances (if you are currently mining you should see the balance +increasing). + +### Starting Remix + +In Mist click on `Develop` / `Open Remix IDE` + +Remix will open in a new window. If this is the first time it is run, +the `Ballot` contract will be loaded. + +Now, we need to check if Remix is connected to Mist: + +Right panel / third tab from the left, `Injected Provider` should be +checked. + +![image](remix4.png) + +Right panel / second tab from the left, `Transaction Origin` should +contain accounts we have previously created in Geth. + +![image](remix5.png) + +Developing contract / front end +------------------------------- + +### Donation contract - Dapp Back end + +Here is a sample solidity contract. + +Copy and paste the following inside remix: + +``` {.sourceCode .none} +contract Donation { + address owner; + event fundMoved(address _to, uint _amount); + modifier onlyowner { if (msg.sender == owner) _; } + address[] _giver; + uint[] _values; + + function Donation() { + owner = msg.sender; + } + + function donate() payable { + addGiver(msg.value); + } + + function moveFund(address _to, uint _amount) onlyowner { + uint balance = this.balance; + uint amount = _amount; + if (_amount <= this.balance) { + if (_to.send(this.balance)) { + fundMoved(_to, _amount); + } else { + throw; + } + } else { + throw; + } + } + + function addGiver(uint _amount) internal { + _giver.push(msg.sender); + _values.push(_amount); + } +} +``` + +### Dapp Front end + +and here is the front end: + +``` {.sourceCode .none} +
+
Donation Contract
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+ + +``` + +I would suggest serving this file using `http-serve`, but you can use +any web server you like. + +Example: Dapp Front End + +### Important notice ! + +The variable `contractspec` contains the abi of the `donation` contract. +This means that if you change something in the contract interface +(function names, parameters, ...) you need to copy the new abi from +remix to the front end. + +Deploying +--------- + +Right panel / Red button `Create` + +![image](remix1.png) + +This creates a new transaction that deploys the `Donation` contract +(Mist will ask for the usual passphrase check). + +Wait for the transaction to be mined (don't forget to activate mining +`miner.start()`). Once this is done, you can use it by executing the +`moveFund` and `donate` function. But this is not what we want to +achieve. We want to run and debug those functions from the front end. + +Remix also display the address of the contract. Save it, we'll need this +address later. + +![image](remix2.png) + +Debugging +--------- + +From Mist, browse the above front end. In the first field, paste the +address of the newly created contract. Now, let's call the first +function (label `give`). + +You will need an account and a value. + +The account could be any account that is declared in the Wallet section +of Mist. This is the sender of the transaction that we are going to +create. The value should be no more than the actual balance of the +account - the unit is in wei, so just put `100` (100 wei), that should +be fine. + +Click on `Give` and wait for the transaction to be mined. + +The HTML block with id `log` is filled by all the transactions created +from the front end. It was easier for the purpose of this tutorial to +just log transactions in a div but you can have your own logging +mechanism. + +There is only one field that we need, this is the `transactionHash`. + +Copy it and switch to Remix. On the right side, the fifth panel shows a +small "bug" icon, that is the debugger. + +Paste the hash into the transaction field and click on the `play` +button. + +![image](remix3.png) + +You are now entering a debug session for the call to `donate`. + +Debugging in Remix is easier than with common tools like gdb because you +can freely move in time. Use the slider to change the current step and +click on the panels below to expand them and explore the curret state, +local variables, etc. There are also breakpoints to move between +sections of the code quickly, but more on all that later. + +At the time of this writing, there is an issue that could break the +contract creation. The a workaround for that at + . Please follow +the workaround or wait for this issue to be closed. + +Also, although retrieving a contract's storage when Remix is using the +JavaScript VM is working well, there is still work to be done when Remix +is using eth or geth as backend. diff --git a/docs/tutorial_remixd_filesystem.md b/docs/tutorial_remixd_filesystem.md new file mode 100644 index 0000000000..1acdc88923 --- /dev/null +++ b/docs/tutorial_remixd_filesystem.md @@ -0,0 +1,45 @@ +Access your local filesystem by using RemixD +=================================================== + +RemixD is an npm module. Its purpose is to give the remix web +application access to a folder from your local computer. + +The code of RemixD can be checked out +[here](https://github.com/ethereum/remixd) . + +Remixd can be globally installed using the following command: +`npm install -g remixd`. + +Then `remixd -s ` will start Remixd +and share the given folder. + +The folder is shared using a websocket connection between `Remix IDE` +and `Remixd`. + +Be sure the user executing Remix has read/write permission on the +folder. + +**Warning!** + +RemixD provides `full read and write access` to the given folder for `any +application` that can access the `TCP port 65520` on your local host. + +From `Remix IDE`, you will need to activate the connection. + +Click on the `localhost connection` icon: + +![image](remixd_noconnection.png) + +A modal dialog will ask confirmation + +![image](remixd_alert.png) + +Accepting this dialog will start a session. Once the connection is made, +the status will update and the connection icon should shows up in green. + +Hovering the icon will give more connection status information. + +At this point if the connection is successful, the shared folder will be +available in the file explorer. + +![image](remixd_connectionok.png) diff --git a/docs/udapp.md b/docs/udapp.md new file mode 100644 index 0000000000..642c0e2012 --- /dev/null +++ b/docs/udapp.md @@ -0,0 +1,25 @@ +Deployed contracts +==================== + +This section in the Run tab contains a list of deployed contracts to interact with through autogenerated UI of the deployed contract (also called udapp). + +Several cases apply: + +- The called function is declared as `constant` or `pure` in Solidity. The action has a blue background, clicking it does not +create a new transaction. Clicking it is not necessary because there are not state changes - but it will update the return +value of the function. + +- The called function has no special keywords. The action has a +light red background, clicking on does create a new transaction. +But this transaction cannot accept any amount of Ether. + +- The called function is declared as `payable` in Solidity. The +action has a red background, clicking it does create a new +transaction and this transaction can accept value. + + +For more information see more about [Solidity +modifier](http://solidity.readthedocs.io/en/develop/miscellaneous.html?highlight=pure#modifiers) +. + +If a function requires input parameters, it is required to specify them. diff --git a/docs/workshop_Building_smart_contracts_with_Remix.md b/docs/workshop_Building_smart_contracts_with_Remix.md new file mode 100644 index 0000000000..25ab50d5fb --- /dev/null +++ b/docs/workshop_Building_smart_contracts_with_Remix.md @@ -0,0 +1,8 @@ +Building Smart Contracts with Remix +======================= + +We prepared a thorough workshop that will help you build your own DApp. We will show you how to build and deploy smart contracts, how to deploy them and how to interact with them. Later in the workshop you will also learn how to connect your frontend with blockchain by using web3.js. + +### Let's get started + +This workshop was part of preparations for ethCC and Edcon. You can [watch the presentation talk](https://www.youtube.com/watch?v=nAI_Cr5Y8JY) and in parallel open the [workshop slides](https://slides.com/ninabreznik/deck-11-13#/) and follow along. diff --git a/docs_old/Makefile b/docs_old/Makefile new file mode 100644 index 0000000000..a04587262d --- /dev/null +++ b/docs_old/Makefile @@ -0,0 +1,216 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Remix.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Remix.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Remix" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Remix" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs_old/analysis_tab.rst b/docs_old/analysis_tab.rst new file mode 100644 index 0000000000..22f34b19f3 --- /dev/null +++ b/docs_old/analysis_tab.rst @@ -0,0 +1,27 @@ +Analysis +======== + +This section gives information about the last compilation. +By default, a new analysis is run at each compilation. + +The analysis tab gives detailed information about the contract code. It can help you avoid code mistakes and to enforce best practices. + +.. image:: images/remix_analysistab.png + +Here is the list of analyzers: + + - Security: + - Transaction origin: Warns if tx.origin is used + - Check effects: Avoid potential reentrancy bugs + - Inline assembly: Use of Inline Assembly + - Block timestamp: Semantics maybe unclear + - Low level calls: Semantics maybe unclear + - Block.blockhash usage: Semantics maybe unclear + + - Gas & Economy: + - Gas costs: Warns if the gas requirements of the functions are too high + - This on local calls: Invocation of local functions via this + + - Miscellaneous: + - Constant functions: Checks for potentially constant functions + - Similar variable names: Checks if variable names are too similar diff --git a/docs_old/compile_tab.rst b/docs_old/compile_tab.rst new file mode 100644 index 0000000000..47b376b095 --- /dev/null +++ b/docs_old/compile_tab.rst @@ -0,0 +1,20 @@ +Compiling contracts +=================== + +By default Remix triggers a compilation each time the current file is changed or another file is selected. +If the contract has a lot of dependencies and takes a long time to compile, it is possible to disable the `autocompilation`. + +.. image:: images/remix_compiletab.png + +After each compilation, a list is updated with all the newly compiled contracts. + +`Details` modal dialog displays detailed information about the current selected contract. + +From this tab, you can also publish your contract to Swarm (only non abstract contracts can be published). + +Published data notably contains the ``abi`` and solidity source code. + +After a contract is published, you can find its metadata information using the `bzz` URL located in the details modal dialog ``SWARM LOCATION``. + +Compilation Errors and Warning are displayed below the contract section. At each compilation, the static analysis tab builds a report. It is very valuable when addressing reported issues even if the compiler doesn't complain. +(see :doc:`../analysis_tab`) diff --git a/docs_old/conf.py b/docs_old/conf.py new file mode 100644 index 0000000000..8df0904e6c --- /dev/null +++ b/docs_old/conf.py @@ -0,0 +1,283 @@ +# -*- coding: utf-8 -*- +# +# Remix documentation build configuration file, created by +# sphinx-quickstart on Mon Feb 20 12:16:16 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# source_suffix = ['.rst', '.md'] +source_suffix = '.md' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Remix' +copyright = u'2017, yann300' +author = u'yann300' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'1' +# The full version, including alpha/beta/rc tags. +release = u'1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' +#html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# Now only 'ja' uses this config value +#html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +#html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Remixdoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', + +# Latex figure (float) alignment +#'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'Remix.tex', u'Remix Documentation', + u'yann300', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'remix', u'Remix Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Remix', u'Remix Documentation', + author, 'Remix', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/docs_old/debugger_tab.rst b/docs_old/debugger_tab.rst new file mode 100644 index 0000000000..cb73c90ca8 --- /dev/null +++ b/docs_old/debugger_tab.rst @@ -0,0 +1,11 @@ +Debugging +========= + +This module allows you to debug the transaction. +It can be used to deploy transactions created from Remix and already mined transactions. +(debugging works only if the current environment provides the necessary features). + +.. image:: images/remix_debuggertab.png + +For more information about debugging, click on the following link: +:doc:`../tutorial_debug` diff --git a/docs_old/file_explorer.rst b/docs_old/file_explorer.rst new file mode 100644 index 0000000000..e77ec2976a --- /dev/null +++ b/docs_old/file_explorer.rst @@ -0,0 +1,37 @@ +File Explorer +============= + +The file explorer lists by default all the files stored in your browser. You can see them in the `browser` folder. You can always rename, remove or add new files to the file explorer. + +.. image:: images/remix_file_explorer_browser.png + +Note that clearing the browser storage will permanently delete all the solidity files you wrote. To avoid this, you can use Remixd, which enables you to store and sync files in the browser with your local computer (for more information see :doc:`../tutorial_remixd_filesystem`) + +.. image:: images/remix_file_explorer_menu.png + +We will start by reviewing at the icons at the top left - from left to the right: + +Create new File +--------------- + +Creates a new file in the `browser` explorer. + +Add Local File +-------------- + +Allows you to select files from the local file system and import them to the Remix browser storage. + +Publish to Gist +--------------- + +Publishes files from the browser storage to an anonymous public gist. + +Copy to another instance +------------------------ + +Enables you to copy files from the browser storage to another instance (URL) of Remix. + +Connect to Localhost +-------------------- + +Allows to use file located in your file system (see :doc:`../tutorial_remixd_filesystem`). diff --git a/docs_old/images/remix_analysistab.png b/docs_old/images/remix_analysistab.png new file mode 100755 index 0000000000..e43143962c Binary files /dev/null and b/docs_old/images/remix_analysistab.png differ diff --git a/docs_old/images/remix_compiletab.png b/docs_old/images/remix_compiletab.png new file mode 100755 index 0000000000..cd51b11574 Binary files /dev/null and b/docs_old/images/remix_compiletab.png differ diff --git a/docs_old/images/remix_debuggertab.png b/docs_old/images/remix_debuggertab.png new file mode 100755 index 0000000000..104d9744b2 Binary files /dev/null and b/docs_old/images/remix_debuggertab.png differ diff --git a/docs_old/images/remix_editor.png b/docs_old/images/remix_editor.png new file mode 100755 index 0000000000..9260f05fe4 Binary files /dev/null and b/docs_old/images/remix_editor.png differ diff --git a/docs_old/images/remix_file_explorer_browser.png b/docs_old/images/remix_file_explorer_browser.png new file mode 100755 index 0000000000..3f09c0c752 Binary files /dev/null and b/docs_old/images/remix_file_explorer_browser.png differ diff --git a/docs_old/images/remix_file_explorer_menu.png b/docs_old/images/remix_file_explorer_menu.png new file mode 100755 index 0000000000..2140e9b36c Binary files /dev/null and b/docs_old/images/remix_file_explorer_menu.png differ diff --git a/docs_old/images/remix_quickstart_javascriptvm_callinginstance.png b/docs_old/images/remix_quickstart_javascriptvm_callinginstance.png new file mode 100644 index 0000000000..fe11a2cb06 Binary files /dev/null and b/docs_old/images/remix_quickstart_javascriptvm_callinginstance.png differ diff --git a/docs_old/images/remix_quickstart_javascriptvm_creation.png b/docs_old/images/remix_quickstart_javascriptvm_creation.png new file mode 100644 index 0000000000..fc7598aea7 Binary files /dev/null and b/docs_old/images/remix_quickstart_javascriptvm_creation.png differ diff --git a/docs_old/images/remix_quickstart_javascriptvm_creationTransaction.png b/docs_old/images/remix_quickstart_javascriptvm_creationTransaction.png new file mode 100644 index 0000000000..0b27bc105f Binary files /dev/null and b/docs_old/images/remix_quickstart_javascriptvm_creationTransaction.png differ diff --git a/docs_old/images/remix_recorder.png b/docs_old/images/remix_recorder.png new file mode 100644 index 0000000000..1f7abc7363 Binary files /dev/null and b/docs_old/images/remix_recorder.png differ diff --git a/docs_old/images/remix_runtab.png b/docs_old/images/remix_runtab.png new file mode 100755 index 0000000000..c636298493 Binary files /dev/null and b/docs_old/images/remix_runtab.png differ diff --git a/docs_old/images/remix_runtab_example.png b/docs_old/images/remix_runtab_example.png new file mode 100755 index 0000000000..78fba7e994 Binary files /dev/null and b/docs_old/images/remix_runtab_example.png differ diff --git a/docs_old/images/remix_settingstab.png b/docs_old/images/remix_settingstab.png new file mode 100755 index 0000000000..bfd50332e1 Binary files /dev/null and b/docs_old/images/remix_settingstab.png differ diff --git a/docs_old/images/remix_supporttab.png b/docs_old/images/remix_supporttab.png new file mode 100755 index 0000000000..b62ead496b Binary files /dev/null and b/docs_old/images/remix_supporttab.png differ diff --git a/docs_old/images/remix_terminal.png b/docs_old/images/remix_terminal.png new file mode 100755 index 0000000000..3f096a2490 Binary files /dev/null and b/docs_old/images/remix_terminal.png differ diff --git a/docs_old/index.rst b/docs_old/index.rst new file mode 100644 index 0000000000..5534f9ca33 --- /dev/null +++ b/docs_old/index.rst @@ -0,0 +1,60 @@ +Remix - Solidity IDE +==================== + +Remix is an IDE for the smart contract programming language Solidity and has +an integrated debugger and testing environment. + +An up to date online version is available at `remix.ethereum.org `_ + +This page will host documentation and tutorial about features Remix provides. + +Please go to `solidity.readthedocs.io `_ for any information regarding ``Solidity`` + +Overview +-------- + +Remix provides an integrated development environment (IDE) for smart contract development. +It focuses on the development and deployment of Solidity written smart contracts. + +Remix is a good solution if you intend to: + + - Develop smart contracts (remix integrates a solidity editor). + - Debug a smart contract's execution. + - Access the state and properties of already deployed smart contract. + - Debug already committed transaction. + - Analyze solidity code to reduce coding mistakes and to enforce best practices. + - Together with Mist (or any tool which inject web3), Remix can be used to test and debug a Dapp (see :doc:`../tutorial_mist`) + +Developing smart contract requires a deep understanding of the associated Blockchain technology. + +!! Don't use Remix against a production network unless you are completely sure what you are doing !! + +This documentation describes all the features remix provides. +The GUI can be separated in 4 parts. Click on one the link to get more information. + + - :doc:`../file_explorer` + - :doc:`../solidity_editor` + - :doc:`../terminal` + - :doc:`../tabs_panel` + - :doc:`../compile_tab` + - :doc:`../run_tab` + - :doc:`../settings_tab` + - :doc:`../debugger_tab` + - :doc:`../analysis_tab` + - :doc:`../support_tab` + +Quick Start +----------- + +(see :doc:`../quickstart_javascriptvm`) + +Tutorial +-------- + +.. toctree:: + :maxdepth: 1 + + tutorial_remixd_filesystem.rst + tutorial_mist.rst + tutorial_debug.rst + tutorial_import.rst diff --git a/docs_old/make.bat b/docs_old/make.bat new file mode 100755 index 0000000000..963594cac0 --- /dev/null +++ b/docs_old/make.bat @@ -0,0 +1,263 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 1>NUL 2>NUL +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +:sphinx_ok + + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Remix.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Remix.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end diff --git a/docs_old/mist1.png b/docs_old/mist1.png new file mode 100644 index 0000000000..cd2991e2fd Binary files /dev/null and b/docs_old/mist1.png differ diff --git a/docs_old/quickstart_javascriptvm.rst b/docs_old/quickstart_javascriptvm.rst new file mode 100644 index 0000000000..4caff3e1c2 --- /dev/null +++ b/docs_old/quickstart_javascriptvm.rst @@ -0,0 +1,86 @@ +Quick Start using the JavaScript VM +=================================== + +There are 3 type of environments Remix can be plugged to: ``Javascript VM``, ``Injected provider``, or ``Web3 provider``. (see :doc:`../run_tab`) + +Both ``Web3 provider`` and ``Injected provider`` require the use of an external tool. + +The external tool for ``Web3 provider`` is an Ethereum node the tools for ``Injected provider`` are Mist or Metamask. + +The ``JavaScript VM`` mode is convenient because each execution runs in your browser. +Thus reloading the page will restart Remix with an empty state. + +So for performance purposes, it might also be better to use an external node. + +Selecting the VM mode +--------------------- + +Make sure the VM mode is selected. All accounts displayed in ``Accounts`` should have 100 ether. + +Sample contract +--------------- + +.. code-block:: none + + pragma solidity ^0.4.16; + contract testContract { + + uint value; + function testContract(uint _p) { + value = _p; + } + + function setP(uint _n) payable { + value = _n; + } + + function setNP(uint _n) { + value = _n; + } + + function get () constant returns (uint) { + return value; + } + } + +This contract is very basic. The goal is to quickly start to create and to interact with a sample contract. + +Deploying an instance +--------------------- + +The ``Compile tab`` displays information related to the current contract (note that there can be more than one) (see :doc:`../compile_tab`). + +Moving on, in the ``Run tab`` select, ``JavaScript VM`` to specify that you are going to deploy an instance of the contract in the ``JavaScript VM`` state. + +.. image:: images/remix_quickstart_javascriptvm_creation.png + +The constructor of ``testContract`` needs a parameter (of type ``uint``). Give any value and click on ``Create``. + +The transaction which deploys the instance of ``testContract`` is created. + +In a "normal" blockchain, it can take several seconds to execute. This is the time for the transaction to be mined. However, because we are using the ``JavaScript VM``, our execution is immediate. + +The terminal will inform you about the transaction. You can see details there and start debugging. + +The newly created instance is displayed in the ``run tab``. + +.. image:: images/remix_quickstart_javascriptvm_creationTransaction.png + +Interacting with an instance +---------------------------- + +This new instance contains 3 actions which corresponds to the 3 functions (``setP``, ``setPN``, ``get``). +Clicking on ``SetP`` or ``SetPN`` will create a new transaction. + +Note that ``SetP`` is ``payable`` (red action) : it is possible to send value (Ether) to the contract. + +``SetPN`` is not payable (light red action) : it is not possible to send value (Ether) to the contract. + +Clicking on ``get`` will not execute a transaction (blue action). It is not necessary to do so because ``get`` does not modify the state (variable ``value``) of this instance. + +As ``get`` is ``constant`` you can see the return value just below the action. + +.. image:: images/remix_quickstart_javascriptvm_callinginstance.png + + + \ No newline at end of file diff --git a/docs_old/remix1.png b/docs_old/remix1.png new file mode 100644 index 0000000000..7575d34f6c Binary files /dev/null and b/docs_old/remix1.png differ diff --git a/docs_old/remix2.png b/docs_old/remix2.png new file mode 100644 index 0000000000..17a53affd9 Binary files /dev/null and b/docs_old/remix2.png differ diff --git a/docs_old/remix3.png b/docs_old/remix3.png new file mode 100644 index 0000000000..060352a755 Binary files /dev/null and b/docs_old/remix3.png differ diff --git a/docs_old/remix4.png b/docs_old/remix4.png new file mode 100644 index 0000000000..404ed50572 Binary files /dev/null and b/docs_old/remix4.png differ diff --git a/docs_old/remix5.png b/docs_old/remix5.png new file mode 100644 index 0000000000..4297d58adc Binary files /dev/null and b/docs_old/remix5.png differ diff --git a/docs_old/remix_breakpoint.png b/docs_old/remix_breakpoint.png new file mode 100644 index 0000000000..c755f61348 Binary files /dev/null and b/docs_old/remix_breakpoint.png differ diff --git a/docs_old/remix_debuginstructions.png b/docs_old/remix_debuginstructions.png new file mode 100644 index 0000000000..bb36decbaf Binary files /dev/null and b/docs_old/remix_debuginstructions.png differ diff --git a/docs_old/remix_debugtransactioninfo.png b/docs_old/remix_debugtransactioninfo.png new file mode 100644 index 0000000000..92d2e33100 Binary files /dev/null and b/docs_old/remix_debugtransactioninfo.png differ diff --git a/docs_old/remix_enterdebugsession.png b/docs_old/remix_enterdebugsession.png new file mode 100644 index 0000000000..77acc520ee Binary files /dev/null and b/docs_old/remix_enterdebugsession.png differ diff --git a/docs_old/remix_executionexception.png b/docs_old/remix_executionexception.png new file mode 100644 index 0000000000..9f28396b8a Binary files /dev/null and b/docs_old/remix_executionexception.png differ diff --git a/docs_old/remix_navigation.png b/docs_old/remix_navigation.png new file mode 100644 index 0000000000..d4187a462a Binary files /dev/null and b/docs_old/remix_navigation.png differ diff --git a/docs_old/remix_soliditylocals.png b/docs_old/remix_soliditylocals.png new file mode 100644 index 0000000000..bfa627d1a4 Binary files /dev/null and b/docs_old/remix_soliditylocals.png differ diff --git a/docs_old/remix_soliditystate.png b/docs_old/remix_soliditystate.png new file mode 100644 index 0000000000..789df079e2 Binary files /dev/null and b/docs_old/remix_soliditystate.png differ diff --git a/docs_old/remix_startdebugging.png b/docs_old/remix_startdebugging.png new file mode 100644 index 0000000000..bf5287a5c3 Binary files /dev/null and b/docs_old/remix_startdebugging.png differ diff --git a/docs_old/remix_stepdetail.png b/docs_old/remix_stepdetail.png new file mode 100644 index 0000000000..efde21304e Binary files /dev/null and b/docs_old/remix_stepdetail.png differ diff --git a/docs_old/remix_valueinput.png b/docs_old/remix_valueinput.png new file mode 100644 index 0000000000..381ebfc86a Binary files /dev/null and b/docs_old/remix_valueinput.png differ diff --git a/docs_old/remixd_alert.png b/docs_old/remixd_alert.png new file mode 100644 index 0000000000..d64e6f7214 Binary files /dev/null and b/docs_old/remixd_alert.png differ diff --git a/docs_old/remixd_connectionok.png b/docs_old/remixd_connectionok.png new file mode 100644 index 0000000000..e8381f55df Binary files /dev/null and b/docs_old/remixd_connectionok.png differ diff --git a/docs_old/remixd_noconnection.png b/docs_old/remixd_noconnection.png new file mode 100644 index 0000000000..b6cd6f5b40 Binary files /dev/null and b/docs_old/remixd_noconnection.png differ diff --git a/docs_old/run_tab.rst b/docs_old/run_tab.rst new file mode 100644 index 0000000000..516ef35a5c --- /dev/null +++ b/docs_old/run_tab.rst @@ -0,0 +1,251 @@ +Running transactions +==================== + +The Run tab is an important section of Remix. It allows you to send transactions to the current environment. + +.. image:: images/remix_runtab.png + +Run Setup +--------- + +The following settings allow you to directly influence the transaction execution: + + - Environment: + - ``JavaScript VM``: All the transactions will be executed in a sandbox blockchain in the browser. This means nothing will be persisted and a page reload will restart a new blockchain from scratch, the old one will not be saved. + + - ``Injected Provider``: Remix will connect to an injected web3 provider. ``Mist`` and ``Metamask`` are example of providers that inject web3, thus can be used with this option. + + - ``Web3 Provider``: Remix will connect to a remote node. You will need to provide the URL address to the selected provider: geth, parity or any Ethereum client. + + - Account: the list of accounts associated with the current environment (and their associated balances). + - Gas Limit: the maximum amount of gas that can be set for all the transactions created in Remix. + - Value: the amount of value for the next created transaction (this value is always reset to 0 after each transaction execution). + + .. image:: images/remix_runtab_example.png + +Initiate Instance +----------------- + +This section contains the list of compiled contracts and 2 actions: + +- ``At Address`` assumes the given address is an instance of the selected contract. It is then possible to interact with an already deployed contract. There's no check at this point, so be careful when using this feature, and be sure you trust the contract at that address. + +- ``Create`` send a transaction that deploys the selected contract. When the transaction is mined, the newly created instance will be added (this might take several seconds). Note that if the ``constructor`` has parameters, you need to specify them. + +Pending Instances +----------------- + +Validating a transaction take several seconds. During this time, the GUI shows it in a pending mode. When transaction is mined the number of pending transactions is updated +and the transaction is added to the log (see :doc:`../terminal`) + +Instance List +------------- + +This section contains a list of instances to interact with. + +Several cases apply: + - The called function is declared as ``constant`` or ``pure`` in Solidity. The action has a blue background, clicking it does not create a new transaction. Clicking it is not necessary because there are not state changes - but it will update the return value of the function. + + - The called function has no special keywords. The action has a light red background, clicking on does create a new transaction. But this transaction cannot accept any amount of Ether. + + - The called function is declared as ``payable`` in Solidity. The action has a red background, clicking it does create a new transaction and this transaction can accept value. + +For more information about Solidity modifier, see `Solidity modifier `_ . + +If a function requires input parameters, it is required to specify them. + +USING ABI +--------- + +Using ``Create`` or ``At Address`` is a classic use case of Remix. It is possible though to interact with a contract by using its ABI. The ABI is a JSON array which describe its interface. + +To interact with a contract using the ABI, create a new file in Remix with extension ``*.abi`` and copy the ABI content to it. +Then in the input next to ``At Address``, put the Address of the contract you want to interact with. Click on ``At Address``, +a new "connection" with the contract will popup below. + +USING THE RECORDER +------------------ + +The Recorder allows to save a bunch of transactions in a JSON file and rerun them later either in the same environment or in another. + +Saving to JSON allows to easily check the transaction list, tweak input parameters, change linked library, etc... + +We can find many use cases for the recorder, for instance: + - After having coded and tested contracts in a constrained environment (like the JavaScript VM), it could be interesting to redeploy them easily in a more persisted environment (like a Geth node) in order to check whether everything behaves normally in a classic environment. + - Deploying contract does often require more than creating one transaction. + - Working in a dev environment does often require to setup the state in a first place. + + +.. image:: images/remix_recorder.png + +Saving a record ends up with the creation of this type of content (see below): + +In that specific record, 3 transactions are executed: + +The first corresponds to the deployment of the lib ``testLib``. + +The second corresponds to the deployment of the contract ``test``, the first parameter of the constructor is set to 11. +That contract depends on a library. The linkage is done using the property ``linkReferences``. +In that case we use the addres of the previously created library : ``created{1512830014773}``. the number is +the id (timestamp) of the transaction that leads to the creation of the library. + +The third parameter corresponds to the call to te function ``set`` of the contract ``test`` (the property to is set to: ``created{1512830015080}``) . Input parameters are ``1`` and ``0xca35b7d915458ef540ade6068dfe2f44e8fa733c`` + +all these transactions are created using the value of the accounts ``account{0}``. + +.. code-block:: none + + { + "accounts": { + "account{0}": "0xca35b7d915458ef540ade6068dfe2f44e8fa733c" + }, + "linkReferences": { + "testLib": "created{1512830014773}" + }, + "transactions": [ + { + "timestamp": 1512830014773, + "record": { + "value": "0", + "parameters": [], + "abi": "0xbc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a", + "contractName": "testLib", + "bytecode": "60606040523415600e57600080fd5b60968061001c6000396000f300606060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680636d4ce63c146044575b600080fd5b604a6060565b6040518082815260200191505060405180910390f35b6000610d809050905600a165627a7a7230582022d123b15248b8176151f8d45c2dc132063bcc9bb8d5cd652aea7efae362c8050029", + "linkReferences": {}, + "type": "constructor", + "from": "account{0}" + } + }, + { + "timestamp": 1512830015080, + "record": { + "value": "100", + "parameters": [ + 11 + ], + "abi": "0xc41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec", + "contractName": "test", + "bytecode": "60606040526040516020806102b183398101604052808051906020019091905050806000819055505061027a806100376000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632f30c6f61461006757806338cc48311461009e57806362738998146100f357806387cc10e11461011c575b600080fd5b61009c600480803590602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610145565b005b34156100a957600080fd5b6100b1610191565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34156100fe57600080fd5b6101066101bb565b6040518082815260200191505060405180910390f35b341561012757600080fd5b61012f6101c4565b6040518082815260200191505060405180910390f35b8160008190555080600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505050565b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b60008054905090565b600073__browser/ballot.sol:testLib____________636d4ce63c6000604051602001526040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160206040518083038186803b151561022e57600080fd5b6102c65a03f4151561023f57600080fd5b505050604051805190509050905600a165627a7a72305820e0b2510bb2890a0334bfe5613d96db3e72442e63b514cdeaee8fc2c6bbd19d3a0029", + "linkReferences": { + "browser/ballot.sol": { + "testLib": [ + { + "length": 20, + "start": 511 + } + ] + } + }, + "name": "", + "type": "constructor", + "from": "account{0}" + } + }, + { + "timestamp": 1512830034180, + "record": { + "value": "1000000000000000000", + "parameters": [ + 1, + "0xca35b7d915458ef540ade6068dfe2f44e8fa733c" + ], + "to": "created{1512830015080}", + "abi": "0xc41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec", + "name": "set", + "type": "function", + "from": "account{0}" + } + } + ], + "abis": { + "0xbc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98a": [ + { + "constant": true, + "inputs": [], + "name": "get", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + } + ], + "0xc41589e7559804ea4a2080dad19d876a024ccb05117835447d72ce08c1d020ec": [ + { + "constant": true, + "inputs": [], + "name": "getInt", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getFromLib", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getAddress", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_t", + "type": "uint256" + }, + { + "name": "_add", + "type": "address" + } + ], + "name": "set", + "outputs": [], + "payable": true, + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "name": "_r", + "type": "uint256" + } + ], + "payable": true, + "stateMutability": "payable", + "type": "constructor" + } + ] + } + } \ No newline at end of file diff --git a/docs_old/settings_tab.rst b/docs_old/settings_tab.rst new file mode 100644 index 0000000000..cc32a9729f --- /dev/null +++ b/docs_old/settings_tab.rst @@ -0,0 +1,13 @@ +Settings +======== + +This section displays the current compiler version and allows one to change to another version. + +.. image:: images/remix_settingstab.png + +Settings, available here: + + - Text wrap: controls if the text in the editor should be wrapped. + - Enable optimization: defines if the compiler should enable optimization during compilation. Enabling this option saves execution gas. + It is useful to enable optimization for contracts ready to be deployed in production + but could lead to some inconsistencies when debugging such a contract. diff --git a/docs_old/solidity_editor.rst b/docs_old/solidity_editor.rst new file mode 100644 index 0000000000..cc1928664f --- /dev/null +++ b/docs_old/solidity_editor.rst @@ -0,0 +1,13 @@ +Solidity Editor +=============== + +The Remix editor recompiles the code each time the current file is changed or another file is selected. It also provides syntax highlighting mapped to solidity keywords. + +.. image:: images/remix_editor.png + +Here's the list of some important features: + +- It display opened files as tabs. +- Compilation Warning and Error are displayed in the gutter +- Remix saves the current file continuously (5s after the last changes) +- +/- on the top left corner enable you to increase/decrease the font size of the editor diff --git a/docs_old/support_tab.rst b/docs_old/support_tab.rst new file mode 100644 index 0000000000..fcc1199740 --- /dev/null +++ b/docs_old/support_tab.rst @@ -0,0 +1,6 @@ +Support +======= + +This section provides a link to Remix Issues where users can report a bug or suggest a feature, as well as providing other useful links. It also displays a `Remix developers' channel `_ + +.. image:: images/remix_supporttab.png diff --git a/docs_old/tabs_panel.rst b/docs_old/tabs_panel.rst new file mode 100644 index 0000000000..dcb4e0f3a8 --- /dev/null +++ b/docs_old/tabs_panel.rst @@ -0,0 +1,9 @@ +Tabs Panel +========== + +- :doc:`../compile_tab` +- :doc:`../run_tab` +- :doc:`../settings_tab` +- :doc:`../debugger_tab` +- :doc:`../analysis_tab` +- :doc:`../support_tab` diff --git a/docs_old/terminal.rst b/docs_old/terminal.rst new file mode 100644 index 0000000000..46e752f5bd --- /dev/null +++ b/docs_old/terminal.rst @@ -0,0 +1,14 @@ +Terminal +======== + +.. image:: images/remix_terminal.png + +Features, available in the terminal: + +- It integrates a JavaScript interpreter and the ``web3`` object. It enables the execution of the JavaScript script which interacts with the current context. (note that ``web3`` is only available if the ``web provider`` or ``injected provider`` mode is selected). + +- It displays important actions made while interacting with the Remix IDE (i.e. sending a new transaction). + +- It displays transactions that are mined in the current context. You can choose to display all transactions or only transactions that refers to the contracts Remix knows (e.g transaction created from the Remix IDE). + +- It allows searching for the data and clearing the logs from the terminal. diff --git a/docs_old/tuto_basicimport.png b/docs_old/tuto_basicimport.png new file mode 100644 index 0000000000..81a8009336 Binary files /dev/null and b/docs_old/tuto_basicimport.png differ diff --git a/docs_old/tuto_importgit.png b/docs_old/tuto_importgit.png new file mode 100644 index 0000000000..022eb4b011 Binary files /dev/null and b/docs_old/tuto_importgit.png differ diff --git a/docs_old/tuto_importswarm.png b/docs_old/tuto_importswarm.png new file mode 100644 index 0000000000..01fba3e646 Binary files /dev/null and b/docs_old/tuto_importswarm.png differ diff --git a/docs_old/tutorial_debug.rst b/docs_old/tutorial_debug.rst new file mode 100644 index 0000000000..8030187f5c --- /dev/null +++ b/docs_old/tutorial_debug.rst @@ -0,0 +1,204 @@ +Debugging a Transaction +======================= + +.. _tutorial-debug: + +The goal of this tutorial is to explain how to debug transaction using Remix. + +Start debugging +--------------- + +There are two different ways to start debugging, each way correspond to a different use case. + +From the Transaction GUI +~~~~~~~~~~~~~~~~~~~~~~~~ + +We will not explain in detail here how to write or deploy contract. +Let us start with a basic contract (replace this one by your's): + +.. code-block:: none + + contract Donation { + address owner; + event fundMoved(address _to, uint _amount); + modifier onlyowner { if (msg.sender == owner) _; } + address[] _giver; + uint[] _values; + + function Donation() { + owner = msg.sender; + } + + function donate() payable { + addGiver(msg.value); + } + + function moveFund(address _to, uint _amount) onlyowner { + uint balance = this.balance; + uint amount = _amount; + if (_amount <= this.balance) { + if (_to.send(this.balance)) { + fundMoved(_to, _amount); + } else { + throw; + } + } else { + throw; + } + } + + function addGiver(uint _amount) internal { + _giver.push(msg.sender); + _values.push(_amount); + } + } + +For the purpose of this tutorial, we will run the ``JavaScript VM`` (that's the default mode when you don't use Remix with Mist or Metamask). This simulates a custom blockchain. You could do the same using a proper backend node. + +Now, let's deploy the contract: + +Right panel / Red button ``Create`` + +.. image:: remix1.png + +Then we should call the ``Donate`` function (that will send Ether to the contract). + +Let's set the amount of Ether: + +Right panel / second tab from the left - fill in the ´´value´´ input (´1 ether´ for instance) + +.. image:: remix_valueinput.png + +Then click on ``Donate``. As we are using the ``JavaScript VM``, everything goes almost instantly. + +Remix displays also some information related to each transaction result. In the terminal, the transaction is logged and you can start debugging it. + +.. image:: remix_startdebugging.png + +From the Debugger +~~~~~~~~~~~~~~~~~ + +The debugger can be found in the right panel / 5th tab from the left. + +You can start a debug session by providing either a ``transaction hash`` or a ``block number`` and ``transaction index``. + +.. image:: remix3.png + +Click the ``play`` button to start debugging. + +Using the debugger +------------------ + +The debugger allows one to see detailed informations about the transaction's execution. It uses the editor (left panel) to display the location +in the source code where the current execution is. + +The transaction panel displays basic information about the current transaction. + +.. image:: remix_debugtransactioninfo.png + +The navigation part contains a slider and buttons that can be used to step through the transaction execution. + +From the left to the right: + +step over back, step into back, step into forward, step over forward, jump out (jump out of the current call), jump to the previous breakpoint, jump to the next breakpoint. + +.. image:: remix_navigation.png + +11 panels give detailed information about the execution: + +Instructions +~~~~~~~~~~~~ + +.. image:: remix_debuginstructions.png + +The Instructions panel displays the bytecode of the current executing contract- with the current step highlighted. + +Important note: +When this panel is hidden, the slider will have a courser granularity and only stop at expression boundaries, even if they are compiled into multiple EVM instructions. +When the panel is displayed, it will be possible to step over every instruction, even those that refers to the same expression. + +Solidity Locals +~~~~~~~~~~~~~~~ + +.. image:: remix_soliditylocals.png + +The Solidity Locals panel displays local variables associated with the current context. + +Solidity State +~~~~~~~~~~~~~~ + +.. image:: remix_soliditystate.png + +The Solidity State panel displays state variables of the current executing contract. + +Low level panels +~~~~~~~~~~~~~~~~ + +These panels display low level informations about the execution: + + - Stack + - Storages Changes + - Memory + - Call Data + - Call Stack + - Return Value (only if the current step is a RETURN opcode) + - Full Storages Changes (only at the end of the execution - display every storage change of every modified contract) + +Reverted Transaction +~~~~~~~~~~~~~~~~~~~~ + +A transaction could be reverted (either because of out of gas exception, Solidity ``throw`` or low level exception). + +In that case it is important to be aware of the exception and to locate where the exception is in the source code. + +Remix will warn you when the execution throws an exception. The ``warning`` button will jump to the last opcode before the exception happened. + +.. image:: remix_executionexception.png + +Breakpoints +~~~~~~~~~~~ + +The two last buttons from the navigation area are used to jump either back to the previous breakpoint or forward to the next breakpoint. + +Breakpoints can be added and removed by clicking on the line number. + +.. image:: remix_breakpoint.png + +When a debug session is started, the execution will jump to the first encountered breakpoint. + +Important note: +If you add a breakpoint to a line that declares a variable, it might be triggered twice: Once for initializing the +variable to zero and second time for assigning the actual value. +As an example, assume you are debugging the following contract: + +.. code-block:: none + + contract ctr { + function hid () { + uint p = 45; + uint m; + m = 89; + uint l = 34; + } + } + +And let's says that breakpoints are set for the lines + +``uint p = 45;`` + +``m = 89;`` + +``uint l = 34;`` + + +then clicking on ``Jump to next breakpoint`` will stop at the following lines in the given order: + + ``uint p = 45;`` (declaration of p) + + ``uint l = 34;`` (declaration of l) + + ``uint p = 45;`` (45 assigned to p) + + ``m = 89;`` (89 assigned to m) + + ``uint l = 34;`` (34 assigned to l) diff --git a/docs_old/tutorial_import.rst b/docs_old/tutorial_import.rst new file mode 100644 index 0000000000..2a74046e4c --- /dev/null +++ b/docs_old/tutorial_import.rst @@ -0,0 +1,35 @@ +Importing Source Files in Solidity +================================== + +.. _tutorial-import: + +This tutorial will show you how to import local and external files. + +The compilation result will also contain contracts implemented in the imported files. + +For a detailed explanation of the ``import`` keyword see the ``Solidity`` +`documentation `_ + +Importing a local file +---------------------- + +Other files in Remix can be imported just by specifying their path. +Please use `./` for relative paths to increase portability. + +.. image:: tuto_basicimport.png + +Importing from Github +---------------------- + +It is possible to import files directly from github with URLs like +``https://github.com///``. + +.. image:: tuto_importgit.png + +Importing from Swarm +-------------------- + +Files can be imported using all URLs supported by swarm. If you do not have a swarm +node, swarm-gateways.net will be used instead. + +.. image:: tuto_importswarm.png diff --git a/docs_old/tutorial_mist.rst b/docs_old/tutorial_mist.rst new file mode 100644 index 0000000000..526882d7fc --- /dev/null +++ b/docs_old/tutorial_mist.rst @@ -0,0 +1,289 @@ +Debugging a Dapp using Remix - Mist - Geth +=================================================== + +.. _tutorial-mist-geth: + +The ultimate goal of this tutorial is to debug transactions that have been created by a dapp front end. + +It is easy in Remix to debug a transaction created from its own GUI. However, setting up an environment that allows you to +debug transactions created outside of Remix, require a bit more of complexity. + +We will need four tools for that : + + - Geth - this is the center piece and provides the blockchain environment. We will basically run geth in a `dev` mode. + + - Mist - this is the Ethereum dapp browser. We will use it to browse our front end. + + - Remix - this is the Ethereum IDE. We will use it to develop our Solidity contract. + + - Any code editor you want - in order to write your front end :) + +Install the environment +----------------------- + +Install Mist +~~~~~~~~~~~~ + +Mist is the Ethereum browser and the entry point of a Dapp. + +Please download `the latest version `_ (at least 0.8.9). + +Basically we will always run our front end in Mist (note that it is also possible to use `Metamask `_). + +Install Geth +~~~~~~~~~~~~ + +`Geth `_ is the official Ethereum client. + +Running the environment +----------------------- + +Run Geth +~~~~~~~~ + +We will run a test node. This node will have a new empty state and will not be synced to the main or ropsten network. + +:: + + geth --ipcpath /geth.ipc --datadir --dev console + + +```` is the folder where keys and chain data will be stored. + +``--ipcpath`` defines the end point that other apps (like Mist) use to talk to geth. + +``--datadir`` specifies the data directory. + +``--dev`` sets the node into private chain mode and adds some debugging flags. + +Then we need to create accounts and mine a bit to generate some Ether: + +:: + + // from the geth console : + personal.newAccount() // You can execute this command several time if you need more than one account. + miner.start() // generate some Ether. + miner.stop() // stop mining after 30s-60s - we could also keep mining. + +Next time we run Geth, we will only need to mine transactions (no need to recreate account). + +Run Mist +~~~~~~~~ + +If we run Mist without any argument, its internal Geth node will run. As we have our own we need to specify the ipc path of the node installed above. + +:: + + mist --rpc /geth.ipc + +(yes the option is --rpc) + +Once Mist is started, verify that it is connected to the test node (that's very important!!). + +On the bottom left, check that the network is ``Private-net`` and that the block number is the same as reported by the test node we are currently running. Run the following command in the Geth Console to check: `web3.eth.blockNumber`. + +.. image:: mist1.png + +Clicking on `Wallet` will allow you to send transactions and check account balances (if you are currently mining you should see the balance increasing). + +Starting Remix +~~~~~~~~~~~~~~ + +In Mist click on ``Develop`` / ``Open Remix IDE`` + +Remix will open in a new window. If this is the first time it is run, the ``Ballot`` contract will be loaded. + +Now, we need to check if Remix is connected to Mist: + +Right panel / third tab from the left, ``Injected Provider`` should be checked. + +.. image:: remix4.png + +Right panel / second tab from the left, ``Transaction Origin`` should contain accounts we have previously created in Geth. + +.. image:: remix5.png + +Developing contract / front end +-------------------------------- + +Donation contract - Dapp Back end +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here is a sample solidity contract. + +Copy and paste the following inside remix: + +.. code-block:: none + + contract Donation { + address owner; + event fundMoved(address _to, uint _amount); + modifier onlyowner { if (msg.sender == owner) _; } + address[] _giver; + uint[] _values; + + function Donation() { + owner = msg.sender; + } + + function donate() payable { + addGiver(msg.value); + } + + function moveFund(address _to, uint _amount) onlyowner { + uint balance = this.balance; + uint amount = _amount; + if (_amount <= this.balance) { + if (_to.send(this.balance)) { + fundMoved(_to, _amount); + } else { + throw; + } + } else { + throw; + } + } + + function addGiver(uint _amount) internal { + _giver.push(msg.sender); + _values.push(_amount); + } + } + + +Dapp Front end +~~~~~~~~~~~~~~ + +and here is the front end: + +.. code-block:: none + +
+
Donation Contract
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+ + + +I would suggest serving this file using ``http-serve``, but you can use any web server you like. + +Example: Dapp Front End https://github.com/ltfschoen/dapp_front_end + +Important notice ! +~~~~~~~~~~~~~~~~~~ + +The variable ``contractspec`` contains the abi of the ``donation`` contract. This means that if you change something in the contract interface (function names, parameters, ...) +you need to copy the new abi from remix to the front end. + +Deploying +--------- + +Right panel / Red button ``Create`` + +.. image:: remix1.png + +This creates a new transaction that deploys the ``Donation`` contract (Mist will ask for the usual passphrase check). + +Wait for the transaction to be mined (don't forget to activate mining ``miner.start()``). +Once this is done, you can use it by executing the ``moveFund`` and ``donate`` function. But this is not what we +want to achieve. We want to run and debug those functions from the front end. + +Remix also display the address of the contract. Save it, we'll need this address later. + +.. image:: remix2.png + +Debugging +--------- + +From Mist, browse the above front end. +In the first field, paste the address of the newly created contract. Now, let's call the first function (label ``give``). + +You will need an account and a value. + +The account could be any account that is declared in the Wallet section of Mist. This is the sender of the transaction that we are going to create. +The value should be no more than the actual balance of the account - the unit is in `wei`, so just put ``100`` (100 wei), that should be fine. + +Click on ``Give`` and wait for the transaction to be mined. + +The HTML block with id ``log`` is filled by all the transactions created from the front end. +It was easier for the purpose of this tutorial to just log transactions in a div but you can have your own logging mechanism. + +There is only one field that we need, this is the ``transactionHash``. + +Copy it and switch to Remix. On the right side, the fifth panel shows a small "bug" icon, that is the debugger. + +Paste the hash into the transaction field and click on the ``play`` button. + +.. image:: remix3.png + +You are now entering a debug session for the call to ``donate``. + +Debugging in Remix is easier than with common tools like gdb because you can freely move in time. +Use the slider to change the current step and click on the panels below to expand them and explore the curret state, local variables, etc. +There are also breakpoints to move between sections of the code quickly, but more on all that later. + + +At the time of this writing, there is an issue that could break the contract creation. +The a workaround for that at https://github.com/ethereum/go-ethereum/issues/3653 . +Please follow the workaround or wait for this issue to be closed. + +Also, although retrieving a contract's storage when Remix is using the JavaScript VM is working well, +there is still work to be done when Remix is using eth or geth as backend. diff --git a/docs_old/tutorial_remixd_filesystem.rst b/docs_old/tutorial_remixd_filesystem.rst new file mode 100644 index 0000000000..d045279307 --- /dev/null +++ b/docs_old/tutorial_remixd_filesystem.rst @@ -0,0 +1,37 @@ +Accessing a shared folder in Remix IDE using Remixd +=================================================== + +.. _tutorial-remixd-filesystem: + +Remixd is an npm module. Its purpose is to give the remix web application access to a folder from your local computer. + +The code of Remixd can be checked out `here `_ . + +Remixd can be globally installed using the following command: ``npm install -g remixd``. + +Then ``remixd -s `` will start Remixd and share the given folder. + +The folder is shared using a websocket connection between ``Remix IDE`` and ``Remixd``. + +Be sure the user executing Remix has read/write permission on the folder. + +.. warning:: + Remixd provides full read and write access to the given folder for any application that can access the TCP port 65520 on your local host. + +From ``Remix IDE``, you will need to activate the connection. + +Click on the ``localhost connection`` icon: + +.. image:: remixd_noconnection.png + +A modal dialog will ask confirmation + +.. image:: remixd_alert.png + +Accepting this dialog will start a session. Once the connection is made, the status will update and the connection icon should shows up in green. + +Hovering the icon will give more connection status information. + +At this point if the connection is successful, the shared folder will be available in the file explorer. + +.. image:: remixd_connectionok.png diff --git a/lerna.json b/lerna.json new file mode 100644 index 0000000000..6a1f75ae93 --- /dev/null +++ b/lerna.json @@ -0,0 +1,11 @@ +{ + "lerna": "2.10.2", + "packages": [ + "remix-debug", + "remix-debugger", + "remix-lib", + "remix-solidity", + "remix-analyzer" + ], + "version": "independent" +} diff --git a/package.json b/package.json index d3b2bd054d..6caf32cecb 100644 --- a/package.json +++ b/package.json @@ -1,61 +1,9 @@ { - "name": "ethereum-remix", - "version": "0.0.2-alpha.0.0.7", - "description": "Ethereum IDE and tools for the web", - "contributors": [ - { - "name": "Yann Levreau", - "email": "yann@ethdev.com" - }, - { - "name": "Liana Husikyan", - "email": "liana@ethdev.com" - } - ], - "main": "./src/index.js", - "dependencies": { - "which": "^1.2.10" - }, - "devDependencies": { - "browserify": "^13.0.1", - "ethereumjs-util": "^4.5.0", - "http-server": "^0.9.0", - "nightwatch": "^0.9.5", - "standard": "^7.0.1", - "standard-reporter": "^1.0.5", - "tape": "^4.6.0", - "web3": "^0.15.3", - "yo-yo": "^1.2.1", - "yo-yoify": "^3.1.0" - }, - "scripts": { - "start_node": "./runNode.sh", - "start_eth": "eth -j --rpccorsdomain '*'", - "start_geth": "geth --rpc --rpcapi 'web3,eth,debug' --rpcport 8545 --rpccorsdomain '*'", - "build": "mkdir -p build; browserify src/index.js -g yo-yoify -o build/app.js", - "test": "standard && tape ./test/tests.js", - "serve": "http-server .", - "nightwatch_local": "nightwatch --config nightwatch.js --env local", - "nightwatch_remote_firefox": "nightwatch --config nightwatch.js --env default", - "nightwatch_remote_chrome": "nightwatch --config nightwatch.js --env chrome", - "nightwatch_remote_safari": "nightwatch --config nightwatch.js --env safari", - "nightwatch_remote_ie": "nightwatch --config nightwatch.js --env ie" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/ethereum/remix.git" - }, - "author": "cpp-ethereum team", - "license": "MIT", - "bugs": { - "url": "https://github.com/ethereum/remix/issues" - }, - "homepage": "https://github.com/ethereum/remix#readme", - "standard": { - "ignore": [ - "node_modules/*", - "build/*", - "test/resources/*" - ] - } + "devDependencies": { + "lerna": "^2.10.2" + }, + "scripts": { + "bootstrap": "lerna bootstrap", + "publish": "lerna publish" + } } diff --git a/remix-analyzer/index.js b/remix-analyzer/index.js new file mode 100644 index 0000000000..3fd9c0fd4a --- /dev/null +++ b/remix-analyzer/index.js @@ -0,0 +1,5 @@ +var CodeAnalysis = require('./src/analysis/staticAnalysisRunner') + +module.exports = { + CodeAnalysis: CodeAnalysis +} diff --git a/remix-analyzer/package.json b/remix-analyzer/package.json new file mode 100644 index 0000000000..35815bbc6d --- /dev/null +++ b/remix-analyzer/package.json @@ -0,0 +1,42 @@ +{ + "name": "remix-analyzer", + "version": "0.1.0", + "description": "Remix Analyzer", + "main": "./index.js", + "contributors": [ + { + "name": "Alex Beregszaszi", + "email": "alex@rtfs.hu" + }, + { + "name": "Iuri Matias", + "email": "iuri@ethereum.org" + }, + { + "name": "Yann Levreau", + "email": "yann@ethdev.com" + } + ], + "dependencies": { + "babel-eslint": "^7.1.1", + "babel-plugin-transform-object-assign": "^6.22.0", + "remix-lib": "^0.2.9", + "babel-preset-es2015": "^6.24.0", + "solc": "^0.4.24", + "standard": "^7.0.1", + "tape": "^4.6.0" + }, + "scripts": { + "test": "standard && tape ./test/tests.js" + }, + "standard": { + "ignore": [ + "node_modules/*", + "soljson.js" + ], + "parser": "babel-eslint" + }, + "author": "Remix Team", + "license": "MIT", + "homepage": "https://github.com/ethereum/remix#readme" +} diff --git a/remix-analyzer/src/analysis/modules/abstractAstView.js b/remix-analyzer/src/analysis/modules/abstractAstView.js new file mode 100644 index 0000000000..ea28f77b32 --- /dev/null +++ b/remix-analyzer/src/analysis/modules/abstractAstView.js @@ -0,0 +1,177 @@ +var common = require('./staticAnalysisCommon') +var AstWalker = require('remix-lib').AstWalker + +function abstractAstView () { + this.contracts = [] + this.currentContractIndex = null + this.currentFunctionIndex = null + this.currentModifierIndex = null + this.isFunctionNotModifier = false + /* + file1: contract c{} + file2: import "file1" as x; contract c{} + therefore we have two contracts with the same name c. At the moment this is not handled because alias name "x" is not + available in the current AST implementation thus can not be resolved. + Additionally the fullQuallified function names e.g. [contractName].[functionName](param1Type, param2Type, ... ) must be prefixed to + fully support this and when inheritance is resolved it must include alias resolving e.g x.c = file1.c + */ + this.multipleContractsWithSameName = false +} + +/** + * Builds a higher level AST view. I creates a list with each contract as an object in it. + * Example contractsOut: + * + * { + * "node": {}, // actual AST Node of the contract + * "functions": [ + * { + * "node": {}, // actual AST Node of the function + * "relevantNodes": [], // AST nodes in the function that are relevant for the anlysis of this function + * "modifierInvocations": [], // Modifier invocation AST nodes that are applied on this function + * "localVariables": [], // Local variable declaration nodes + * "parameters": [] // Parameter types of the function in order of definition + * } + * ], + * "modifiers": [], // Modifiers definded by the contract, format similar to functions + * "inheritsFrom": [], // Names of contract this one inherits from in order of definition + * "stateVariables": [] // AST nodes of all State variables + * } + * + * @relevantNodeFilter {ASTNode -> bool} function that selects relevant ast nodes for analysis on function level. + * @contractsOut {list} return list for high level AST view + * @return {ASTNode -> void} returns a function that can be used as visit function for static analysis modules, to build up a higher level AST view for further analysis. + */ +abstractAstView.prototype.build_visit = function (relevantNodeFilter) { + var that = this + return function (node) { + if (common.isContractDefinition(node)) { + setCurrentContract(that, { + node: node, + functions: [], + relevantNodes: [], + modifiers: [], + inheritsFrom: [], + stateVariables: common.getStateVariableDeclarationsFormContractNode(node) + }) + } else if (common.isInheritanceSpecifier(node)) { + var currentContract = getCurrentContract(that) + var inheritsFromName = common.getInheritsFromName(node) + currentContract.inheritsFrom.push(inheritsFromName) + } else if (common.isFunctionDefinition(node)) { + setCurrentFunction(that, { + node: node, + relevantNodes: [], + modifierInvocations: [], + localVariables: getLocalVariables(node), + parameters: getLocalParameters(node), + returns: getReturnParameters(node) + }) + // push back relevant nodes to their the current fn if any + getCurrentContract(that).relevantNodes.map((item) => { + if (item.referencedDeclaration === node.id) { + getCurrentFunction(that).relevantNodes.push(item.node) + } + }) + } else if (common.isModifierDefinition(node)) { + setCurrentModifier(that, { + node: node, + relevantNodes: [], + localVariables: getLocalVariables(node), + parameters: getLocalParameters(node) + }) + } else if (common.isModifierInvocation(node)) { + if (!that.isFunctionNotModifier) throw new Error('abstractAstView.js: Found modifier invocation outside of function scope.') + getCurrentFunction(that).modifierInvocations.push(node) + } else if (relevantNodeFilter(node)) { + var scope = (that.isFunctionNotModifier) ? getCurrentFunction(that) : getCurrentModifier(that) + if (scope) { + scope.relevantNodes.push(node) + } else { + scope = getCurrentContract(that) // if we are not in a function scope, add the node to the contract scope + if (scope && node.children[0] && node.children[0].attributes && node.children[0].attributes.referencedDeclaration) { + scope.relevantNodes.push({ referencedDeclaration: node.children[0].attributes.referencedDeclaration, node: node }) + } + } + } + } +} + +abstractAstView.prototype.build_report = function (wrap) { + var that = this + return function (compilationResult) { + resolveStateVariablesInHierarchy(that.contracts) + return wrap(that.contracts, that.multipleContractsWithSameName) + } +} + +function resolveStateVariablesInHierarchy (contracts) { + contracts.map((c) => { + resolveStateVariablesInHierarchyForContract(c, contracts) + }) +} +function resolveStateVariablesInHierarchyForContract (currentContract, contracts) { + currentContract.inheritsFrom.map((inheritsFromName) => { + // add variables from inherited contracts + var inheritsFrom = contracts.find((contract) => common.getContractName(contract.node) === inheritsFromName) + if (inheritsFrom) { + currentContract.stateVariables = currentContract.stateVariables.concat(inheritsFrom.stateVariables) + } else { + console.log('abstractAstView.js: could not find contract defintion inherited from ' + inheritsFromName) + } + }) +} +function setCurrentContract (that, contract) { + var name = common.getContractName(contract.node) + if (that.contracts.map((c) => common.getContractName(c.node)).filter((n) => n === name).length > 0) { + console.log('abstractAstView.js: two or more contracts with the same name dectected, import aliases not supported at the moment') + that.multipleContractsWithSameName = true + } + that.currentContractIndex = (that.contracts.push(contract) - 1) +} + +function setCurrentFunction (that, func) { + that.isFunctionNotModifier = true + that.currentFunctionIndex = (getCurrentContract(that).functions.push(func) - 1) +} + +function setCurrentModifier (that, modi) { + that.isFunctionNotModifier = false + that.currentModifierIndex = (getCurrentContract(that).modifiers.push(modi) - 1) +} + +function getCurrentContract (that) { + return that.contracts[that.currentContractIndex] +} + +function getCurrentFunction (that) { + return getCurrentContract(that).functions[that.currentFunctionIndex] +} + +function getCurrentModifier (that) { + return getCurrentContract(that).modifiers[that.currentModifierIndex] +} + +function getLocalParameters (funcNode) { + return getLocalVariables(common.getFunctionOrModifierDefinitionParameterPart(funcNode)).map(common.getType) +} + +function getReturnParameters (funcNode) { + return getLocalVariables(common.getFunctionOrModifierDefinitionReturnParameterPart(funcNode)).map((n) => { + return { + type: common.getType(n), + name: common.getDeclaredVariableName(n) + } + }) +} + +function getLocalVariables (funcNode) { + var locals = [] + new AstWalker().walk(funcNode, {'*': function (node) { + if (common.isVariableDeclaration(node)) locals.push(node) + return true + }}) + return locals +} + +module.exports = abstractAstView diff --git a/remix-analyzer/src/analysis/modules/assignAndCompare.js b/remix-analyzer/src/analysis/modules/assignAndCompare.js new file mode 100644 index 0000000000..c9ec2e7820 --- /dev/null +++ b/remix-analyzer/src/analysis/modules/assignAndCompare.js @@ -0,0 +1,28 @@ +var name = 'Result not used: ' +var desc = 'The result of an operation was not used.' +var categories = require('./categories') +var common = require('./staticAnalysisCommon') + +function assignAndCompare () { + this.warningNodes = [] +} + +assignAndCompare.prototype.visit = function (node) { + if (common.isSubScopeWithTopLevelUnAssignedBinOp(node)) common.getUnAssignedTopLevelBinOps(node).forEach((n) => this.warningNodes.push(n)) +} + +assignAndCompare.prototype.report = function (compilationResults) { + return this.warningNodes.map(function (item, i) { + return { + warning: 'A binary operation yields a value that is not used in the following. This is often caused by confusing assignment (=) and comparison (==).', + location: item.src + } + }) +} + +module.exports = { + name: name, + description: desc, + category: categories.MISC, + Module: assignAndCompare +} diff --git a/remix-analyzer/src/analysis/modules/blockBlockhash.js b/remix-analyzer/src/analysis/modules/blockBlockhash.js new file mode 100644 index 0000000000..094c2d33d4 --- /dev/null +++ b/remix-analyzer/src/analysis/modules/blockBlockhash.js @@ -0,0 +1,32 @@ +var name = 'Block.blockhash usage: ' +var desc = 'Semantics maybe unclear' +var categories = require('./categories') +var common = require('./staticAnalysisCommon') + +function blockBlockhash () { + this.warningNodes = [] +} + +blockBlockhash.prototype.visit = function (node) { + if (common.isBlockBlockHashAccess(node)) this.warningNodes.push(node) +} + +blockBlockhash.prototype.report = function (compilationResults) { + return this.warningNodes.map(function (item, i) { + return { + warning: `use of "block.blockhash": "block.blockhash" is used to access the last 256 block hashes. + A miner computes the block hash by "summing up" the information in the current block mined. + By "summing up" the information in a clever way a miner can try to influence the outcome of a transaction in the current block. + This is especially easy if there are only a small number of equally likely outcomes.`, + location: item.src + } + }) +} + +module.exports = { + name: name, + description: desc, + category: categories.SECURITY, + Module: blockBlockhash +} + diff --git a/remix-analyzer/src/analysis/modules/blockTimestamp.js b/remix-analyzer/src/analysis/modules/blockTimestamp.js new file mode 100644 index 0000000000..a91d77ff81 --- /dev/null +++ b/remix-analyzer/src/analysis/modules/blockTimestamp.js @@ -0,0 +1,40 @@ +var name = 'Block timestamp: ' +var desc = 'Semantics maybe unclear' +var categories = require('./categories') +var common = require('./staticAnalysisCommon') + +function blockTimestamp () { + this.warningNowNodes = [] + this.warningblockTimestampNodes = [] +} + +blockTimestamp.prototype.visit = function (node) { + if (common.isNowAccess(node)) this.warningNowNodes.push(node) + else if (common.isBlockTimestampAccess(node)) this.warningblockTimestampNodes.push(node) +} + +blockTimestamp.prototype.report = function (compilationResults) { + return this.warningNowNodes.map(function (item, i) { + return { + warning: `use of "now": "now" does not mean current time. Now is an alias for block.timestamp. + Block.timestamp can be influenced by miners to a certain degree, be careful.`, + location: item.src, + more: 'http://solidity.readthedocs.io/en/develop/frequently-asked-questions.html#are-timestamps-now-block-timestamp-reliable' + } + }).concat(this.warningblockTimestampNodes.map(function (item, i) { + return { + warning: `use of "block.timestamp": "block.timestamp" can be influenced by miners to a certain degree. + That means that a miner can "choose" the block.timestamp, to a certain degree, to change the outcome of a transaction in the mined block.`, + location: item.src, + more: 'http://solidity.readthedocs.io/en/develop/frequently-asked-questions.html#are-timestamps-now-block-timestamp-reliable' + } + })) +} + +module.exports = { + name: name, + description: desc, + category: categories.SECURITY, + Module: blockTimestamp +} + diff --git a/remix-analyzer/src/analysis/modules/categories.js b/remix-analyzer/src/analysis/modules/categories.js new file mode 100644 index 0000000000..2b0e8d3414 --- /dev/null +++ b/remix-analyzer/src/analysis/modules/categories.js @@ -0,0 +1,5 @@ +module.exports = { + SECURITY: {displayName: 'Security', id: 'SEC'}, + GAS: {displayName: 'Gas & Economy', id: 'GAS'}, + MISC: {displayName: 'Miscellaneous', id: 'MISC'} +} diff --git a/remix-analyzer/src/analysis/modules/checksEffectsInteraction.js b/remix-analyzer/src/analysis/modules/checksEffectsInteraction.js new file mode 100644 index 0000000000..54a1dd051b --- /dev/null +++ b/remix-analyzer/src/analysis/modules/checksEffectsInteraction.js @@ -0,0 +1,88 @@ +var name = 'Check effects: ' +var desc = 'Avoid potential reentrancy bugs' +var categories = require('./categories') +var common = require('./staticAnalysisCommon') +var fcallGraph = require('./functionCallGraph') +var AbstractAst = require('./abstractAstView') + +function checksEffectsInteraction () { + this.abstractAst = new AbstractAst() + this.visit = this.abstractAst.build_visit( + (node) => common.isInteraction(node) || common.isEffect(node) || common.isLocalCallGraphRelevantNode(node) + ) + + this.report = this.abstractAst.build_report(report) +} + +checksEffectsInteraction.prototype.visit = function () { throw new Error('checksEffectsInteraction.js no visit function set upon construction') } + +checksEffectsInteraction.prototype.report = function () { throw new Error('checksEffectsInteraction.js no report function set upon construction') } + +function report (contracts, multipleContractsWithSameName) { + var warnings = [] + var hasModifiers = contracts.some((item) => item.modifiers.length > 0) + + var callGraph = fcallGraph.buildGlobalFuncCallGraph(contracts) + + contracts.forEach((contract) => { + contract.functions.forEach((func) => { + func.changesState = checkIfChangesState(common.getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters), + getContext(callGraph, contract, func)) + }) + + contract.functions.forEach((func) => { + if (isPotentialVulnerableFunction(func, getContext(callGraph, contract, func))) { + var funcName = common.getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters) + var comments = (hasModifiers) ? 'Note: Modifiers are currently not considered by this static analysis.' : '' + comments += (multipleContractsWithSameName) ? 'Note: Import aliases are currently not supported by this static analysis.' : '' + warnings.push({ + warning: `Potential Violation of Checks-Effects-Interaction pattern in ${funcName}: Could potentially lead to re-entrancy vulnerability. ${comments}`, + location: func.src, + more: 'http://solidity.readthedocs.io/en/develop/security-considerations.html#re-entrancy' + }) + } + }) + }) + + return warnings +} + +function getContext (callGraph, currentContract, func) { + return { callGraph: callGraph, currentContract: currentContract, stateVariables: getStateVariables(currentContract, func) } +} + +function getStateVariables (contract, func) { + return contract.stateVariables.concat(func.localVariables.filter(common.isStorageVariableDeclaration)) +} + +function isPotentialVulnerableFunction (func, context) { + var isPotentialVulnerable = false + var interaction = false + func.relevantNodes.forEach((node) => { + if (common.isInteraction(node)) { + interaction = true + } else if (interaction && (common.isWriteOnStateVariable(node, context.stateVariables) || isLocalCallWithStateChange(node, context))) { + isPotentialVulnerable = true + } + }) + return isPotentialVulnerable +} + +function isLocalCallWithStateChange (node, context) { + if (common.isLocalCallGraphRelevantNode(node)) { + var func = fcallGraph.resolveCallGraphSymbol(context.callGraph, common.getFullQualifiedFunctionCallIdent(context.currentContract.node, node)) + return !func || (func && func.node.changesState) + } + return false +} + +function checkIfChangesState (startFuncName, context) { + return fcallGraph.analyseCallGraph(context.callGraph, startFuncName, context, (node, context) => common.isWriteOnStateVariable(node, context.stateVariables)) +} + +module.exports = { + name: name, + description: desc, + category: categories.SECURITY, + Module: checksEffectsInteraction +} diff --git a/remix-analyzer/src/analysis/modules/constantFunctions.js b/remix-analyzer/src/analysis/modules/constantFunctions.js new file mode 100644 index 0000000000..5da3562eb6 --- /dev/null +++ b/remix-analyzer/src/analysis/modules/constantFunctions.js @@ -0,0 +1,108 @@ +var name = 'Constant functions: ' +var desc = 'Check for potentially constant functions' +var categories = require('./categories') +var common = require('./staticAnalysisCommon') +var fcallGraph = require('./functionCallGraph') +var AbstractAst = require('./abstractAstView') + +function constantFunctions () { + this.abstractAst = new AbstractAst() + + this.visit = this.abstractAst.build_visit( + (node) => common.isLowLevelCall(node) || + common.isTransfer(node) || + common.isExternalDirectCall(node) || + common.isEffect(node) || + common.isLocalCallGraphRelevantNode(node) || + common.isInlineAssembly(node) || + common.isNewExpression(node) || + common.isSelfdestructCall(node) || + common.isDeleteUnaryOperation(node) + ) + + this.report = this.abstractAst.build_report(report) +} + +constantFunctions.prototype.visit = function () { throw new Error('constantFunctions.js no visit function set upon construction') } + +constantFunctions.prototype.report = function () { throw new Error('constantFunctions.js no report function set upon construction') } + +function report (contracts, multipleContractsWithSameName) { + var warnings = [] + var hasModifiers = contracts.some((item) => item.modifiers.length > 0) + + var callGraph = fcallGraph.buildGlobalFuncCallGraph(contracts) + + contracts.forEach((contract) => { + contract.functions.forEach((func) => { + if (common.isPayableFunction(func.node) || common.isConstructor(func.node)) { + func.potentiallyshouldBeConst = false + } else { + func.potentiallyshouldBeConst = checkIfShouldBeConstant(common.getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters), + getContext(callGraph, contract, func)) + } + }) + + contract.functions.filter((func) => common.hasFunctionBody(func.node)).forEach((func) => { + if (common.isConstantFunction(func.node) !== func.potentiallyshouldBeConst) { + var funcName = common.getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters) + var comments = (hasModifiers) ? 'Note: Modifiers are currently not considered by this static analysis.' : '' + comments += (multipleContractsWithSameName) ? 'Note: Import aliases are currently not supported by this static analysis.' : '' + if (func.potentiallyshouldBeConst) { + warnings.push({ + warning: `${funcName} : Potentially should be constant but is not. ${comments}`, + location: func.src, + more: 'http://solidity.readthedocs.io/en/develop/contracts.html#constant-functions' + }) + } else { + warnings.push({ + warning: `${funcName} : Is constant but potentially should not be. ${comments}`, + location: func.src, + more: 'http://solidity.readthedocs.io/en/develop/contracts.html#constant-functions' + }) + } + } + }) + }) + + return warnings +} + +function getContext (callGraph, currentContract, func) { + return { callGraph: callGraph, currentContract: currentContract, stateVariables: getStateVariables(currentContract, func) } +} + +function getStateVariables (contract, func) { + return contract.stateVariables.concat(func.localVariables.filter(common.isStorageVariableDeclaration)) +} + +function checkIfShouldBeConstant (startFuncName, context) { + return !fcallGraph.analyseCallGraph(context.callGraph, startFuncName, context, isConstBreaker) +} + +function isConstBreaker (node, context) { + return common.isWriteOnStateVariable(node, context.stateVariables) || + common.isLowLevelCall(node) || + common.isTransfer(node) || + isCallOnNonConstExternalInterfaceFunction(node, context) || + common.isCallToNonConstLocalFunction(node) || + common.isInlineAssembly(node) || + common.isNewExpression(node) || + common.isSelfdestructCall(node) || + common.isDeleteUnaryOperation(node) +} + +function isCallOnNonConstExternalInterfaceFunction (node, context) { + if (common.isExternalDirectCall(node)) { + var func = fcallGraph.resolveCallGraphSymbol(context.callGraph, common.getFullQualifiedFunctionCallIdent(context.currentContract, node)) + return !func || (func && !common.isConstantFunction(func.node.node)) + } + return false +} + +module.exports = { + name: name, + description: desc, + category: categories.MISC, + Module: constantFunctions +} diff --git a/remix-analyzer/src/analysis/modules/deleteDynamicArrays.js b/remix-analyzer/src/analysis/modules/deleteDynamicArrays.js new file mode 100644 index 0000000000..b1143d179e --- /dev/null +++ b/remix-analyzer/src/analysis/modules/deleteDynamicArrays.js @@ -0,0 +1,29 @@ +var name = 'Delete on dynamic Array: ' +var desc = 'Use require and appropriately' +var categories = require('./categories') +var common = require('./staticAnalysisCommon') + +function deleteDynamicArrays () { + this.rel = [] +} + +deleteDynamicArrays.prototype.visit = function (node) { + if (common.isDeleteOfDynamicArray(node)) this.rel.push(node) +} + +deleteDynamicArrays.prototype.report = function (compilationResults) { + return this.rel.map((node) => { + return { + warning: 'The “delete” operation when applied to a dynamically sized array in Solidity generates code to delete each of the elements contained. If the array is large, this operation can surpass the block gas limit and raise an OOG exception. Also nested dynamically sized objects can produce the same results.', + location: node.src, + more: 'http://solidity.readthedocs.io/en/latest/types.html?highlight=array#delete' + } + }) +} + +module.exports = { + name: name, + description: desc, + category: categories.GAS, + Module: deleteDynamicArrays +} diff --git a/remix-analyzer/src/analysis/modules/functionCallGraph.js b/remix-analyzer/src/analysis/modules/functionCallGraph.js new file mode 100644 index 0000000000..748e2a9b43 --- /dev/null +++ b/remix-analyzer/src/analysis/modules/functionCallGraph.js @@ -0,0 +1,114 @@ +'use strict' + +var common = require('./staticAnalysisCommon') + +function buildLocalFuncCallGraphInternal (functions, nodeFilter, extractNodeIdent, extractFuncDefIdent) { + var callGraph = {} + functions.forEach((func) => { + var calls = func.relevantNodes + .filter(nodeFilter) + .map(extractNodeIdent) + .filter((name) => name !== extractFuncDefIdent(func)) // filter self recursive call + + callGraph[extractFuncDefIdent(func)] = { node: func, calls: calls } + }) + + return callGraph +} + +/** + * Builds a function call graph for the current contracts. + * Example Contract call graph: + * + * { + * "KingOfTheEtherThrone": { + * "contracts": {...}, // Contract node as defined in abstractAstView.js + * "functions": { + * "KingOfTheEtherThrone.claimThrone(string memory)": { // function in KingOfEtherThrone + * "node": {...}, // function node as defined in abstractAstView.js + * "calls": { // list of full qualified function names which are called form this function + * } + * } + * } + * }, + * "foo": { + * "contract": {...}, // Contract node as definded in abstractAstView.js + * "functions": {} // map from full qualified function name to func node + * } + * } + * + * @contracts {list contracts} Expects as input the contract structure defined in abstractAstView.js + * @return {map (string -> Contract Call Graph)} returns map from contract name to contract call graph + */ +function buildGlobalFuncCallGraph (contracts) { + var callGraph = {} + contracts.forEach((contract) => { + var filterNodes = (node) => { return common.isLocalCallGraphRelevantNode(node) || common.isExternalDirectCall(node) } + var getNodeIdent = (node) => { return common.getFullQualifiedFunctionCallIdent(contract.node, node) } + var getFunDefIdent = (funcDef) => { return common.getFullQuallyfiedFuncDefinitionIdent(contract.node, funcDef.node, funcDef.parameters) } + + callGraph[common.getContractName(contract.node)] = { contract: contract, functions: buildLocalFuncCallGraphInternal(contract.functions, filterNodes, getNodeIdent, getFunDefIdent) } + }) + + return callGraph +} + +/** + * Walks through the call graph from a defined starting function, true if nodeCheck holds for every relevant node in the callgraph + * @callGraph {callGraph} As returned by buildGlobalFuncCallGraph + * @funcName {string} full qualified name of the starting function + * @context {Object} provides additional context information that can be used by the nodeCheck function + * @nodeCheck {(ASTNode, context) -> bool} applied on every relevant node in the call graph + * @return {bool} returns map from contract name to contract call graph + */ +function analyseCallGraph (callGraph, funcName, context, nodeCheck) { + return analyseCallGraphInternal(callGraph, funcName, context, (a, b) => a || b, nodeCheck, {}) +} + +function analyseCallGraphInternal (callGraph, funcName, context, combinator, nodeCheck, visited) { + var current = resolveCallGraphSymbol(callGraph, funcName) + + if (current === undefined || visited[funcName] === true) return true + visited[funcName] = true + + return combinator(current.node.relevantNodes.reduce((acc, val) => combinator(acc, nodeCheck(val, context)), false), + current.calls.reduce((acc, val) => combinator(acc, analyseCallGraphInternal(callGraph, val, context, combinator, nodeCheck, visited)), false)) +} + +function resolveCallGraphSymbol (callGraph, funcName) { + return resolveCallGraphSymbolInternal(callGraph, funcName, false) +} + +function resolveCallGraphSymbolInternal (callGraph, funcName, silent) { + var current + if (funcName.includes('.')) { + var parts = funcName.split('.') + var contractPart = parts[0] + var functionPart = parts[1] + var currentContract = callGraph[contractPart] + if (!(currentContract === undefined)) { + current = currentContract.functions[funcName] + // resolve inheritance hierarchy + if (current === undefined) { + // resolve inheritance lookup in linearized fashion + var inheritsFromNames = currentContract.contract.inheritsFrom.reverse() + for (var i = 0; i < inheritsFromNames.length; i++) { + var res = resolveCallGraphSymbolInternal(callGraph, inheritsFromNames[i] + '.' + functionPart, true) + if (!(res === undefined)) return res + } + } + } else { + if (!silent) console.log(`static analysis functionCallGraph.js: Contract ${contractPart} not found in function call graph.`) + } + } else { + throw new Error('functionCallGraph.js: function does not have full qualified name.') + } + if (current === undefined && !silent) console.log(`static analysis functionCallGraph.js: ${funcName} not found in function call graph.`) + return current +} + +module.exports = { + analyseCallGraph: analyseCallGraph, + buildGlobalFuncCallGraph: buildGlobalFuncCallGraph, + resolveCallGraphSymbol: resolveCallGraphSymbol +} diff --git a/remix-analyzer/src/analysis/modules/gasCosts.js b/remix-analyzer/src/analysis/modules/gasCosts.js new file mode 100644 index 0000000000..d9d09cfa4c --- /dev/null +++ b/remix-analyzer/src/analysis/modules/gasCosts.js @@ -0,0 +1,65 @@ +var name = 'Gas costs: ' +var desc = 'Warn if the gas requirements of functions are too high.' +var categories = require('./categories') + +function gasCosts () { +} + +/** + * call the given @arg cb (function) for all the contracts. Uses last compilation result + * stop visiting when cb return true + * @param {Function} cb - callback + */ + // @TODO has been copied from remix-ide repo ! should fix that soon ! +function visitContracts (contracts, cb) { + for (var file in contracts) { + for (var name in contracts[file]) { + if (cb({ name: name, object: contracts[file][name], file: file })) return + } + } +} + +gasCosts.prototype.report = function (compilationResults) { + var report = [] + visitContracts(compilationResults.contracts, (contract) => { + if ( + !contract.object.evm.gasEstimates || + !contract.object.evm.gasEstimates.external + ) { + return + } + var fallback = contract.object.evm.gasEstimates.external[''] + if (fallback !== undefined) { + if (fallback === null || fallback >= 2100 || fallback === 'infinite') { + report.push({ + warning: `Fallback function of contract ${contract.name} requires too much gas (${fallback}). + If the fallback function requires more than 2300 gas, the contract cannot receive Ether.` + }) + } + } + + for (var functionName in contract.object.evm.gasEstimates.external) { + if (functionName === '') { + continue + } + var gas = contract.object.evm.gasEstimates.external[functionName] + var gasString = gas === null ? 'unknown or not constant' : 'high: ' + gas + if (gas === null || gas >= 3000000 || gas === 'infinite') { + report.push({ + warning: `Gas requirement of function ${contract.name}.${functionName} ${gasString}. + If the gas requirement of a function is higher than the block gas limit, it cannot be executed. + Please avoid loops in your functions or actions that modify large areas of storage + (this includes clearing or copying arrays in storage)` + }) + } + } + }) + return report +} + +module.exports = { + name: name, + description: desc, + category: categories.GAS, + Module: gasCosts +} diff --git a/remix-analyzer/src/analysis/modules/guardConditions.js b/remix-analyzer/src/analysis/modules/guardConditions.js new file mode 100644 index 0000000000..a9d877c57c --- /dev/null +++ b/remix-analyzer/src/analysis/modules/guardConditions.js @@ -0,0 +1,29 @@ +var name = 'Guard Conditions: ' +var desc = 'Use require and appropriately' +var categories = require('./categories') +var common = require('./staticAnalysisCommon') + +function guardConditions () { + this.guards = [] +} + +guardConditions.prototype.visit = function (node) { + if (common.isRequireCall(node) || common.isAssertCall(node)) this.guards.push(node) +} + +guardConditions.prototype.report = function (compilationResults) { + if (this.guards.length > 0) { + return [{ + warning: 'Use assert(x) if you never ever want x to be false, not in any circumstance (apart from a bug in your code). Use require(x) if x can be false, due to e.g. invalid input or a failing external component.', + more: 'http://solidity.readthedocs.io/en/develop/control-structures.html#error-handling-assert-require-revert-and-exceptions' + }] + } + return [] +} + +module.exports = { + name: name, + description: desc, + category: categories.MISC, + Module: guardConditions +} diff --git a/remix-analyzer/src/analysis/modules/inlineAssembly.js b/remix-analyzer/src/analysis/modules/inlineAssembly.js new file mode 100644 index 0000000000..ab9b6e1d03 --- /dev/null +++ b/remix-analyzer/src/analysis/modules/inlineAssembly.js @@ -0,0 +1,30 @@ +var name = 'Inline assembly: ' +var desc = 'Use of Inline Assembly' +var categories = require('./categories') +var common = require('./staticAnalysisCommon') + +function inlineAssembly () { + this.inlineAssNodes = [] +} + +inlineAssembly.prototype.visit = function (node) { + if (common.isInlineAssembly(node)) this.inlineAssNodes.push(node) +} + +inlineAssembly.prototype.report = function (compilationResults) { + return this.inlineAssNodes.map((node) => { + return { + warning: `CAUTION: The Contract uses inline assembly, this is only advised in rare cases. + Additionally static analysis modules do not parse inline Assembly, this can lead to wrong analysis results.`, + location: node.src, + more: 'http://solidity.readthedocs.io/en/develop/assembly.html#solidity-assembly' + } + }) +} + +module.exports = { + name: name, + description: desc, + category: categories.SECURITY, + Module: inlineAssembly +} diff --git a/remix-analyzer/src/analysis/modules/intDivisionTruncate.js b/remix-analyzer/src/analysis/modules/intDivisionTruncate.js new file mode 100644 index 0000000000..51a3199a99 --- /dev/null +++ b/remix-analyzer/src/analysis/modules/intDivisionTruncate.js @@ -0,0 +1,28 @@ +var name = 'Data Trucated: ' +var desc = 'Division on int/uint values truncates the result.' +var categories = require('./categories') +var common = require('./staticAnalysisCommon') + +function intDivitionTruncate () { + this.warningNodes = [] +} + +intDivitionTruncate.prototype.visit = function (node) { + if (common.isIntDivision(node)) this.warningNodes.push(node) +} + +intDivitionTruncate.prototype.report = function (compilationResults) { + return this.warningNodes.map(function (item, i) { + return { + warning: 'Division of integer values yields an integer value again. That means eg. a / 100 = 0 instead of 0.a since the result is an integer again. This does not hold for division of (only) literal values since those yield rational constants.', + location: item.src + } + }) +} + +module.exports = { + name: name, + description: desc, + category: categories.MISC, + Module: intDivitionTruncate +} diff --git a/remix-analyzer/src/analysis/modules/list.js b/remix-analyzer/src/analysis/modules/list.js new file mode 100644 index 0000000000..f693508ac4 --- /dev/null +++ b/remix-analyzer/src/analysis/modules/list.js @@ -0,0 +1,17 @@ +module.exports = [ + require('./txOrigin'), + require('./gasCosts'), + require('./thisLocal'), + require('./checksEffectsInteraction'), + require('./constantFunctions'), + require('./similarVariableNames.js'), + require('./inlineAssembly'), + require('./blockTimestamp'), + require('./lowLevelCalls'), + require('./blockBlockhash'), + require('./noReturn'), + require('./selfdestruct'), + require('./guardConditions'), + require('./deleteDynamicArrays'), + require('./assignAndCompare') +] diff --git a/remix-analyzer/src/analysis/modules/lowLevelCalls.js b/remix-analyzer/src/analysis/modules/lowLevelCalls.js new file mode 100644 index 0000000000..6e8dd0a352 --- /dev/null +++ b/remix-analyzer/src/analysis/modules/lowLevelCalls.js @@ -0,0 +1,64 @@ +var name = 'Low level calls: ' +var desc = 'Semantics maybe unclear' +var categories = require('./categories') +var common = require('./staticAnalysisCommon') + +function lowLevelCalls () { + this.llcNodes = [] +} + +lowLevelCalls.prototype.visit = function (node) { + if (common.isLowLevelCallInst(node)) { + this.llcNodes.push({node: node, type: common.lowLevelCallTypes.CALL}) + } else if (common.isLowLevelCallcodeInst(node)) { + this.llcNodes.push({node: node, type: common.lowLevelCallTypes.CALLCODE}) + } else if (common.isLowLevelDelegatecallInst(node)) { + this.llcNodes.push({node: node, type: common.lowLevelCallTypes.DELEGATECALL}) + } else if (common.isLowLevelSendInst(node)) { + this.llcNodes.push({node: node, type: common.lowLevelCallTypes.SEND}) + } +} + +lowLevelCalls.prototype.report = function (compilationResults) { + return this.llcNodes.map(function (item, i) { + var text = '' + var morehref = null + switch (item.type) { + case common.lowLevelCallTypes.CALL: + text = `use of "call": the use of low level "call" should be avoided whenever possible. + It can lead to unexpected behavior if return value is not handled properly. + Please use Direct Calls via specifying the called contract's interface.` + morehref = 'http://solidity.readthedocs.io/en/develop/control-structures.html?#external-function-calls' + // http://solidity.readthedocs.io/en/develop/frequently-asked-questions.html?#why-is-the-low-level-function-call-less-favorable-than-instantiating-a-contract-with-a-variable-contractb-b-and-executing-its-functions-b-dosomething + break + case common.lowLevelCallTypes.CALLCODE: + text = `use of "callcode": the use of low level "callcode" should be avoided whenever possible. + External code that is called can change the state of the calling contract and send ether from the caller's balance. + If this is wanted behaviour use the Solidity library feature if possible.` + morehref = 'http://solidity.readthedocs.io/en/develop/contracts.html#libraries' + break + case common.lowLevelCallTypes.DELEGATECALL: + text = `use of "delegatecall": the use of low level "delegatecall" should be avoided whenever possible. + External code that is called can change the state of the calling contract and send ether from the caller's balance. + If this is wanted behaviour use the Solidity library feature if possible.` + morehref = 'http://solidity.readthedocs.io/en/develop/contracts.html#libraries' + break + case common.lowLevelCallTypes.SEND: + text = `use of "send": "send" does not throw an exception when not successful, make sure you deal with the failure case accordingly. + Use "transfer" whenever failure of the ether transfer should rollback the whole transaction. + Note: if you "send/transfer" ether to a contract the fallback function is called, the callees fallback function is very limited due to the limited amount of gas provided by "send/transfer". + No state changes are possible but the callee can log the event or revert the transfer. "send/transfer" is syntactic sugar for a "call" to the fallback function with 2300 gas and a specified ether value.` + morehref = 'http://solidity.readthedocs.io/en/develop/security-considerations.html#sending-and-receiving-ether' + break + } + return { warning: text, more: morehref, location: item.node.src } + }) +} + +module.exports = { + name: name, + description: desc, + category: categories.SECURITY, + Module: lowLevelCalls +} + diff --git a/remix-analyzer/src/analysis/modules/noReturn.js b/remix-analyzer/src/analysis/modules/noReturn.js new file mode 100644 index 0000000000..dbd212ead5 --- /dev/null +++ b/remix-analyzer/src/analysis/modules/noReturn.js @@ -0,0 +1,73 @@ +var name = 'no return: ' +var desc = 'Function with return type is not returning' +var categories = require('./categories') +var common = require('./staticAnalysisCommon') +var AbstractAst = require('./abstractAstView') + +function noReturn () { + this.abstractAst = new AbstractAst() + + this.visit = this.abstractAst.build_visit( + (node) => common.isReturn(node) || common.isAssignment(node) + ) + + this.report = this.abstractAst.build_report(report) +} + +noReturn.prototype.visit = function () { throw new Error('noReturn.js no visit function set upon construction') } + +noReturn.prototype.report = function () { throw new Error('noReturn.js no report function set upon construction') } + +function report (contracts, multipleContractsWithSameName) { + var warnings = [] + + contracts.forEach((contract) => { + contract.functions.filter((func) => common.hasFunctionBody(func.node)).forEach((func) => { + var funcName = common.getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters) + if (hasNamedAndUnnamedReturns(func)) { + warnings.push({ + warning: `${funcName}: Mixing of named and unnamed return parameters is not advised.`, + location: func.src + }) + } else if (shouldReturn(func) && !(hasReturnStatement(func) || (hasNamedReturns(func) && hasAssignToAllNamedReturns(func)))) { + warnings.push({ + warning: `${funcName}: Defines a return type but never explicitly returns a value.`, + location: func.src + }) + } + }) + }) + + return warnings +} + +function shouldReturn (func) { + return func.returns.length > 0 +} + +function hasReturnStatement (func) { + return func.relevantNodes.filter(common.isReturn).length > 0 +} + +function hasAssignToAllNamedReturns (func) { + var namedReturns = func.returns.filter((n) => n.name.length > 0).map((n) => n.name) + var assignedVars = func.relevantNodes.filter(common.isAssignment).map(common.getEffectedVariableName) + var diff = namedReturns.filter(e => !assignedVars.includes(e)) + return diff.length === 0 +} + +function hasNamedReturns (func) { + return func.returns.filter((n) => n.name.length > 0).length > 0 +} + +function hasNamedAndUnnamedReturns (func) { + return func.returns.filter((n) => n.name.length === 0).length > 0 && + hasNamedReturns(func) +} + +module.exports = { + name: name, + description: desc, + category: categories.MISC, + Module: noReturn +} diff --git a/remix-analyzer/src/analysis/modules/selfdestruct.js b/remix-analyzer/src/analysis/modules/selfdestruct.js new file mode 100644 index 0000000000..14b24f9e3c --- /dev/null +++ b/remix-analyzer/src/analysis/modules/selfdestruct.js @@ -0,0 +1,31 @@ +var name = 'Selfdestruct: ' +var desc = 'Be aware of caller contracts.' +var categories = require('./categories') +var common = require('./staticAnalysisCommon') + +function selfdestruct () { + this.relevantNodes = [] +} + +selfdestruct.prototype.visit = function (node) { + if (common.isSelfdestructCall(node)) { + this.relevantNodes.push(node) + } +} + +selfdestruct.prototype.report = function () { + return this.relevantNodes.map(function (item, i) { + return { + warning: 'Use of selfdestruct: can block calling contracts unexpectedly. Be especially careful if this contract is planned to be used by other contracts (i.e. library contracts, interactions). Selfdestruction of the callee contract can leave callers in an inoperable state.', + location: item.src, + more: 'https://paritytech.io/blog/security-alert.html' + } + }) +} + +module.exports = { + name: name, + description: desc, + category: categories.SECURITY, + Module: selfdestruct +} diff --git a/remix-analyzer/src/analysis/modules/similarVariableNames.js b/remix-analyzer/src/analysis/modules/similarVariableNames.js new file mode 100644 index 0000000000..903fbab20d --- /dev/null +++ b/remix-analyzer/src/analysis/modules/similarVariableNames.js @@ -0,0 +1,86 @@ +var name = 'Similar variable names: ' +var desc = 'Check if variable names are too similar' +var categories = require('./categories') +var common = require('./staticAnalysisCommon') +var AbstractAst = require('./abstractAstView') +var levenshtein = require('fast-levenshtein') +var remixLib = require('remix-lib') +var util = remixLib.util + +function similarVariableNames () { + this.abstractAst = new AbstractAst() + + this.visit = this.abstractAst.build_visit( + (node) => false + ) + + this.report = this.abstractAst.build_report(report) +} + +similarVariableNames.prototype.visit = function () { throw new Error('similarVariableNames.js no visit function set upon construction') } + +similarVariableNames.prototype.report = function () { throw new Error('similarVariableNames.js no report function set upon construction') } + +function report (contracts, multipleContractsWithSameName) { + var warnings = [] + var hasModifiers = contracts.some((item) => item.modifiers.length > 0) + + contracts.forEach((contract) => { + contract.functions.forEach((func) => { + var funcName = common.getFullQuallyfiedFuncDefinitionIdent(contract.node, func.node, func.parameters) + var hasModifiersComments = '' + if (hasModifiers) { + hasModifiersComments = 'Note: Modifiers are currently not considered by this static analysis.' + } + var multipleContractsWithSameNameComments = '' + if (multipleContractsWithSameName) { + multipleContractsWithSameNameComments = 'Note: Import aliases are currently not supported by this static analysis.' + } + + var vars = getFunctionVariables(contract, func).map(common.getDeclaredVariableName) + + findSimilarVarNames(vars).map((sim) => { + warnings.push({ + warning: `${funcName} : Variables have very similar names ${sim.var1} and ${sim.var2}. ${hasModifiersComments} ${multipleContractsWithSameNameComments}`, + location: func.src + }) + }) + }) + }) + + return warnings +} + +function findSimilarVarNames (vars) { + var similar = [] + var comb = {} + vars.map((varName1) => vars.map((varName2) => { + if (varName1.length > 1 && varName2.length > 1 && varName2 !== varName1 && !isCommonPrefixedVersion(varName1, varName2) && !isCommonNrSuffixVersion(varName1, varName2) && !(comb[varName1 + ';' + varName2] || comb[varName2 + ';' + varName1])) { + comb[varName1 + ';' + varName2] = true + var distance = levenshtein.get(varName1, varName2) + if (distance <= 2) similar.push({ var1: varName1, var2: varName2, distance: distance }) + } + })) + return similar +} + +function isCommonPrefixedVersion (varName1, varName2) { + return (varName1.startsWith('_') && varName1.slice(1) === varName2) || (varName2.startsWith('_') && varName2.slice(1) === varName1) +} + +function isCommonNrSuffixVersion (varName1, varName2) { + var ref = '^' + util.escapeRegExp(varName1.slice(0, -1)) + '[0-9]*$' + + return varName2.match(ref) != null +} + +function getFunctionVariables (contract, func) { + return contract.stateVariables.concat(func.localVariables) +} + +module.exports = { + name: name, + description: desc, + category: categories.MISC, + Module: similarVariableNames +} diff --git a/remix-analyzer/src/analysis/modules/staticAnalysisCommon.js b/remix-analyzer/src/analysis/modules/staticAnalysisCommon.js new file mode 100644 index 0000000000..51929708d0 --- /dev/null +++ b/remix-analyzer/src/analysis/modules/staticAnalysisCommon.js @@ -0,0 +1,1028 @@ +'use strict' + +var remixLib = require('remix-lib') +var util = remixLib.util + +var nodeTypes = { + IDENTIFIER: 'Identifier', + MEMBERACCESS: 'MemberAccess', + FUNCTIONCALL: 'FunctionCall', + FUNCTIONDEFINITION: 'FunctionDefinition', + VARIABLEDECLARATION: 'VariableDeclaration', + ASSIGNMENT: 'Assignment', + CONTRACTDEFINITION: 'ContractDefinition', + UNARYOPERATION: 'UnaryOperation', + BINARYOPERATION: 'BinaryOperation', + EXPRESSIONSTATEMENT: 'ExpressionStatement', + MODIFIERDEFINITION: 'ModifierDefinition', + MODIFIERINVOCATION: 'ModifierInvocation', + INHERITANCESPECIFIER: 'InheritanceSpecifier', + USERDEFINEDTYPENAME: 'UserDefinedTypeName', + INLINEASSEMBLY: 'InlineAssembly', + BLOCK: 'Block', + NEWEXPRESSION: 'NewExpression', + RETURN: 'Return', + IFSTATEMENT: 'IfStatement', + FORSTATEMENT: 'ForStatement', + WHILESTATEMENT: 'WhileStatement', + DOWHILESTATEMENT: 'DoWhileStatement' +} + +var basicTypes = { + UINT: 'uint256', + BOOL: 'bool', + ADDRESS: 'address', + BYTES32: 'bytes32', + STRING_MEM: 'string memory', + BYTES_MEM: 'bytes memory', + BYTES4: 'bytes4' +} + +var basicRegex = { + CONTRACTTYPE: '^contract ', + FUNCTIONTYPE: '^function \\(', + EXTERNALFUNCTIONTYPE: '^function \\(.*\\).* external', + CONSTANTFUNCTIONTYPE: '^function \\(.*\\).* (constant|view|pure)', + REFTYPE: '( storage )|(mapping\\()|(\\[\\])', + FUNCTIONSIGNATURE: '^function \\(([^\\(]*)\\)', + LIBRARYTYPE: '^type\\(library (.*)\\)' +} + +var basicFunctionTypes = { + SEND: buildFunctionSignature([basicTypes.UINT], [basicTypes.BOOL], false), + CALL: buildFunctionSignature([], [basicTypes.BOOL], true), + DELEGATECALL: buildFunctionSignature([], [basicTypes.BOOL], false), + TRANSFER: buildFunctionSignature([basicTypes.UINT], [], false) +} + +var builtinFunctions = { + 'keccak256()': true, + 'sha3()': true, + 'sha256()': true, + 'ripemd160()': true, + 'ecrecover(bytes32,uint8,bytes32,bytes32)': true, + 'addmod(uint256,uint256,uint256)': true, + 'mulmod(uint256,uint256,uint256)': true, + 'selfdestruct(address)': true, + 'revert()': true, + 'revert(string memory)': true, + 'assert(bool)': true, + 'require(bool)': true, + 'require(bool,string memory)': true, + 'gasleft()': true, + 'blockhash(uint)': true +} + +var lowLevelCallTypes = { + CALL: { ident: 'call', type: basicFunctionTypes.CALL }, + CALLCODE: { ident: 'callcode', type: basicFunctionTypes.CALL }, + DELEGATECALL: { ident: 'delegatecall', type: basicFunctionTypes.DELEGATECALL }, + SEND: { ident: 'send', type: basicFunctionTypes.SEND }, + TRANSFER: { ident: 'transfer', type: basicFunctionTypes.TRANSFER } +} + +var specialVariables = { + BLOCKTIMESTAMP: { obj: 'block', member: 'timestamp', type: basicTypes.UINT }, + BLOCKHASH: { + obj: 'block', + member: 'blockhash', + type: buildFunctionSignature([basicTypes.UINT], [basicTypes.BYTES32], false) + } +} + +var abiNamespace = { + ENCODE: { + obj: 'abi', + member: 'encode', + type: buildFunctionSignature([], [basicTypes.BYTES_MEM], false, 'pure') + }, + ENCODEPACKED: { + obj: 'abi', + member: 'encodePacked', + type: buildFunctionSignature([], [basicTypes.BYTES_MEM], false, 'pure') + }, + ENCODE_SELECT: { + obj: 'abi', + member: 'encodeWithSelector', + type: buildFunctionSignature([basicTypes.BYTES4], [basicTypes.BYTES_MEM], false, 'pure') + }, + ENCODE_SIG: { + obj: 'abi', + member: 'encodeWithSignature', + type: buildFunctionSignature([basicTypes.STRING_MEM], [basicTypes.BYTES_MEM], false, 'pure') + } +} + +// #################### Trivial Getters + +function getType (node) { + return node.attributes.type +} + +// #################### Complex Getters + +/** + * Returns the type parameter of function call AST nodes. Throws if not a function call node + * @func {ASTNode} Function call node + * @return {string} type of function call + */ +function getFunctionCallType (func) { + if (!(isExternalDirectCall(func) || isThisLocalCall(func) || isSuperLocalCall(func) || isLocalCall(func) || isLibraryCall(func))) throw new Error('staticAnalysisCommon.js: not function call Node') + if (isExternalDirectCall(func) || isThisLocalCall(func) || isSuperLocalCall(func) || isLibraryCall(func)) return func.attributes.type + return findFirstSubNodeLTR(func, exactMatch(nodeTypes.IDENTIFIER)).attributes.type +} + +/** + * Get the variable name written to by a effect node, except for inline assembly because there is no information to find out where we write to. Trows if not a effect node or is inlineassmbly. + * Example: x = 10; => x + * @effectNode {ASTNode} Function call node + * @return {string} variable name written to + */ +function getEffectedVariableName (effectNode) { + if (!isEffect(effectNode) || isInlineAssembly(effectNode)) throw new Error('staticAnalysisCommon.js: not an effect Node or inline assembly') + return findFirstSubNodeLTR(effectNode, exactMatch(nodeTypes.IDENTIFIER)).attributes.value +} + +/** + * Returns the identifier of a local call, Throws on wrong node. + * Example: f(103) => f + * @localCallNode {ASTNode} Function call node + * @return {string} name of the function called + */ +function getLocalCallName (localCallNode) { + if (!isLocalCall(localCallNode) && !isAbiNamespaceCall(localCallNode)) throw new Error('staticAnalysisCommon.js: not an local call Node') + return localCallNode.children[0].attributes.value +} + +/** + * Returns the identifier of a this local call, Throws on wrong node. + * Example: this.f(103) => f + * @localCallNode {ASTNode} Function call node + * @return {string} name of the function called + */ +function getThisLocalCallName (localCallNode) { + if (!isThisLocalCall(localCallNode)) throw new Error('staticAnalysisCommon.js: not an this local call Node') + return localCallNode.attributes.value +} + +/** + * Returns the identifier of a super local call, Throws on wrong node. + * Example: super.f(103) => f + * @localCallNode {ASTNode} Function call node + * @return {string} name of the function called + */ +function getSuperLocalCallName (localCallNode) { + if (!isSuperLocalCall(localCallNode)) throw new Error('staticAnalysisCommon.js: not an super local call Node') + return localCallNode.attributes.member_name +} + +/** + * Returns the contract type of a external direct call, Throws on wrong node. + * Example: + * foo x = foo(0xdeadbeef...); + * x.f(103) => foo + * @extDirectCall {ASTNode} Function call node + * @return {string} name of the contract the function is defined in + */ +function getExternalDirectCallContractName (extDirectCall) { + if (!isExternalDirectCall(extDirectCall)) throw new Error('staticAnalysisCommon.js: not an external direct call Node') + return extDirectCall.children[0].attributes.type.replace(new RegExp(basicRegex.CONTRACTTYPE), '') +} + +/** + * Returns the name of the contract of a this local call (current contract), Throws on wrong node. + * Example: + * Contract foo { + * ... + * this.f(103) => foo + * ... + * @thisLocalCall {ASTNode} Function call node + * @return {string} name of the contract the function is defined in + */ +function getThisLocalCallContractName (thisLocalCall) { + if (!isThisLocalCall(thisLocalCall)) throw new Error('staticAnalysisCommon.js: not an this local call Node') + return thisLocalCall.children[0].attributes.type.replace(new RegExp(basicRegex.CONTRACTTYPE), '') +} + +/** + * Returns the function identifier of a external direct call, Throws on wrong node. + * Example: + * foo x = foo(0xdeadbeef...); + * x.f(103) => f + * @extDirectCall {ASTNode} Function call node + * @return {string} name of the function called + */ +function getExternalDirectCallMemberName (extDirectCall) { + if (!isExternalDirectCall(extDirectCall)) throw new Error('staticAnalysisCommon.js: not an external direct call Node') + return extDirectCall.attributes.member_name +} + +/** + * Returns the name of a contract, Throws on wrong node. + * Example: + * Contract foo { => foo + * @contract {ASTNode} Contract Definition node + * @return {string} name of a contract defined + */ +function getContractName (contract) { + if (!isContractDefinition(contract)) throw new Error('staticAnalysisCommon.js: not an contractDefinition Node') + return contract.attributes.name +} + +/** + * Returns the name of a function definition, Throws on wrong node. + * Example: + * func foo(uint bla) { => foo + * @funcDef {ASTNode} Function Definition node + * @return {string} name of a function defined + */ +function getFunctionDefinitionName (funcDef) { + if (!isFunctionDefinition(funcDef)) throw new Error('staticAnalysisCommon.js: not an functionDefinition Node') + return funcDef.attributes.name +} + +/** + * Returns the identifier of an inheritance specifier, Throws on wrong node. + * Example: + * contract KingOfTheEtherThrone is b { => b + * @func {ASTNode} Inheritance specifier + * @return {string} name of contract inherited from + */ +function getInheritsFromName (inheritsNode) { + if (!isInheritanceSpecifier(inheritsNode)) throw new Error('staticAnalysisCommon.js: not an InheritanceSpecifier Node') + return inheritsNode.children[0].attributes.name +} + +/** + * Returns the identifier of a variable definition, Throws on wrong node. + * Example: + * var x = 10; => x + * @varDeclNode {ASTNode} Variable declaration node + * @return {string} variable name + */ +function getDeclaredVariableName (varDeclNode) { + if (!isVariableDeclaration(varDeclNode)) throw new Error('staticAnalysisCommon.js: not a variable declaration') + return varDeclNode.attributes.name +} + +/** + * Returns state variable declaration nodes for a contract, Throws on wrong node. + * Example: + * contract foo { + * ... + * var y = true; + * var x = 10; => [y,x] + * @contractNode {ASTNode} Contract Definition node + * @return {list variable declaration} state variable node list + */ +function getStateVariableDeclarationsFormContractNode (contractNode) { + if (!isContractDefinition(contractNode)) throw new Error('staticAnalysisCommon.js: not a contract definition declaration') + if (!contractNode.children) return [] + return contractNode.children.filter((el) => isVariableDeclaration(el)) +} + +/** + * Returns parameter node for a function or modifier definition, Throws on wrong node. + * Example: + * function bar(uint a, uint b) => uint a, uint b + * @funcNode {ASTNode} Contract Definition node + * @return {parameterlist node} parameterlist node + */ +function getFunctionOrModifierDefinitionParameterPart (funcNode) { + if (!isFunctionDefinition(funcNode) && !isModifierDefinition(funcNode)) throw new Error('staticAnalysisCommon.js: not a function definition') + return funcNode.children[0] +} + +/** + * Returns return parameter node for a function or modifier definition, Throws on wrong node. + * Example: + * function bar(uint a, uint b) returns (bool a, bool b) => bool a, bool b + * @funcNode {ASTNode} Contract Definition node + * @return {parameterlist node} parameterlist node + */ +function getFunctionOrModifierDefinitionReturnParameterPart (funcNode) { + if (!isFunctionDefinition(funcNode) && !isModifierDefinition(funcNode)) throw new Error('staticAnalysisCommon.js: not a function definition') + return funcNode.children[1] +} + +/** + * Extracts the parameter types for a function type signature + * Example: + * function(uint a, uint b) returns (bool) => uint a, uint b + * @func {ASTNode} function call node + * @return {string} parameter signature + */ +function getFunctionCallTypeParameterType (func) { + var type = getFunctionCallType(func) + if (type.startsWith('function (')) { + var paramTypes = '' + var openPar = 1 + for (var x = 10; x < type.length; x++) { + var c = type.charAt(x) + if (c === '(') openPar++ + else if (c === ')') openPar-- + + if (openPar === 0) return paramTypes + + paramTypes += c + } + } else { + throw new Error('staticAnalysisCommon.js: cannot extract parameter types from function call') + } +} + +/** + * Returns the name of the library called, Throws on wrong node. + * Example: + * library set{...} + * contract foo { + * ... + * function () { set.union() => set} + * @funcCall {ASTNode} function call node + * @return {string} name of the lib defined + */ +function getLibraryCallContractName (funcCall) { + if (!isLibraryCall(funcCall)) throw new Error('staticAnalysisCommon.js: not an this library call Node') + return new RegExp(basicRegex.LIBRARYTYPE).exec(funcCall.children[0].attributes.type)[1] +} + +/** + * Returns the name of the function of a library call, Throws on wrong node. + * Example: + * library set{...} + * contract foo { + * ... + * function () { set.union() => uinion} + * @func {ASTNode} function call node + * @return {string} name of function called on the library + */ +function getLibraryCallMemberName (funcCall) { + if (!isLibraryCall(funcCall)) throw new Error('staticAnalysisCommon.js: not an library call Node') + return funcCall.attributes.member_name +} + +/** + * Returns full qualified name for a function call, Throws on wrong node. + * Example: + * contract foo { + * ... + * function bar(uint b) { } + * function baz() { + * bar(10) => foo.bar(uint) + * @func {ASTNode} function call node + * @func {ASTNode} contract defintion + * @return {string} full qualified identifier for the function call + */ +function getFullQualifiedFunctionCallIdent (contract, func) { + if (isLocalCall(func)) return getContractName(contract) + '.' + getLocalCallName(func) + '(' + getFunctionCallTypeParameterType(func) + ')' + else if (isThisLocalCall(func)) return getThisLocalCallContractName(func) + '.' + getThisLocalCallName(func) + '(' + getFunctionCallTypeParameterType(func) + ')' + else if (isSuperLocalCall(func)) return getContractName(contract) + '.' + getSuperLocalCallName(func) + '(' + getFunctionCallTypeParameterType(func) + ')' + else if (isExternalDirectCall(func)) return getExternalDirectCallContractName(func) + '.' + getExternalDirectCallMemberName(func) + '(' + getFunctionCallTypeParameterType(func) + ')' + else if (isLibraryCall(func)) return getLibraryCallContractName(func) + '.' + getLibraryCallMemberName(func) + '(' + getFunctionCallTypeParameterType(func) + ')' + else throw new Error('staticAnalysisCommon.js: Can not get function name form non function call node') +} + +function getFullQuallyfiedFuncDefinitionIdent (contract, func, paramTypes) { + return getContractName(contract) + '.' + getFunctionDefinitionName(func) + '(' + util.concatWithSeperator(paramTypes, ',') + ')' +} + +function getUnAssignedTopLevelBinOps (subScope) { + return subScope.children.filter(isBinaryOpInExpression) +} + +// #################### Trivial Node Identification + +function isFunctionDefinition (node) { + return nodeType(node, exactMatch(nodeTypes.FUNCTIONDEFINITION)) +} + +function isModifierDefinition (node) { + return nodeType(node, exactMatch(nodeTypes.MODIFIERDEFINITION)) +} + +function isModifierInvocation (node) { + return nodeType(node, exactMatch(nodeTypes.MODIFIERINVOCATION)) +} + +function isVariableDeclaration (node) { + return nodeType(node, exactMatch(nodeTypes.VARIABLEDECLARATION)) +} + +function isReturn (node) { + return nodeType(node, exactMatch(nodeTypes.RETURN)) +} + +function isInheritanceSpecifier (node) { + return nodeType(node, exactMatch(nodeTypes.INHERITANCESPECIFIER)) +} + +function isAssignment (node) { + return nodeType(node, exactMatch(nodeTypes.ASSIGNMENT)) +} + +function isContractDefinition (node) { + return nodeType(node, exactMatch(nodeTypes.CONTRACTDEFINITION)) +} + +function isInlineAssembly (node) { + return nodeType(node, exactMatch(nodeTypes.INLINEASSEMBLY)) +} + +function isNewExpression (node) { + return nodeType(node, exactMatch(nodeTypes.NEWEXPRESSION)) +} + +/** + * True if is Expression + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isExpressionStatement (node) { + return nodeType(node, exactMatch(nodeTypes.EXPRESSIONSTATEMENT)) +} + +/** + * True if is binaryop + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isBinaryOperation (node) { + return nodeType(node, exactMatch(nodeTypes.BINARYOPERATION)) +} + +// #################### Complex Node Identification + +/** + * True if function defintion has function body + * @funcNode {ASTNode} function defintion node + * @return {bool} + */ +function hasFunctionBody (funcNode) { + return findFirstSubNodeLTR(funcNode, exactMatch(nodeTypes.BLOCK)) != null +} + +/** + * True if node is a delete instruction of a dynamic array + * @node {ASTNode} node to check for + * @return {bool} + */ +function isDeleteOfDynamicArray (node) { + return isDeleteUnaryOperation(node) && isDynamicArrayAccess(node.children[0]) +} + +/** + * True if node is node is a ref to a dynamic array + * @node {ASTNode} node to check for + * @return {bool} + */ +function isDynamicArrayAccess (node) { + return node && nodeType(node, exactMatch(nodeTypes.IDENTIFIER)) && (node.attributes.type.endsWith('[] storage ref') || node.attributes.type === 'bytes storage ref' || node.attributes.type === 'string storage ref') +} + +/** + * True if call to code within the current contracts context including (delegate) library call + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isLocalCallGraphRelevantNode (node) { + return ((isLocalCall(node) || isSuperLocalCall(node) || isLibraryCall(node)) && !isBuiltinFunctionCall(node)) +} + +/** + * True if is builtin function like assert, sha3, erecover, ... + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isBuiltinFunctionCall (node) { + return (isLocalCall(node) && builtinFunctions[getLocalCallName(node) + '(' + getFunctionCallTypeParameterType(node) + ')'] === true) || isAbiNamespaceCall(node) +} + +/** + * True if is builtin function like assert, sha3, erecover, ... + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isAbiNamespaceCall (node) { + return Object.keys(abiNamespace).some((key) => abiNamespace.hasOwnProperty(key) && node.children && node.children[0] && isSpecialVariableAccess(node.children[0], abiNamespace[key])) +} + +/** + * True if node is a call to selfdestruct + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isSelfdestructCall (node) { + return isBuiltinFunctionCall(node) && getLocalCallName(node) === 'selfdestruct' +} + +/** + * True if node is a call to builtin assert(bool) + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isAssertCall (node) { + return isBuiltinFunctionCall(node) && getLocalCallName(node) === 'assert' +} + +/** + * True if node is a call to builtin require(bool) + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isRequireCall (node) { + return isBuiltinFunctionCall(node) && getLocalCallName(node) === 'require' +} + +/** + * True if is storage variable declaration + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isStorageVariableDeclaration (node) { + return isVariableDeclaration(node) && expressionType(node, basicRegex.REFTYPE) +} + +/** + * True if is interaction with external contract (change in context, no delegate calls) (send, call of other contracts) + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isInteraction (node) { + return isLLCall(node) || isLLSend(node) || isExternalDirectCall(node) || isTransfer(node) +} + +/** + * True if node changes state of a variable or is inline assembly (does not include check if it is a global state change, on a state variable) + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isEffect (node) { + return isAssignment(node) || isPlusPlusUnaryOperation(node) || isMinusMinusUnaryOperation(node) || isInlineAssembly(node) +} + +/** + * True if node changes state of a variable or is inline assembly (Checks if variable is a state variable via provided list) + * @node {ASTNode} some AstNode + * @node {list Variable declaration} state variable declaration currently in scope + * @return {bool} + */ +function isWriteOnStateVariable (effectNode, stateVariables) { + return isInlineAssembly(effectNode) || (isEffect(effectNode) && isStateVariable(getEffectedVariableName(effectNode), stateVariables)) +} + +/** + * True if there is a variable with name, name in stateVariables + * @node {ASTNode} some AstNode + * @node {list Variable declaration} state variable declaration currently in scope + * @return {bool} + */ +function isStateVariable (name, stateVariables) { + return stateVariables.some((item) => name === getDeclaredVariableName(item)) +} + +/** + * True if is function defintion that is flaged as constant + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isConstantFunction (node) { + return isFunctionDefinition(node) && ( + node.attributes.constant === true || + node.attributes.stateMutability === 'view' || + node.attributes.stateMutability === 'pure' + ) +} + +/** + * True if is function defintion has payable modifier + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isPayableFunction (node) { + return isFunctionDefinition(node) && ( + node.attributes.stateMutability === 'payable' + ) +} + +/** + * True if is constructor + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isConstructor (node) { + return isFunctionDefinition(node) && ( + node.attributes.isConstructor === true + ) +} + +/** + * True if node is integer division that truncates (not only int literals since those yield a rational value) + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isIntDivision (node) { + return isBinaryOperation(node) && operator(node, exactMatch(util.escapeRegExp('/'))) && expressionType(node, util.escapeRegExp('int')) +} + +/** + * True if is block / SubScope has top level binops (e.g. that are not assigned to anything, most of the time confused compare instead of assign) + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isSubScopeWithTopLevelUnAssignedBinOp (node) { + return nodeType(node, exactMatch(nodeTypes.BLOCK)) && node.children && node.children.some(isBinaryOpInExpression) || + isSubScopeStatement(node) && node.children && node.children.some(isBinaryOpInExpression) // Second Case for if without braces +} + +function isSubScopeStatement (node) { + return (nodeType(node, exactMatch(nodeTypes.IFSTATEMENT)) || + nodeType(node, exactMatch(nodeTypes.FORSTATEMENT)) || + nodeType(node, exactMatch(nodeTypes.WHILESTATEMENT)) || + nodeType(node, exactMatch(nodeTypes.DOWHILESTATEMENT))) && + minNrOfChildren(node, 2) && !nodeType(node.children[1], exactMatch(nodeTypes.BLOCK)) +} + +/** + * True if binary operation inside of expression statement + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isBinaryOpInExpression (node) { + return isExpressionStatement(node) && nrOfChildren(node, 1) && isBinaryOperation(node.children[0]) +} + +/** + * True if unary increment operation + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isPlusPlusUnaryOperation (node) { + return nodeType(node, exactMatch(nodeTypes.UNARYOPERATION)) && operator(node, exactMatch(util.escapeRegExp('++'))) +} + +/** + * True if unary delete operation + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isDeleteUnaryOperation (node) { + return nodeType(node, exactMatch(nodeTypes.UNARYOPERATION)) && operator(node, exactMatch(util.escapeRegExp('delete'))) +} + +/** + * True if unary decrement operation + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isMinusMinusUnaryOperation (node) { + return nodeType(node, exactMatch(nodeTypes.UNARYOPERATION)) && operator(node, exactMatch(util.escapeRegExp('--'))) +} + +/** + * True if all functions on a contract are implemented + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isFullyImplementedContract (node) { + return nodeType(node, exactMatch(nodeTypes.CONTRACTDEFINITION)) && node.attributes.fullyImplemented === true +} + +/** + * True if it is a library contract defintion + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isLibrary (node) { + return nodeType(node, exactMatch(nodeTypes.CONTRACTDEFINITION)) && node.attributes.isLibrary === true +} + +/** + * True if it is a local call to non const function + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isCallToNonConstLocalFunction (node) { + return isLocalCall(node) && !expressionType(node.children[0], basicRegex.CONSTANTFUNCTIONTYPE) +} + +/** + * True if it is a call to a library + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isLibraryCall (node) { + return isMemberAccess(node, basicRegex.FUNCTIONTYPE, undefined, basicRegex.LIBRARYTYPE, undefined) +} + +/** + * True if it is an external call via defined interface (not low level call) + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isExternalDirectCall (node) { + return isMemberAccess(node, basicRegex.EXTERNALFUNCTIONTYPE, undefined, basicRegex.CONTRACTTYPE, undefined) && !isThisLocalCall(node) && !isSuperLocalCall(node) +} + +/** + * True if access to block.timestamp via now alias + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isNowAccess (node) { + return nodeType(node, exactMatch(nodeTypes.IDENTIFIER)) && + expressionType(node, exactMatch(basicTypes.UINT)) && + name(node, exactMatch('now')) +} + +/** + * True if access to block.timestamp + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isBlockTimestampAccess (node) { + return isSpecialVariableAccess(node, specialVariables.BLOCKTIMESTAMP) +} + +/** + * True if access to block.blockhash + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isBlockBlockHashAccess (node) { + return isSpecialVariableAccess(node, specialVariables.BLOCKHASH) +} + +/** + * True if call to local function via this keyword + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isThisLocalCall (node) { + return isMemberAccess(node, basicRegex.FUNCTIONTYPE, exactMatch('this'), basicRegex.CONTRACTTYPE, undefined) +} + +/** + * True if access to local function via super keyword + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isSuperLocalCall (node) { + return isMemberAccess(node, basicRegex.FUNCTIONTYPE, exactMatch('super'), basicRegex.CONTRACTTYPE, undefined) +} + +/** + * True if call to local function + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isLocalCall (node) { + return nodeType(node, exactMatch(nodeTypes.FUNCTIONCALL)) && + minNrOfChildren(node, 1) && + nodeType(node.children[0], exactMatch(nodeTypes.IDENTIFIER)) && + expressionType(node.children[0], basicRegex.FUNCTIONTYPE) && + !expressionType(node.children[0], basicRegex.EXTERNALFUNCTIONTYPE) && + nrOfChildren(node.children[0], 0) +} + +/** + * True if low level call (send, call, delegatecall, callcode) + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isLowLevelCall (node) { + return isLLCall(node) || + isLLCallcode(node) || + isLLDelegatecall(node) || + isLLSend(node) +} + +/** + * True if low level send + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isLLSend (node) { + return isMemberAccess(node, + exactMatch(util.escapeRegExp(lowLevelCallTypes.SEND.type)), + undefined, exactMatch(basicTypes.ADDRESS), exactMatch(lowLevelCallTypes.SEND.ident)) +} + +/** + * True if low level call + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isLLCall (node) { + return isMemberAccess(node, + exactMatch(util.escapeRegExp(lowLevelCallTypes.CALL.type)), + undefined, exactMatch(basicTypes.ADDRESS), exactMatch(lowLevelCallTypes.CALL.ident)) +} + +/** + * True if low level callcode + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isLLCallcode (node) { + return isMemberAccess(node, + exactMatch(util.escapeRegExp(lowLevelCallTypes.CALLCODE.type)), + undefined, exactMatch(basicTypes.ADDRESS), exactMatch(lowLevelCallTypes.CALLCODE.ident)) +} + +/** + * True if low level delegatecall + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isLLDelegatecall (node) { + return isMemberAccess(node, + exactMatch(util.escapeRegExp(lowLevelCallTypes.DELEGATECALL.type)), + undefined, exactMatch(basicTypes.ADDRESS), exactMatch(lowLevelCallTypes.DELEGATECALL.ident)) +} + +/** + * True if transfer call + * @node {ASTNode} some AstNode + * @return {bool} + */ +function isTransfer (node) { + return isMemberAccess(node, + exactMatch(util.escapeRegExp(lowLevelCallTypes.TRANSFER.type)), + undefined, exactMatch(basicTypes.ADDRESS), exactMatch(lowLevelCallTypes.TRANSFER.ident)) +} + +// #################### Complex Node Identification - Private + +function isMemberAccess (node, retType, accessor, accessorType, memberName) { + return nodeType(node, exactMatch(nodeTypes.MEMBERACCESS)) && + expressionType(node, retType) && + name(node, memberName) && + nrOfChildren(node, 1) && + name(node.children[0], accessor) && + expressionType(node.children[0], accessorType) +} + +function isSpecialVariableAccess (node, varType) { + return isMemberAccess(node, exactMatch(util.escapeRegExp(varType.type)), varType.obj, varType.obj, varType.member) +} + +// #################### Node Identification Primitives + +function nrOfChildren (node, nr) { + return (node && (nr === undefined || nr === null)) || (node && nr === 0 && !node.children) || (node && node.children && node.children.length === nr) +} + +function minNrOfChildren (node, nr) { + return (node && (nr === undefined || nr === null)) || (node && nr === 0 && !node.children) || (node && node.children && node.children.length >= nr) +} + +function expressionType (node, typeRegex) { + return (node && !typeRegex) || (node && node.attributes && new RegExp(typeRegex).test(node.attributes.type)) +} + +function nodeType (node, typeRegex) { + return (node && !typeRegex) || (node && new RegExp(typeRegex).test(node.name)) +} + +function name (node, nameRegex) { + var regex = new RegExp(nameRegex) + return (node && !nameRegex) || (node && node.attributes && (regex.test(node.attributes.value) || regex.test(node.attributes.member_name))) +} + +function operator (node, opRegex) { + return (node && !opRegex) || (node && new RegExp(opRegex).test(node.attributes.operator)) +} + +// #################### Helpers + +function exactMatch (regexStr) { + return '^' + regexStr + '$' +} + +/** + * Builds an function signature as used in the AST of the solc-json AST + * @param {Array} paramTypes + * list of parameter type names + * @param {Array} returnTypes + * list of return type names + * @return {Boolean} isPayable + */ +function buildFunctionSignature (paramTypes, returnTypes, isPayable, additionalMods) { + return 'function (' + util.concatWithSeperator(paramTypes, ',') + ')' + ((isPayable) ? ' payable' : '') + ((additionalMods) ? ' ' + additionalMods : '') + ((returnTypes.length) ? ' returns (' + util.concatWithSeperator(returnTypes, ',') + ')' : '') +} + +/** + * Finds first node of a certain type under a specific node. + * @node {AstNode} node to start form + * @type {String} Type the ast node should have + * @return {AstNode} null or node found + */ +function findFirstSubNodeLTR (node, type) { + if (!node || !node.children) return null + for (let i = 0; i < node.children.length; ++i) { + var item = node.children[i] + if (nodeType(item, type)) return item + else { + var res = findFirstSubNodeLTR(item, type) + if (res) return res + } + } + return null +} + +module.exports = { + // #################### Trivial Getters + getType: getType, + // #################### Complex Getters + getThisLocalCallName: getThisLocalCallName, + getSuperLocalCallName: getSuperLocalCallName, + getFunctionCallType: getFunctionCallType, + getContractName: getContractName, + getEffectedVariableName: getEffectedVariableName, + getDeclaredVariableName: getDeclaredVariableName, + getLocalCallName: getLocalCallName, + getInheritsFromName: getInheritsFromName, + getExternalDirectCallContractName: getExternalDirectCallContractName, + getThisLocalCallContractName: getThisLocalCallContractName, + getExternalDirectCallMemberName: getExternalDirectCallMemberName, + getFunctionDefinitionName: getFunctionDefinitionName, + getFunctionCallTypeParameterType: getFunctionCallTypeParameterType, + getLibraryCallContractName: getLibraryCallContractName, + getLibraryCallMemberName: getLibraryCallMemberName, + getFullQualifiedFunctionCallIdent: getFullQualifiedFunctionCallIdent, + getFullQuallyfiedFuncDefinitionIdent: getFullQuallyfiedFuncDefinitionIdent, + getStateVariableDeclarationsFormContractNode: getStateVariableDeclarationsFormContractNode, + getFunctionOrModifierDefinitionParameterPart: getFunctionOrModifierDefinitionParameterPart, + getFunctionOrModifierDefinitionReturnParameterPart: getFunctionOrModifierDefinitionReturnParameterPart, + getUnAssignedTopLevelBinOps: getUnAssignedTopLevelBinOps, + + // #################### Complex Node Identification + isDeleteOfDynamicArray: isDeleteOfDynamicArray, + isAbiNamespaceCall: isAbiNamespaceCall, + isSpecialVariableAccess: isSpecialVariableAccess, + isDynamicArrayAccess: isDynamicArrayAccess, + isSubScopeWithTopLevelUnAssignedBinOp: isSubScopeWithTopLevelUnAssignedBinOp, + hasFunctionBody: hasFunctionBody, + isInteraction: isInteraction, + isEffect: isEffect, + isNowAccess: isNowAccess, + isBlockTimestampAccess: isBlockTimestampAccess, + isBlockBlockHashAccess: isBlockBlockHashAccess, + isThisLocalCall: isThisLocalCall, + isSuperLocalCall: isSuperLocalCall, + isLibraryCall: isLibraryCall, + isLocalCallGraphRelevantNode: isLocalCallGraphRelevantNode, + isLocalCall: isLocalCall, + isWriteOnStateVariable: isWriteOnStateVariable, + isStateVariable: isStateVariable, + isTransfer: isTransfer, + isLowLevelCall: isLowLevelCall, + isLowLevelCallInst: isLLCall, + isLowLevelCallcodeInst: isLLCallcode, + isLowLevelDelegatecallInst: isLLDelegatecall, + isLowLevelSendInst: isLLSend, + isExternalDirectCall: isExternalDirectCall, + isFullyImplementedContract: isFullyImplementedContract, + isLibrary: isLibrary, + isCallToNonConstLocalFunction: isCallToNonConstLocalFunction, + isPlusPlusUnaryOperation: isPlusPlusUnaryOperation, + isMinusMinusUnaryOperation: isMinusMinusUnaryOperation, + isBuiltinFunctionCall: isBuiltinFunctionCall, + isSelfdestructCall: isSelfdestructCall, + isAssertCall: isAssertCall, + isRequireCall: isRequireCall, + isIntDivision: isIntDivision, + + // #################### Trivial Node Identification + isDeleteUnaryOperation: isDeleteUnaryOperation, + isFunctionDefinition: isFunctionDefinition, + isModifierDefinition: isModifierDefinition, + isInheritanceSpecifier: isInheritanceSpecifier, + isModifierInvocation: isModifierInvocation, + isVariableDeclaration: isVariableDeclaration, + isStorageVariableDeclaration: isStorageVariableDeclaration, + isAssignment: isAssignment, + isContractDefinition: isContractDefinition, + isConstantFunction: isConstantFunction, + isPayableFunction: isPayableFunction, + isConstructor: isConstructor, + isInlineAssembly: isInlineAssembly, + isNewExpression: isNewExpression, + isReturn: isReturn, + + // #################### Constants + nodeTypes: nodeTypes, + basicTypes: basicTypes, + basicFunctionTypes: basicFunctionTypes, + lowLevelCallTypes: lowLevelCallTypes, + specialVariables: specialVariables, + helpers: { + nrOfChildren: nrOfChildren, + minNrOfChildren: minNrOfChildren, + expressionType: expressionType, + nodeType: nodeType, + name: name, + operator: operator, + buildFunctionSignature: buildFunctionSignature + } +} diff --git a/remix-analyzer/src/analysis/modules/thisLocal.js b/remix-analyzer/src/analysis/modules/thisLocal.js new file mode 100644 index 0000000000..2ca7a5975c --- /dev/null +++ b/remix-analyzer/src/analysis/modules/thisLocal.js @@ -0,0 +1,29 @@ +var name = 'This on local calls: ' +var desc = 'Invocation of local functions via this' +var categories = require('./categories') +var common = require('./staticAnalysisCommon') + +function thisLocal () { + this.warningNodes = [] +} + +thisLocal.prototype.visit = function (node) { + if (common.isThisLocalCall(node)) this.warningNodes.push(node) +} + +thisLocal.prototype.report = function (compilationResults) { + return this.warningNodes.map(function (item, i) { + return { + warning: 'Use of "this" for local functions: Never use this to call functions in the same contract, it only consumes more gas than normal local calls.', + location: item.src, + more: 'http://solidity.readthedocs.io/en/develop/control-structures.html#external-function-calls' + } + }) +} + +module.exports = { + name: name, + description: desc, + category: categories.GAS, + Module: thisLocal +} diff --git a/remix-analyzer/src/analysis/modules/txOrigin.js b/remix-analyzer/src/analysis/modules/txOrigin.js new file mode 100644 index 0000000000..46834e4f8e --- /dev/null +++ b/remix-analyzer/src/analysis/modules/txOrigin.js @@ -0,0 +1,35 @@ +var name = 'Transaction origin: ' +var desc = 'Warn if tx.origin is used' +var categories = require('./categories') + +function txOrigin () { + this.txOriginNodes = [] +} + +txOrigin.prototype.visit = function (node) { + if (node.name === 'MemberAccess' && + node.attributes.member_name === 'origin' && + node.attributes.type === 'address' && + node.children && node.children.length && + node.children[0].attributes.type === 'tx' && + node.children[0].attributes.value === 'tx') { + this.txOriginNodes.push(node) + } +} + +txOrigin.prototype.report = function () { + return this.txOriginNodes.map(function (item, i) { + return { + warning: `Use of tx.origin: "tx.origin" is useful only in very exceptional cases. + If you use it for authentication, you usually want to replace it by "msg.sender", because otherwise any contract you call can act on your behalf.`, + location: item.src + } + }) +} + +module.exports = { + name: name, + description: desc, + category: categories.SECURITY, + Module: txOrigin +} diff --git a/remix-analyzer/src/analysis/staticAnalysisRunner.js b/remix-analyzer/src/analysis/staticAnalysisRunner.js new file mode 100644 index 0000000000..8f3083d712 --- /dev/null +++ b/remix-analyzer/src/analysis/staticAnalysisRunner.js @@ -0,0 +1,57 @@ +'use strict' +var AstWalker = require('remix-lib').AstWalker +var list = require('./modules/list') + +function staticAnalysisRunner () { +} + +staticAnalysisRunner.prototype.run = function (compilationResult, toRun, callback) { + var self = this + var modules = toRun.map(function (i) { + var m = self.modules()[i] + return { 'name': m.name, 'mod': new m.Module() } + }) + + this.runWithModuleList(compilationResult, modules, callback) +} + +staticAnalysisRunner.prototype.runWithModuleList = function (compilationResult, modules, callback) { + var reports = [] + // Also provide convenience analysis via the AST walker. + var walker = new AstWalker() + for (var k in compilationResult.sources) { + walker.walk(compilationResult.sources[k].legacyAST, {'*': function (node) { + modules.map(function (item, i) { + if (item.mod.visit !== undefined) { + try { + item.mod.visit(node) + } catch (e) { + reports.push({ + name: item.name, report: [{ warning: 'INTERNAL ERROR in module ' + item.name + ' ' + e.message, error: e.stack }] + }) + } + } + }) + return true + }}) + } + + // Here, modules can just collect the results from the AST walk, + // but also perform new analysis. + reports = reports.concat(modules.map(function (item, i) { + var report = null + try { + report = item.mod.report(compilationResult) + } catch (e) { + report = [{ warning: 'INTERNAL ERROR in module ' + item.name + ' ' + e.message, error: e.stack }] + } + return { name: item.name, report: report } + })) + callback(reports) +} + +staticAnalysisRunner.prototype.modules = function () { + return list +} + +module.exports = staticAnalysisRunner diff --git a/remix-analyzer/test/analysis/staticAnalysisCommon-test.js b/remix-analyzer/test/analysis/staticAnalysisCommon-test.js new file mode 100644 index 0000000000..7d4fc4a95e --- /dev/null +++ b/remix-analyzer/test/analysis/staticAnalysisCommon-test.js @@ -0,0 +1,2138 @@ +var test = require('tape') + +var common = require('../../src/analysis/modules/staticAnalysisCommon') + +function escapeRegExp (str) { + return str.replace(/[-[\]/{}()+?.\\^$|]/g, '\\$&') +} + +test('staticAnalysisCommon.helpers.buildFunctionSignature', function (t) { + t.plan(9) + + t.equal(common.helpers.buildFunctionSignature([common.basicTypes.UINT, common.basicTypes.ADDRESS], [common.basicTypes.BOOL], false), + 'function (uint256,address) returns (bool)', + 'two params and return value without payable') + + t.equal(common.helpers.buildFunctionSignature([common.basicTypes.UINT, common.basicTypes.ADDRESS], [common.basicTypes.BOOL], false, 'pure'), + 'function (uint256,address) pure returns (bool)', + 'two params and return value without payable but pure') + + t.equal(common.helpers.buildFunctionSignature([common.basicTypes.UINT, common.basicTypes.ADDRESS], [common.basicTypes.BOOL], true, 'pure'), + 'function (uint256,address) payable pure returns (bool)', + 'two params and return value without payable but pure') + + t.equal(common.helpers.buildFunctionSignature([common.basicTypes.UINT, common.basicTypes.BYTES32, common.basicTypes.BYTES32], [], true), + 'function (uint256,bytes32,bytes32) payable', + 'three params and no return with payable') + + t.equal(common.helpers.buildFunctionSignature([common.basicTypes.BOOL], [common.basicTypes.BYTES32, common.basicTypes.ADDRESS], true), + 'function (bool) payable returns (bytes32,address)', + 'one param and two return values with payable') + + t.equal(common.lowLevelCallTypes.CALL.type, + 'function () payable returns (bool)', + 'check fixed call type') + + t.equal(common.lowLevelCallTypes.CALLCODE.type, + 'function () payable returns (bool)', + 'check fixed callcode type') + + t.equal(common.lowLevelCallTypes.SEND.type, + 'function (uint256) returns (bool)', + 'check fixed send type') + + t.equal(common.lowLevelCallTypes.DELEGATECALL.type, + 'function () returns (bool)', + 'check fixed call type') +}) + +// #################### Node Identification Primitives + +test('staticAnalysisCommon.helpers.name', function (t) { + t.plan(9) + var node = { attributes: { value: 'now' } } + var node2 = { attributes: { member_name: 'call' } } + + t.ok(common.helpers.name(node, 'now'), 'should work for values') + t.ok(common.helpers.name(node2, 'call'), 'should work for member_name') + t.ok(common.helpers.name(node2, '.all'), 'regex should work') + + lowlevelAccessersCommon(t, common.helpers.name, node) +}) + +test('staticAnalysisCommon.helpers.operator', function (t) { + t.plan(10) + var node = { attributes: { operator: '++' } } + var node2 = { attributes: { operator: '+++' } } + + var escapedPP = escapeRegExp('++') + var escapedPPExact = `^${escapedPP}$` + + t.ok(common.helpers.operator(node, escapedPPExact), 'should work for ++') + t.notOk(common.helpers.operator(node2, escapedPPExact), 'should not work for +++') + t.ok(common.helpers.operator(node, escapedPP), 'should work for ++') + t.ok(common.helpers.operator(node2, escapedPP), 'should work for +++') + + lowlevelAccessersCommon(t, common.helpers.operator, node) +}) + +test('staticAnalysisCommon.helpers.nodeType', function (t) { + t.plan(9) + var node = { name: 'Identifier', attributes: { name: 'now' } } + var node2 = { name: 'FunctionCall', attributes: { member_name: 'call' } } + + t.ok(common.helpers.nodeType(node, common.nodeTypes.IDENTIFIER), 'should work for ident') + t.ok(common.helpers.nodeType(node2, common.nodeTypes.FUNCTIONCALL), 'should work for funcall') + t.ok(common.helpers.nodeType(node2, '^F'), 'regex should work for funcall') + + lowlevelAccessersCommon(t, common.helpers.nodeType, node) +}) + +test('staticAnalysisCommon.helpers.expressionType', function (t) { + t.plan(9) + var node = { name: 'Identifier', attributes: { value: 'now', type: 'uint256' } } + var node2 = { name: 'FunctionCall', attributes: { member_name: 'call', type: 'function () payable returns (bool)' } } + + t.ok(common.helpers.expressionType(node, common.basicTypes.UINT), 'should work for ident') + t.ok(common.helpers.expressionType(node2, escapeRegExp(common.basicFunctionTypes.CALL)), 'should work for funcall') + t.ok(common.helpers.expressionType(node2, '^function \\('), 'regex should work') + + lowlevelAccessersCommon(t, common.helpers.expressionType, node) +}) + +test('staticAnalysisCommon.helpers.nrOfChildren', function (t) { + t.plan(10) + var node = { name: 'Identifier', children: ['a', 'b'], attributes: { value: 'now', type: 'uint256' } } + var node2 = { name: 'FunctionCall', children: [], attributes: { member_name: 'call', type: 'function () payable returns (bool)' } } + var node3 = { name: 'FunctionCall', attributes: { member_name: 'call', type: 'function () payable returns (bool)' } } + + t.ok(common.helpers.nrOfChildren(node, 2), 'should work for 2 children') + t.notOk(common.helpers.nrOfChildren(node, '1+2'), 'regex should not work') + t.ok(common.helpers.nrOfChildren(node2, 0), 'should work for 0 children') + t.ok(common.helpers.nrOfChildren(node3, 0), 'should work without children arr') + + lowlevelAccessersCommon(t, common.helpers.nrOfChildren, node) +}) + +test('staticAnalysisCommon.helpers.minNrOfChildren', function (t) { + t.plan(13) + var node = { name: 'Identifier', children: ['a', 'b'], attributes: { value: 'now', type: 'uint256' } } + var node2 = { name: 'FunctionCall', children: [], attributes: { member_name: 'call', type: 'function () payable returns (bool)' } } + var node3 = { name: 'FunctionCall', attributes: { member_name: 'call', type: 'function () payable returns (bool)' } } + + t.ok(common.helpers.minNrOfChildren(node, 2), 'should work for 2 children') + t.ok(common.helpers.minNrOfChildren(node, 1), 'should work for 1 children') + t.ok(common.helpers.minNrOfChildren(node, 0), 'should work for 0 children') + t.notOk(common.helpers.minNrOfChildren(node, 3), 'has less than 3 children') + t.notOk(common.helpers.minNrOfChildren(node, '1+2'), 'regex should not work') + t.ok(common.helpers.minNrOfChildren(node2, 0), 'should work for 0 children') + t.ok(common.helpers.minNrOfChildren(node3, 0), 'should work without children arr') + + lowlevelAccessersCommon(t, common.helpers.minNrOfChildren, node) +}) + +function lowlevelAccessersCommon (t, f, someNode) { + t.ok(f(someNode), 'always ok if type is undefinded') + t.ok(f(someNode, undefined), 'always ok if name is undefinded 2') + t.notOk(f(null, undefined), 'false on no node') + t.notOk(f(null, 'call'), 'false on no node') + t.notOk(f(undefined, null), 'false on no node') + t.notOk(f(), 'false on no params') +} + +// #################### Trivial Getter Test + +test('staticAnalysisCommon.getType', function (t) { + t.plan(3) + var node = { name: 'Identifier', children: ['a', 'b'], attributes: { value: 'now', type: 'uint256' } } + var node2 = { name: 'FunctionCall', children: [], attributes: { member_name: 'call', type: 'function () payable returns (bool)' } } + var node3 = { name: 'FunctionCall', attributes: { member_name: 'call', type: 'function () payable returns (bool)' } } + + t.ok(common.getType(node) === 'uint256', 'gettype should work for different nodes') + t.ok(common.getType(node2) === 'function () payable returns (bool)', 'gettype should work for different nodes') + t.ok(common.getType(node3) === 'function () payable returns (bool)', 'gettype should work for different nodes') +}) + +// #################### Complex Getter Test + +test('staticAnalysisCommon.getFunctionCallType', function (t) { + t.plan(5) + var localCall = { + 'attributes': { + 'type': 'tuple()', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'type': 'function (struct Ballot.Voter storage pointer)', + 'value': 'bli' + }, + 'id': 37, + 'name': 'Identifier', + 'src': '540:3:0' + }, + { + 'attributes': { + 'type': 'struct Ballot.Voter storage pointer', + 'value': 'x' + }, + 'id': 38, + 'name': 'Identifier', + 'src': '544:1:0' + } + ], + 'id': 39, + 'name': 'FunctionCall', + 'src': '540:6:0' + } + var thisLocalCall = { name: 'MemberAccess', children: [ { attributes: { value: 'this', type: 'contract test' }, name: 'Identifier' } ], attributes: { value: 'b', type: 'function (bytes32,address) returns (bool)' } } + var externalDirect = { + attributes: { + member_name: 'info', + type: 'function () payable external returns (uint256)' + }, + children: [ + { + attributes: { + type: 'contract InfoFeed', + value: 'f' + }, + id: 30, + name: 'Identifier', + src: '405:1:0' + } + ], + id: 32, + name: 'MemberAccess', + src: '405:6:0' + } + var libCall = { + 'attributes': { + 'member_name': 'insert', + 'type': 'function (struct Set.Data storage pointer,uint256) returns (bool)' + }, + 'children': [ + { + 'attributes': { + 'type': 'type(library Set)', + 'value': 'Set' + }, + 'name': 'Identifier' + } + ], + 'name': 'MemberAccess' + } + t.equal(common.getFunctionCallType(libCall), 'function (struct Set.Data storage pointer,uint256) returns (bool)', 'this lib call returns correct type') + t.equal(common.getFunctionCallType(thisLocalCall), 'function (bytes32,address) returns (bool)', 'this local call returns correct type') + t.equal(common.getFunctionCallType(externalDirect), 'function () payable external returns (uint256)', 'external direct call returns correct type') + t.equal(common.getFunctionCallType(localCall), 'function (struct Ballot.Voter storage pointer)', 'local call returns correct type') + t.throws(() => common.getFunctionCallType({ name: 'MemberAccess' }), undefined, 'throws on wrong type') +}) + +test('staticAnalysisCommon.getEffectedVariableName', function (t) { + t.plan(3) + var assignment = { + 'attributes': { + 'operator': '=', + 'type': 'uint256' + }, + 'children': [ + { + 'attributes': { + 'type': 'uint256' + }, + 'children': [ + { + 'attributes': { + 'type': 'mapping(address => uint256)', + 'value': 'c' + }, + 'id': 61, + 'name': 'Identifier', + 'src': '873:1:0' + }, + { + 'attributes': { + 'member_name': 'sender', + 'type': 'address' + }, + 'children': [ + { + 'attributes': { + 'type': 'msg', + 'value': 'msg' + }, + 'id': 62, + 'name': 'Identifier', + 'src': '875:3:0' + } + ], + 'id': 63, + 'name': 'MemberAccess', + 'src': '875:10:0' + } + ], + 'id': 64, + 'name': 'IndexAccess', + 'src': '873:13:0' + }, + { + 'attributes': { + 'hexvalue': '30', + 'subdenomination': null, + 'token': null, + 'type': 'int_const 0', + 'value': '0' + }, + 'id': 65, + 'name': 'Literal', + 'src': '889:1:0' + } + ], + 'id': 66, + 'name': 'Assignment', + 'src': '873:17:0' + } + var inlineAssembly = { + 'children': [ + ], + 'id': 21, + 'name': 'InlineAssembly', + 'src': '809:41:0' + } + t.throws(() => common.getEffectedVariableName(inlineAssembly), 'staticAnalysisCommon.js: not an effect Node or inline assembly', 'get from inline assembly should throw') + t.ok(common.getEffectedVariableName(assignment) === 'c', 'get right name for assignment') + t.throws(() => common.getEffectedVariableName({ name: 'MemberAccess' }), undefined, 'should throw on all other nodes') +}) + +test('staticAnalysisCommon.getLocalCallName', function (t) { + t.plan(3) + var localCall = { + 'attributes': { + 'type': 'tuple()', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'type': 'function (struct Ballot.Voter storage pointer)', + 'value': 'bli' + }, + 'id': 37, + 'name': 'Identifier', + 'src': '540:3:0' + }, + { + 'attributes': { + 'type': 'struct Ballot.Voter storage pointer', + 'value': 'x' + }, + 'id': 38, + 'name': 'Identifier', + 'src': '544:1:0' + } + ], + 'id': 39, + 'name': 'FunctionCall', + 'src': '540:6:0' + } + var thisLocalCall = { name: 'MemberAccess', children: [ { attributes: { value: 'this', type: 'contract test' }, name: 'Identifier' } ], attributes: { value: 'b', type: 'function (bytes32,address) returns (bool)' } } + var externalDirect = { + attributes: { + member_name: 'info', + type: 'function () payable external returns (uint256)' + }, + children: [ + { + attributes: { + type: 'contract InfoFeed', + value: 'f' + }, + id: 30, + name: 'Identifier', + src: '405:1:0' + } + ], + id: 32, + name: 'MemberAccess', + src: '405:6:0' + } + t.ok(common.getLocalCallName(localCall) === 'bli', 'getLocal call name from node') + t.throws(() => common.getLocalCallName(externalDirect), undefined, 'throws on other nodes') + t.throws(() => common.getLocalCallName(thisLocalCall), undefined, 'throws on other nodes') +}) + +test('staticAnalysisCommon.getThisLocalCallName', function (t) { + t.plan(3) + var localCall = { + 'attributes': { + 'type': 'tuple()', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'type': 'function (struct Ballot.Voter storage pointer)', + 'value': 'bli' + }, + 'id': 37, + 'name': 'Identifier', + 'src': '540:3:0' + }, + { + 'attributes': { + 'type': 'struct Ballot.Voter storage pointer', + 'value': 'x' + }, + 'id': 38, + 'name': 'Identifier', + 'src': '544:1:0' + } + ], + 'id': 39, + 'name': 'FunctionCall', + 'src': '540:6:0' + } + var thisLocalCall = { name: 'MemberAccess', children: [ { attributes: { value: 'this', type: 'contract test' }, name: 'Identifier' } ], attributes: { value: 'b', type: 'function (bytes32,address) returns (bool)' } } + var externalDirect = { + attributes: { + member_name: 'info', + type: 'function () payable external returns (uint256)' + }, + children: [ + { + attributes: { + type: 'contract InfoFeed', + value: 'f' + }, + id: 30, + name: 'Identifier', + src: '405:1:0' + } + ], + id: 32, + name: 'MemberAccess', + src: '405:6:0' + } + t.ok(common.getThisLocalCallName(thisLocalCall) === 'b', 'get this Local call name from node') + t.throws(() => common.getThisLocalCallName(externalDirect), undefined, 'throws on other nodes') + t.throws(() => common.getThisLocalCallName(localCall), undefined, 'throws on other nodes') +}) + +test('staticAnalysisCommon.getSuperLocalCallName', function (t) { + t.plan(4) + var superLocal = { + 'attributes': { + 'member_name': 'duper', + 'type': 'function ()' + }, + 'children': [ + { + 'attributes': { + 'type': 'contract super a', + 'value': 'super' + }, + 'name': 'Identifier' + } + ], + 'name': 'MemberAccess' + } + var localCall = { + 'attributes': { + 'type': 'tuple()', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'type': 'function (struct Ballot.Voter storage pointer)', + 'value': 'bli' + }, + 'id': 37, + 'name': 'Identifier', + 'src': '540:3:0' + }, + { + 'attributes': { + 'type': 'struct Ballot.Voter storage pointer', + 'value': 'x' + }, + 'id': 38, + 'name': 'Identifier', + 'src': '544:1:0' + } + ], + 'id': 39, + 'name': 'FunctionCall', + 'src': '540:6:0' + } + var thisLocalCall = { name: 'MemberAccess', children: [ { attributes: { value: 'this', type: 'contract test' }, name: 'Identifier' } ], attributes: { value: 'b', type: 'function (bytes32,address) returns (bool)' } } + var externalDirect = { + attributes: { + member_name: 'info', + type: 'function () payable external returns (uint256)' + }, + children: [ + { + attributes: { + type: 'contract InfoFeed', + value: 'f' + }, + id: 30, + name: 'Identifier', + src: '405:1:0' + } + ], + id: 32, + name: 'MemberAccess', + src: '405:6:0' + } + t.equal(common.getSuperLocalCallName(superLocal), 'duper', 'get local name from super local call') + t.throws(() => common.getSuperLocalCallName(thisLocalCall), 'throws on other nodes') + t.throws(() => common.getSuperLocalCallName(externalDirect), undefined, 'throws on other nodes') + t.throws(() => common.getSuperLocalCallName(localCall), undefined, 'throws on other nodes') +}) + +test('staticAnalysisCommon.getExternalDirectCallContractName', function (t) { + t.plan(3) + var localCall = { + 'attributes': { + 'type': 'tuple()', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'type': 'function (struct Ballot.Voter storage pointer)', + 'value': 'bli' + }, + 'id': 37, + 'name': 'Identifier', + 'src': '540:3:0' + }, + { + 'attributes': { + 'type': 'struct Ballot.Voter storage pointer', + 'value': 'x' + }, + 'id': 38, + 'name': 'Identifier', + 'src': '544:1:0' + } + ], + 'id': 39, + 'name': 'FunctionCall', + 'src': '540:6:0' + } + var thisLocalCall = { name: 'MemberAccess', children: [ { attributes: { value: 'this', type: 'contract test' }, name: 'Identifier' } ], attributes: { value: 'b', type: 'function (bytes32,address) returns (bool)' } } + var externalDirect = { + attributes: { + member_name: 'info', + type: 'function () payable external returns (uint256)' + }, + children: [ + { + attributes: { + type: 'contract InfoFeed', + value: 'f' + }, + id: 30, + name: 'Identifier', + src: '405:1:0' + } + ], + id: 32, + name: 'MemberAccess', + src: '405:6:0' + } + + t.ok(common.getExternalDirectCallContractName(externalDirect) === 'InfoFeed', 'external direct call contract name from node') + t.throws(() => common.getExternalDirectCallContractName(thisLocalCall), undefined, 'throws on other nodes') + t.throws(() => common.getExternalDirectCallContractName(localCall), undefined, 'throws on other nodes') +}) + +test('staticAnalysisCommon.getThisLocalCallContractName', function (t) { + t.plan(3) + var localCall = { + 'attributes': { + 'type': 'tuple()', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'type': 'function (struct Ballot.Voter storage pointer)', + 'value': 'bli' + }, + 'id': 37, + 'name': 'Identifier', + 'src': '540:3:0' + }, + { + 'attributes': { + 'type': 'struct Ballot.Voter storage pointer', + 'value': 'x' + }, + 'id': 38, + 'name': 'Identifier', + 'src': '544:1:0' + } + ], + 'id': 39, + 'name': 'FunctionCall', + 'src': '540:6:0' + } + var thisLocalCall = { name: 'MemberAccess', children: [ { attributes: { value: 'this', type: 'contract test' }, name: 'Identifier' } ], attributes: { value: 'b', type: 'function (bytes32,address) returns (bool)' } } + var externalDirect = { + attributes: { + member_name: 'info', + type: 'function () payable external returns (uint256)' + }, + children: [ + { + attributes: { + type: 'contract InfoFeed', + value: 'f' + }, + id: 30, + name: 'Identifier', + src: '405:1:0' + } + ], + id: 32, + name: 'MemberAccess', + src: '405:6:0' + } + t.ok(common.getThisLocalCallContractName(thisLocalCall) === 'test', 'this local call contract name from node') + t.throws(() => common.getThisLocalCallContractName(localCall), undefined, 'throws on other nodes') + t.throws(() => common.getThisLocalCallContractName(externalDirect), undefined, 'throws on other nodes') +}) + +test('staticAnalysisCommon.getExternalDirectCallMemberName', function (t) { + t.plan(3) + var localCall = { + 'attributes': { + 'type': 'tuple()', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'type': 'function (struct Ballot.Voter storage pointer)', + 'value': 'bli' + }, + 'id': 37, + 'name': 'Identifier', + 'src': '540:3:0' + }, + { + 'attributes': { + 'type': 'struct Ballot.Voter storage pointer', + 'value': 'x' + }, + 'id': 38, + 'name': 'Identifier', + 'src': '544:1:0' + } + ], + 'id': 39, + 'name': 'FunctionCall', + 'src': '540:6:0' + } + var thisLocalCall = { name: 'MemberAccess', children: [ { attributes: { value: 'this', type: 'contract test' }, name: 'Identifier' } ], attributes: { value: 'b', type: 'function (bytes32,address) returns (bool)' } } + var externalDirect = { + attributes: { + member_name: 'info', + type: 'function () payable external returns (uint256)' + }, + children: [ + { + attributes: { + type: 'contract InfoFeed', + value: 'f' + }, + id: 30, + name: 'Identifier', + src: '405:1:0' + } + ], + id: 32, + name: 'MemberAccess', + src: '405:6:0' + } + t.ok(common.getExternalDirectCallMemberName(externalDirect) === 'info', 'external direct call name from node') + t.throws(() => common.getExternalDirectCallMemberName(thisLocalCall), undefined, 'throws on other nodes') + t.throws(() => common.getExternalDirectCallMemberName(localCall), undefined, 'throws on other nodes') +}) + +test('staticAnalysisCommon.getContractName', function (t) { + t.plan(2) + var contract = { name: 'ContractDefinition', attributes: { name: 'baz' } } + t.ok(common.getContractName(contract) === 'baz', 'returns right contract name') + t.throws(() => common.getContractName({ name: 'InheritanceSpecifier' }), undefined, 'throws on other nodes') +}) + +test('staticAnalysisCommon.getFunctionDefinitionName', function (t) { + t.plan(2) + var func = { name: 'FunctionDefinition', attributes: { name: 'foo' } } + t.ok(common.getFunctionDefinitionName(func) === 'foo', 'returns right contract name') + t.throws(() => common.getFunctionDefinitionName({ name: 'InlineAssembly' }), undefined, 'throws on other nodes') +}) + +test('staticAnalysisCommon.getInheritsFromName', function (t) { + t.plan(2) + var inh = { + 'children': [ + { + 'attributes': { + 'name': 'r' + }, + 'id': 7, + 'name': 'UserDefinedTypeName', + 'src': '84:1:0' + } + ], + 'id': 8, + 'name': 'InheritanceSpecifier', + 'src': '84:1:0' + } + t.ok(common.getInheritsFromName(inh) === 'r', 'returns right contract name') + t.throws(() => common.getInheritsFromName({ name: 'ElementaryTypeName' }), undefined, 'throws on other nodes') +}) + +test('staticAnalysisCommon.getDeclaredVariableName', function (t) { + t.plan(2) + var node1 = { + 'attributes': { + 'name': 'x', + 'type': 'struct Ballot.Voter storage pointer' + }, + 'children': [ + { + 'attributes': { + 'name': 'Voter' + }, + 'id': 43, + 'name': 'UserDefinedTypeName', + 'src': '604:5:0' + } + ], + 'id': 44, + 'name': 'VariableDeclaration', + 'src': '604:15:0' + } + t.ok(common.getDeclaredVariableName(node1) === 'x', 'extract right variable name') + node1.name = 'FunctionCall' + t.throws(() => common.getDeclaredVariableName(node1) === 'x', undefined, 'throw if wrong node') +}) + +test('staticAnalysisCommon.getStateVariableDeclarationsFormContractNode', function (t) { + t.plan(4) + var contract = { + 'attributes': { + 'fullyImplemented': true, + 'isLibrary': false, + 'linearizedBaseContracts': [ + 274 + ], + 'name': 'Ballot' + }, + 'children': [ + { + 'attributes': { + 'name': 'Voter' + }, + 'children': [], + 'name': 'StructDefinition' + }, + { + 'attributes': { + 'name': 'Proposal' + }, + 'children': [], + 'name': 'StructDefinition' + }, + { + 'attributes': { + 'name': 'chairperson', + 'type': 'address' + }, + 'children': [ + { + 'attributes': { + 'name': 'address' + }, + 'name': 'ElementaryTypeName' + } + ], + 'name': 'VariableDeclaration' + }, + { + 'attributes': { + 'name': 'voters', + 'type': 'mapping(address => struct Ballot.Voter storage ref)' + }, + 'children': [ + { + 'children': [ + { + 'attributes': { + 'name': 'address' + }, + 'name': 'ElementaryTypeName' + }, + { + 'attributes': { + 'name': 'Voter' + }, + 'name': 'UserDefinedTypeName' + } + ], + 'name': 'Mapping' + } + ], + 'name': 'VariableDeclaration' + }, + { + 'attributes': { + 'name': 'proposals', + 'type': 'struct Ballot.Proposal storage ref[] storage ref' + }, + 'children': [ + { + 'children': [ + { + 'attributes': { + 'name': 'Proposal' + }, + 'name': 'UserDefinedTypeName' + } + ], + 'name': 'ArrayTypeName' + } + ], + 'name': 'VariableDeclaration' + }, + { + 'attributes': { + 'constant': false, + 'name': 'Ballot', + 'payable': false, + 'visibility': 'public' + }, + 'children': [], + 'name': 'FunctionDefinition' + }, + { + 'attributes': { + 'constant': false, + 'name': 'giveRightToVote', + 'payable': false, + 'visibility': 'public' + }, + 'children': [], + 'name': 'FunctionDefinition' + } + ], + 'name': 'ContractDefinition' + } + var res = common.getStateVariableDeclarationsFormContractNode(contract).map(common.getDeclaredVariableName) + + t.ok(res[0] === 'chairperson', 'var 1 should be ') + t.ok(res[1] === 'voters', 'var 2 should be ') + t.ok(res[2] === 'proposals', 'var 3 should be ') + t.ok(res[3] === undefined, 'var 4 should be undefined') +}) + +test('staticAnalysisCommon.getFunctionOrModifierDefinitionParameterPart', function (t) { + t.plan(2) + var funDef = { + 'attributes': { + 'constant': true, + 'name': 'winnerName', + 'payable': false, + 'visibility': 'public' + }, + 'children': [ + { + 'children': [ + ], + 'name': 'ParameterList' + }, + { + 'children': [], + 'name': 'ParameterList' + }, + { + 'children': [], + 'name': 'Block' + } + ], + 'name': 'FunctionDefinition' + } + t.ok(common.helpers.nodeType(common.getFunctionOrModifierDefinitionParameterPart(funDef), 'ParameterList'), 'should return a parameterList') + t.throws(() => common.getFunctionOrModifierDefinitionParameterPart({ name: 'SourceUnit' }), undefined, 'throws on other nodes') +}) + +test('staticAnalysisCommon.getFunctionCallTypeParameterType', function (t) { + t.plan(4) + var localCall = { + 'attributes': { + 'type': 'tuple()', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'type': 'function (struct Ballot.Voter storage pointer)', + 'value': 'bli' + }, + 'id': 37, + 'name': 'Identifier', + 'src': '540:3:0' + }, + { + 'attributes': { + 'type': 'struct Ballot.Voter storage pointer', + 'value': 'x' + }, + 'id': 38, + 'name': 'Identifier', + 'src': '544:1:0' + } + ], + 'id': 39, + 'name': 'FunctionCall', + 'src': '540:6:0' + } + var thisLocalCall = { name: 'MemberAccess', children: [ { attributes: { value: 'this', type: 'contract test' }, name: 'Identifier' } ], attributes: { value: 'b', type: 'function (bytes32,address) returns (bool)' } } + var externalDirect = { + attributes: { + member_name: 'info', + type: 'function () payable external returns (uint256)' + }, + children: [ + { + attributes: { + type: 'contract InfoFeed', + value: 'f' + }, + id: 30, + name: 'Identifier', + src: '405:1:0' + } + ], + id: 32, + name: 'MemberAccess', + src: '405:6:0' + } + t.ok(common.getFunctionCallTypeParameterType(thisLocalCall) === 'bytes32,address', 'this local call returns correct type') + t.ok(common.getFunctionCallTypeParameterType(externalDirect) === '', 'external direct call returns correct type') + t.ok(common.getFunctionCallTypeParameterType(localCall) === 'struct Ballot.Voter storage pointer', 'local call returns correct type') + t.throws(() => common.getFunctionCallTypeParameterType({ name: 'MemberAccess' }), undefined, 'throws on wrong type') +}) + +test('staticAnalysisCommon.getLibraryCallContractName', function (t) { + t.plan(2) + var node = { + 'attributes': { + 'member_name': 'insert', + 'type': 'function (struct Set.Data storage pointer,uint256) returns (bool)' + }, + 'children': [ + { + 'attributes': { + 'type': 'type(library Set)', + 'value': 'Set' + }, + 'name': 'Identifier' + } + ], + 'name': 'MemberAccess' + } + t.equal(common.getLibraryCallContractName(node), 'Set', 'should return correct contract name') + t.throws(() => common.getLibraryCallContractName({ name: 'Identifier' }), undefined, 'should throw on wrong node') +}) + +test('staticAnalysisCommon.getLibraryCallMemberName', function (t) { + t.plan(2) + var node = { + 'attributes': { + 'member_name': 'insert', + 'type': 'function (struct Set.Data storage pointer,uint256) returns (bool)' + }, + 'children': [ + { + 'attributes': { + 'type': 'type(library Set)', + 'value': 'Set' + }, + 'name': 'Identifier' + } + ], + 'name': 'MemberAccess' + } + t.equal(common.getLibraryCallMemberName(node), 'insert', 'should return correct member name') + t.throws(() => common.getLibraryCallMemberName({ name: 'Identifier' }), undefined, 'should throw on wrong node') +}) + +test('staticAnalysisCommon.getFullQualifiedFunctionCallIdent', function (t) { + t.plan(4) + var contract = { name: 'ContractDefinition', attributes: { name: 'baz' } } + var localCall = { + 'attributes': { + 'type': 'tuple()', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'type': 'function (struct Ballot.Voter storage pointer)', + 'value': 'bli' + }, + 'id': 37, + 'name': 'Identifier', + 'src': '540:3:0' + }, + { + 'attributes': { + 'type': 'struct Ballot.Voter storage pointer', + 'value': 'x' + }, + 'id': 38, + 'name': 'Identifier', + 'src': '544:1:0' + } + ], + 'id': 39, + 'name': 'FunctionCall', + 'src': '540:6:0' + } + var thisLocalCall = { name: 'MemberAccess', children: [ { attributes: { value: 'this', type: 'contract test' }, name: 'Identifier' } ], attributes: { value: 'b', type: 'function (bytes32,address) returns (bool)' } } + var externalDirect = { + attributes: { + member_name: 'info', + type: 'function () payable external returns (uint256)' + }, + children: [ + { + attributes: { + type: 'contract InfoFeed', + value: 'f' + }, + id: 30, + name: 'Identifier', + src: '405:1:0' + } + ], + id: 32, + name: 'MemberAccess', + src: '405:6:0' + } + + t.ok(common.getFullQualifiedFunctionCallIdent(contract, thisLocalCall) === 'test.b(bytes32,address)', 'this local call returns correct type') + t.ok(common.getFullQualifiedFunctionCallIdent(contract, externalDirect) === 'InfoFeed.info()', 'external direct call returns correct type') + t.ok(common.getFullQualifiedFunctionCallIdent(contract, localCall) === 'baz.bli(struct Ballot.Voter storage pointer)', 'local call returns correct type') + t.throws(() => common.getFullQualifiedFunctionCallIdent(contract, { name: 'MemberAccess' }), undefined, 'throws on wrong type') +}) + +test('staticAnalysisCommon.getFullQuallyfiedFuncDefinitionIdent', function (t) { + t.plan(3) + var contract = { name: 'ContractDefinition', attributes: { name: 'baz' } } + var funDef = { + 'attributes': { + 'constant': false, + 'name': 'getY', + 'payable': false, + 'visibility': 'public' + }, + 'children': [ + { + 'children': [ + { + 'attributes': { + 'name': 'z', + 'type': 'uint256' + }, + 'children': [ + { + 'attributes': { + 'name': 'uint' + }, + 'name': 'ElementaryTypeName' + } + ], + 'name': 'VariableDeclaration' + }, + { + 'attributes': { + 'name': 'r', + 'type': 'bool' + }, + 'children': [ + { + 'attributes': { + 'name': 'bool' + }, + 'name': 'ElementaryTypeName' + } + ], + 'name': 'VariableDeclaration' + } + ], + 'name': 'ParameterList' + }, + { + 'children': [ + { + 'attributes': { + 'name': '', + 'type': 'uint256' + }, + 'children': [ + { + 'attributes': { + 'name': 'uint' + }, + 'id': 34, + 'name': 'ElementaryTypeName', + 'src': '285:4:0' + } + ], + 'id': 35, + 'name': 'VariableDeclaration', + 'src': '285:4:0' + } + ], + 'name': 'ParameterList' + }, + { + 'children': [], + 'name': 'Block' + } + ], + 'name': 'FunctionDefinition' + } + t.ok(common.getFullQuallyfiedFuncDefinitionIdent(contract, funDef, ['uint256', 'bool']) === 'baz.getY(uint256,bool)', 'creates right signature') + t.throws(() => common.getFullQuallyfiedFuncDefinitionIdent(contract, { name: 'MemberAccess' }, ['uint256', 'bool']), undefined, 'throws on wrong nodes') + t.throws(() => common.getFullQuallyfiedFuncDefinitionIdent({ name: 'FunctionCall' }, funDef, ['uint256', 'bool']), undefined, 'throws on wrong nodes') +}) + +// #################### Trivial Node Identification + +test('staticAnalysisCommon.isFunctionDefinition', function (t) { + t.plan(3) + var node1 = { name: 'FunctionDefinition' } + var node2 = { name: 'MemberAccess' } + var node3 = { name: 'FunctionDefinitionBLABLA' } + + t.ok(common.isFunctionDefinition(node1), 'is exact match should work') + t.notOk(common.isFunctionDefinition(node2), 'different node should not work') + t.notOk(common.isFunctionDefinition(node3), 'substring should not work') +}) + +test('staticAnalysisCommon.isModifierDefinition', function (t) { + t.plan(3) + var node1 = { name: 'ModifierDefinition' } + var node2 = { name: 'MemberAccess' } + var node3 = { name: 'ModifierDefinitionBLABLA' } + + t.ok(common.isModifierDefinition(node1), 'is exact match should work') + t.notOk(common.isModifierDefinition(node2), 'different node should not work') + t.notOk(common.isModifierDefinition(node3), 'substring should not work') +}) + +test('staticAnalysisCommon.isModifierInvocation', function (t) { + t.plan(3) + var node1 = { name: 'ModifierInvocation' } + var node2 = { name: 'MemberAccess' } + var node3 = { name: 'ModifierInvocationBLABLA' } + + t.ok(common.isModifierInvocation(node1), 'is exact match should work') + t.notOk(common.isModifierInvocation(node2), 'different node should not work') + t.notOk(common.isModifierInvocation(node3), 'substring should not work') +}) + +test('staticAnalysisCommon.isVariableDeclaration', function (t) { + t.plan(3) + var node1 = { name: 'VariableDeclaration' } + var node2 = { name: 'MemberAccess' } + var node3 = { name: 'VariableDeclarationBLABLA' } + + t.ok(common.isVariableDeclaration(node1), 'is exact match should work') + t.notOk(common.isVariableDeclaration(node2), 'different node should not work') + t.notOk(common.isVariableDeclaration(node3), 'substring should not work') +}) + +test('staticAnalysisCommon.isInheritanceSpecifier', function (t) { + t.plan(3) + var node1 = { name: 'InheritanceSpecifier' } + var node2 = { name: 'MemberAccess' } + var node3 = { name: 'InheritanceSpecifierBLABLA' } + + t.ok(common.isInheritanceSpecifier(node1), 'is exact match should work') + t.notOk(common.isInheritanceSpecifier(node2), 'different node should not work') + t.notOk(common.isInheritanceSpecifier(node3), 'substring should not work') +}) + +test('staticAnalysisCommon.isAssignment', function (t) { + t.plan(3) + var node1 = { name: 'Assignment' } + var node2 = { name: 'MemberAccess' } + var node3 = { name: 'AssignmentBLABLA' } + + t.ok(common.isAssignment(node1), 'is exact match should work') + t.notOk(common.isAssignment(node2), 'different node should not work') + t.notOk(common.isAssignment(node3), 'substring should not work') +}) + +test('staticAnalysisCommon.isContractDefinition', function (t) { + t.plan(3) + var node1 = { name: 'ContractDefinition' } + var node2 = { name: 'MemberAccess' } + var node3 = { name: 'ContractDefinitionBLABLA' } + + t.ok(common.isContractDefinition(node1), 'is exact match should work') + t.notOk(common.isContractDefinition(node2), 'different node should not work') + t.notOk(common.isContractDefinition(node3), 'substring should not work') +}) + +test('staticAnalysisCommon.isInlineAssembly', function (t) { + t.plan(3) + var node1 = { name: 'InlineAssembly' } + var node2 = { name: 'MemberAccess' } + var node3 = { name: 'InlineAssemblyBLABLA' } + + t.ok(common.isInlineAssembly(node1), 'is exact match should work') + t.notOk(common.isInlineAssembly(node2), 'different node should not work') + t.notOk(common.isInlineAssembly(node3), 'substring should not work') +}) + +// #################### Complex Node Identification + +test('staticAnalysisCommon.isBuiltinFunctionCall', function (t) { + t.plan(2) + var selfdestruct = { + 'attributes': { + 'type': 'tuple()', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'type': 'function (address)', + 'value': 'selfdestruct' + }, + 'name': 'Identifier' + }, + { + 'attributes': { + 'type': 'address', + 'value': 'a' + }, + 'name': 'Identifier' + } + ], + 'name': 'FunctionCall' + } + var localCall = { + 'attributes': { + 'type': 'tuple()', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'type': 'function (struct Ballot.Voter storage pointer)', + 'value': 'bli' + }, + 'name': 'Identifier' + }, + { + 'attributes': { + 'type': 'struct Ballot.Voter storage pointer', + 'value': 'x' + }, + 'name': 'Identifier' + } + ], + 'name': 'FunctionCall' + } + + t.ok(common.isBuiltinFunctionCall(selfdestruct), 'selfdestruct is builtin') + t.notOk(common.isBuiltinFunctionCall(localCall), 'local call is not builtin') +}) + +test('staticAnalysisCommon.isStorageVariableDeclaration', function (t) { + t.plan(3) + var node1 = { + 'attributes': { + 'name': 'x', + 'type': 'struct Ballot.Voter storage pointer' + }, + 'children': [ + { + 'attributes': { + 'name': 'Voter' + }, + 'id': 43, + 'name': 'UserDefinedTypeName', + 'src': '604:5:0' + } + ], + 'id': 44, + 'name': 'VariableDeclaration', + 'src': '604:15:0' + } + var node2 = { + 'attributes': { + 'name': 'voters', + 'type': 'mapping(address => struct Ballot.Voter storage ref)' + }, + 'children': [ + { + 'children': [ + { + 'attributes': { + 'name': 'address' + }, + 'id': 16, + 'name': 'ElementaryTypeName', + 'src': '235:7:0' + }, + { + 'attributes': { + 'name': 'Voter' + }, + 'id': 17, + 'name': 'UserDefinedTypeName', + 'src': '246:5:0' + } + ], + 'id': 18, + 'name': 'Mapping', + 'src': '227:25:0' + } + ], + 'id': 19, + 'name': 'VariableDeclaration', + 'src': '227:32:0' + } + var node3 = { + 'attributes': { + 'name': 'voters', + 'type': 'bytes32' + }, + 'children': [ + { + 'attributes': { + 'name': 'bytes' + }, + 'id': 16, + 'name': 'ElementaryTypeName', + 'src': '235:7:0' + } + ], + 'id': 19, + 'name': 'VariableDeclaration', + 'src': '227:32:0' + } + + t.ok(common.isStorageVariableDeclaration(node1), 'struct storage pointer param is storage') + t.ok(common.isStorageVariableDeclaration(node2), 'struct storage pointer mapping param is storage') + t.notOk(common.isStorageVariableDeclaration(node3), 'bytes is not storage') +}) + +test('staticAnalysisCommon.isInteraction', function (t) { + t.plan(6) + var sendAst = { name: 'MemberAccess', children: [{attributes: { value: 'd', type: 'address' }}], attributes: { value: 'send', type: 'function (uint256) returns (bool)' } } + var callAst = { name: 'MemberAccess', children: [{attributes: { value: 'f', type: 'address' }}], attributes: { member_name: 'call', type: 'function () payable returns (bool)' } } + var callcodeAst = { name: 'MemberAccess', children: [{attributes: { value: 'f', type: 'address' }}], attributes: { member_name: 'callcode', type: 'function () payable returns (bool)' } } + var delegatecallAst = { name: 'MemberAccess', children: [{attributes: { value: 'g', type: 'address' }}], attributes: { member_name: 'delegatecall', type: 'function () returns (bool)' } } + var nodeExtDir = { + attributes: { + member_name: 'info', + type: 'function () payable external returns (uint256)' + }, + children: [ + { + attributes: { + type: 'contract InfoFeed', + value: 'f' + }, + id: 30, + name: 'Identifier', + src: '405:1:0' + } + ], + id: 32, + name: 'MemberAccess', + src: '405:6:0' + } + var nodeNot = { + 'attributes': { + 'type': 'tuple()', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'type': 'function (struct Ballot.Voter storage pointer)', + 'value': 'bli' + }, + 'id': 37, + 'name': 'Identifier', + 'src': '540:3:0' + }, + { + 'attributes': { + 'type': 'struct Ballot.Voter storage pointer', + 'value': 'x' + }, + 'id': 38, + 'name': 'Identifier', + 'src': '544:1:0' + } + ], + 'id': 39, + 'name': 'FunctionCall', + 'src': '540:6:0' + } + + t.ok(common.isInteraction(sendAst), 'send is interaction') + t.ok(common.isInteraction(callAst), 'call is interaction') + t.ok(common.isInteraction(nodeExtDir), 'ExternalDirecCall is interaction') + t.notOk(common.isInteraction(callcodeAst), 'callcode is not interaction') + t.notOk(common.isInteraction(delegatecallAst), 'callcode is not interaction') + t.notOk(common.isInteraction(nodeNot), 'local call is not interaction') +}) + +test('staticAnalysisCommon.isEffect', function (t) { + t.plan(5) + var inlineAssembly = { + 'children': [ + ], + 'id': 21, + 'name': 'InlineAssembly', + 'src': '809:41:0' + } + var assignment = { + 'attributes': { + 'operator': '=', + 'type': 'uint256' + }, + 'children': [ + { + 'attributes': { + 'type': 'uint256' + }, + 'children': [ + { + 'attributes': { + 'type': 'mapping(address => uint256)', + 'value': 'c' + }, + 'id': 61, + 'name': 'Identifier', + 'src': '873:1:0' + }, + { + 'attributes': { + 'member_name': 'sender', + 'type': 'address' + }, + 'children': [ + { + 'attributes': { + 'type': 'msg', + 'value': 'msg' + }, + 'id': 62, + 'name': 'Identifier', + 'src': '875:3:0' + } + ], + 'id': 63, + 'name': 'MemberAccess', + 'src': '875:10:0' + } + ], + 'id': 64, + 'name': 'IndexAccess', + 'src': '873:13:0' + }, + { + 'attributes': { + 'hexvalue': '30', + 'subdenomination': null, + 'token': null, + 'type': 'int_const 0', + 'value': '0' + }, + 'id': 65, + 'name': 'Literal', + 'src': '889:1:0' + } + ], + 'id': 66, + 'name': 'Assignment', + 'src': '873:17:0' + } + var unaryOp = { name: 'UnaryOperation', attributes: { operator: '++' } } + t.ok(common.isEffect(inlineAssembly), 'inline assembly is treated as effect') + t.ok(common.isEffect(assignment), 'assignment is treated as effect') + t.ok(common.isEffect(unaryOp), '++ is treated as effect') + unaryOp.attributes.operator = '--' + t.ok(common.isEffect(unaryOp), '-- is treated as effect') + t.notOk(common.isEffect({ name: 'MemberAccess', attributes: { operator: '++' } }), 'MemberAccess not treated as effect') +}) + +test('staticAnalysisCommon.isWriteOnStateVariable', function (t) { + t.plan(3) + var inlineAssembly = { + 'children': [ + ], + 'id': 21, + 'name': 'InlineAssembly', + 'src': '809:41:0' + } + var assignment = { + 'attributes': { + 'operator': '=', + 'type': 'uint256' + }, + 'children': [ + { + 'attributes': { + 'type': 'uint256' + }, + 'children': [ + { + 'attributes': { + 'type': 'mapping(address => uint256)', + 'value': 'c' + }, + 'id': 61, + 'name': 'Identifier', + 'src': '873:1:0' + }, + { + 'attributes': { + 'member_name': 'sender', + 'type': 'address' + }, + 'children': [ + { + 'attributes': { + 'type': 'msg', + 'value': 'msg' + }, + 'id': 62, + 'name': 'Identifier', + 'src': '875:3:0' + } + ], + 'id': 63, + 'name': 'MemberAccess', + 'src': '875:10:0' + } + ], + 'id': 64, + 'name': 'IndexAccess', + 'src': '873:13:0' + }, + { + 'attributes': { + 'hexvalue': '30', + 'subdenomination': null, + 'token': null, + 'type': 'int_const 0', + 'value': '0' + }, + 'id': 65, + 'name': 'Literal', + 'src': '889:1:0' + } + ], + 'id': 66, + 'name': 'Assignment', + 'src': '873:17:0' + } + var node1 = { + 'attributes': { + 'name': 'x', + 'type': 'struct Ballot.Voter storage pointer' + }, + 'children': [ + { + 'attributes': { + 'name': 'Voter' + }, + 'name': 'UserDefinedTypeName' + } + ], + 'name': 'VariableDeclaration' + } + var node2 = { + 'attributes': { + 'name': 'y', + 'type': 'uint' + }, + 'children': [ + { + 'attributes': { + 'name': 'Voter' + }, + 'name': 'UserDefinedTypeName' + } + ], + 'name': 'VariableDeclaration' + } + var node3 = { + 'attributes': { + 'name': 'xx', + 'type': 'uint' + }, + 'children': [ + { + 'attributes': { + 'name': 'Voter' + }, + 'name': 'UserDefinedTypeName' + } + ], + 'name': 'VariableDeclaration' + } + t.ok(common.isWriteOnStateVariable(inlineAssembly, [node1, node2, node3]), 'inline Assembly is write on state') + t.notOk(common.isWriteOnStateVariable(assignment, [node1, node2, node3]), 'assignment on non state is not write on state') + node3.attributes.name = 'c' + t.ok(common.isWriteOnStateVariable(assignment, [node1, node2, node3]), 'assignment on state is not write on state') +}) + +test('staticAnalysisCommon.isStateVariable', function (t) { + t.plan(3) + var node1 = { + 'attributes': { + 'name': 'x', + 'type': 'struct Ballot.Voter storage pointer' + }, + 'children': [ + { + 'attributes': { + 'name': 'Voter' + }, + 'name': 'UserDefinedTypeName' + } + ], + 'name': 'VariableDeclaration' + } + var node2 = { + 'attributes': { + 'name': 'y', + 'type': 'uint' + }, + 'children': [ + { + 'attributes': { + 'name': 'Voter' + }, + 'name': 'UserDefinedTypeName' + } + ], + 'name': 'VariableDeclaration' + } + var node3 = { + 'attributes': { + 'name': 'xx', + 'type': 'uint' + }, + 'children': [ + { + 'attributes': { + 'name': 'Voter' + }, + 'name': 'UserDefinedTypeName' + } + ], + 'name': 'VariableDeclaration' + } + + t.ok(common.isStateVariable('x', [node1, node2]), 'is contained') + t.ok(common.isStateVariable('x', [node2, node1, node1]), 'is contained twice') + t.notOk(common.isStateVariable('x', [node2, node3]), 'not contained') +}) + +test('staticAnalysisCommon.isConstantFunction', function (t) { + t.plan(3) + var node1 = { name: 'FunctionDefinition', attributes: { constant: true } } + var node2 = { name: 'FunctionDefinition', attributes: { constant: false } } + var node3 = { name: 'MemberAccess', attributes: { constant: true } } + + t.ok(common.isConstantFunction(node1), 'should be const func definition') + t.notOk(common.isConstantFunction(node2), 'should not be const func definition') + t.notOk(common.isConstantFunction(node3), 'wrong node should not be const func definition') +}) + +test('staticAnalysisCommon.isPlusPlusUnaryOperation', function (t) { + t.plan(3) + var node1 = { name: 'UnaryOperation', attributes: { operator: '++' } } + var node2 = { name: 'UnaryOperation', attributes: { operator: '--' } } + var node3 = { name: 'FunctionDefinition', attributes: { operator: '++' } } + + t.ok(common.isPlusPlusUnaryOperation(node1), 'should be unary ++') + t.notOk(common.isPlusPlusUnaryOperation(node2), 'should not be unary ++') + t.notOk(common.isPlusPlusUnaryOperation(node3), 'wrong node should not be unary ++') +}) + +test('staticAnalysisCommon.isMinusMinusUnaryOperation', function (t) { + t.plan(3) + var node1 = { name: 'UnaryOperation', attributes: { operator: '--' } } + var node2 = { name: 'UnaryOperation', attributes: { operator: '++' } } + var node3 = { name: 'FunctionDefinition', attributes: { operator: '--' } } + + t.ok(common.isMinusMinusUnaryOperation(node1), 'should be unary --') + t.notOk(common.isMinusMinusUnaryOperation(node2), 'should not be unary --') + t.notOk(common.isMinusMinusUnaryOperation(node3), 'wrong node should not be unary --') +}) + +test('staticAnalysisCommon.isFullyImplementedContract', function (t) { + t.plan(3) + var node1 = { name: 'ContractDefinition', attributes: { fullyImplemented: true } } + var node2 = { name: 'ContractDefinition', attributes: { fullyImplemented: false } } + var node3 = { name: 'FunctionDefinition', attributes: { operator: '--' } } + + t.ok(common.isFullyImplementedContract(node1), 'should be fully implemented contract') + t.notOk(common.isFullyImplementedContract(node2), 'should not be fully implemented contract') + t.notOk(common.isFullyImplementedContract(node3), 'wrong node should not be fully implemented contract') +}) + +test('staticAnalysisCommon.isCallToNonConstLocalFunction', function (t) { + t.plan(2) + var node1 = { + 'attributes': { + 'type': 'tuple()', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'type': 'function (struct Ballot.Voter storage pointer)', + 'value': 'bli' + }, + 'name': 'Identifier' + }, + { + 'attributes': { + 'type': 'struct Ballot.Voter storage pointer', + 'value': 'x' + }, + 'name': 'Identifier' + } + ], + 'name': 'FunctionCall' + } + + t.ok(common.isCallToNonConstLocalFunction(node1), 'should be call to non const Local func') + node1.children[0].attributes.type = 'function (struct Ballot.Voter storage pointer) constant payable (uint256)' + t.notok(common.isCallToNonConstLocalFunction(node1), 'should no longer be call to non const Local func') +}) + +test('staticAnalysisCommon.isExternalDirectCall', function (t) { + t.plan(5) + var node = { + attributes: { + member_name: 'info', + type: 'function () payable external returns (uint256)' + }, + children: [ + { + attributes: { + type: 'contract InfoFeed', + value: 'f' + }, + id: 30, + name: 'Identifier', + src: '405:1:0' + } + ], + id: 32, + name: 'MemberAccess', + src: '405:6:0' + } + + var node2 = { name: 'MemberAccess', children: [{attributes: { value: 'this', type: 'contract test' }}], attributes: { value: 'b', type: 'function (bytes32,address) returns (bool)' } } + t.notOk(common.isThisLocalCall(node), 'is this.local_method() used should not work') + t.notOk(common.isBlockTimestampAccess(node), 'is block.timestamp used should not work') + t.notOk(common.isNowAccess(node), 'is now used should not work') + t.ok(common.isExternalDirectCall(node), 'f.info() should be external direct call') + t.notOk(common.isExternalDirectCall(node2), 'local call is not an exernal call') +}) + +test('staticAnalysisCommon.isNowAccess', function (t) { + t.plan(3) + var node = { name: 'Identifier', attributes: { value: 'now', type: 'uint256' } } + t.notOk(common.isThisLocalCall(node), 'is this.local_method() used should not work') + t.notOk(common.isBlockTimestampAccess(node), 'is block.timestamp used should not work') + t.ok(common.isNowAccess(node), 'is now used should work') +}) + +test('staticAnalysisCommon.isBlockTimestampAccess', function (t) { + t.plan(3) + var node = { name: 'MemberAccess', children: [{attributes: { value: 'block', type: 'block' }}], attributes: { value: 'timestamp', type: 'uint256' } } + t.notOk(common.isThisLocalCall(node), 'is this.local_method() used should not work') + t.ok(common.isBlockTimestampAccess(node), 'is block.timestamp used should work') + t.notOk(common.isNowAccess(node), 'is now used should not work') +}) + +test('staticAnalysisCommon.isBlockBlockhashAccess', function (t) { + t.plan(4) + var node = { + 'attributes': { + 'member_name': 'blockhash', + 'type': 'function (uint256) returns (bytes32)' + }, + 'children': [ + { + 'attributes': { + 'type': 'block', + 'value': 'block' + }, + 'name': 'Identifier' + } + ], + 'name': 'MemberAccess' + } + + t.notOk(common.isThisLocalCall(node), 'is this.local_method() used should not work') + t.notOk(common.isBlockTimestampAccess(node), 'is block.timestamp used should not work') + t.ok(common.isBlockBlockHashAccess(node), 'blockhash should work') // todo: + t.notOk(common.isNowAccess(node), 'is now used should not work') +}) + +test('staticAnalysisCommon.isThisLocalCall', function (t) { + t.plan(3) + var node = { name: 'MemberAccess', children: [{attributes: { value: 'this', type: 'contract test' }}], attributes: { value: 'b', type: 'function (bytes32,address) returns (bool)' } } + t.ok(common.isThisLocalCall(node), 'is this.local_method() used should work') + t.notOk(common.isBlockTimestampAccess(node), 'is block.timestamp used should not work') + t.notOk(common.isNowAccess(node), 'is now used should not work') +}) + +test('staticAnalysisCommon.isSuperLocalCall', function (t) { + t.plan(4) + var node = { + 'attributes': { + 'member_name': 'duper', + 'type': 'function ()' + }, + 'children': [ + { + 'attributes': { + 'type': 'contract super a', + 'value': 'super' + }, + 'name': 'Identifier' + } + ], + 'name': 'MemberAccess' + } + t.ok(common.isSuperLocalCall(node), 'is super.local_method() used should work') + t.notOk(common.isThisLocalCall(node), 'is this.local_method() used should not work') + t.notOk(common.isBlockTimestampAccess(node), 'is block.timestamp used should not work') + t.notOk(common.isNowAccess(node), 'is now used should not work') +}) + +test('staticAnalysisCommon.isLibraryCall', function (t) { + t.plan(5) + var node = { + 'attributes': { + 'member_name': 'insert', + 'type': 'function (struct Set.Data storage pointer,uint256) returns (bool)' + }, + 'children': [ + { + 'attributes': { + 'type': 'type(library Set)', + 'value': 'Set' + }, + 'name': 'Identifier' + } + ], + 'name': 'MemberAccess' + } + t.ok(common.isLibraryCall(node), 'is lib call should not work') + t.notOk(common.isSuperLocalCall(node), 'is super.local_method() used should not work') + t.notOk(common.isThisLocalCall(node), 'is this.local_method() used should not work') + t.notOk(common.isBlockTimestampAccess(node), 'is block.timestamp used should not work') + t.notOk(common.isNowAccess(node), 'is now used should not work') +}) + +test('staticAnalysisCommon.isLocalCall', function (t) { + t.plan(5) + var node1 = { + 'attributes': { + 'type': 'tuple()', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'type': 'function (struct Ballot.Voter storage pointer)', + 'value': 'bli' + }, + 'id': 37, + 'name': 'Identifier', + 'src': '540:3:0' + }, + { + 'attributes': { + 'type': 'struct Ballot.Voter storage pointer', + 'value': 'x' + }, + 'id': 38, + 'name': 'Identifier', + 'src': '544:1:0' + } + ], + 'id': 39, + 'name': 'FunctionCall', + 'src': '540:6:0' + } + + t.ok(common.isLocalCall(node1), 'isLocalCall') + t.notOk(common.isLowLevelCall(node1), 'is not low level call') + t.notOk(common.isExternalDirectCall(node1), 'is not external direct call') + t.notOk(common.isEffect(node1), 'is not effect') + t.notOk(common.isInteraction(node1), 'is not interaction') +}) + +test('staticAnalysisCommon.isLowLevelCall', function (t) { + t.plan(6) + var sendAst = { name: 'MemberAccess', children: [{attributes: { value: 'd', type: 'address' }}], attributes: { value: 'send', type: 'function (uint256) returns (bool)' } } + var callAst = { name: 'MemberAccess', children: [{attributes: { value: 'f', type: 'address' }}], attributes: { member_name: 'call', type: 'function () payable returns (bool)' } } + var callcodeAst = { name: 'MemberAccess', children: [{attributes: { value: 'f', type: 'address' }}], attributes: { member_name: 'callcode', type: 'function () payable returns (bool)' } } + var delegatecallAst = { name: 'MemberAccess', children: [{attributes: { value: 'g', type: 'address' }}], attributes: { member_name: 'delegatecall', type: 'function () returns (bool)' } } + + t.ok(common.isLowLevelSendInst(sendAst) && common.isLowLevelCall(sendAst), 'send is llc should work') + t.ok(common.isLowLevelCallInst(callAst) && common.isLowLevelCall(callAst), 'call is llc should work') + t.notOk(common.isLowLevelCallInst(callcodeAst), 'callcode is not call') + t.ok(common.isLowLevelCallcodeInst(callcodeAst) && common.isLowLevelCall(callcodeAst), 'callcode is llc should work') + t.notOk(common.isLowLevelCallcodeInst(callAst), 'call is not callcode') + t.ok(common.isLowLevelDelegatecallInst(delegatecallAst) && common.isLowLevelCall(delegatecallAst), 'delegatecall is llc should work') +}) + +test('staticAnalysisCommon: Call of parameter function', function (t) { + t.plan(7) + var node1 = { + 'attributes': { + 'argumentTypes': null, + 'isConstant': false, + 'isLValue': false, + 'isPure': false, + 'isStructConstructorCall': false, + 'lValueRequested': false, + 'names': [ + null + ], + 'type': 'uint256', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'argumentTypes': [ + { + 'typeIdentifier': 't_uint256', + 'typeString': 'uint256' + }, + { + 'typeIdentifier': 't_uint256', + 'typeString': 'uint256' + } + ], + 'overloadedDeclarations': [ + null + ], + 'referencedDeclaration': 25, + 'type': 'function (uint256,uint256) pure returns (uint256)', + 'value': 'f' + }, + 'id': 34, + 'name': 'Identifier', + 'src': '267:1:0' + }, + { + 'attributes': { + 'argumentTypes': null, + 'overloadedDeclarations': [ + null + ], + 'referencedDeclaration': 27, + 'type': 'uint256', + 'value': 'x' + }, + 'id': 35, + 'name': 'Identifier', + 'src': '269:1:0' + }, + { + 'attributes': { + 'argumentTypes': null, + 'overloadedDeclarations': [ + null + ], + 'referencedDeclaration': 29, + 'type': 'uint256', + 'value': 'y' + }, + 'id': 36, + 'name': 'Identifier', + 'src': '272:1:0' + } + ], + 'id': 37, + 'name': 'FunctionCall', + 'src': '267:7:0' + } + + t.ok(common.isLocalCall(node1), 'is not LocalCall') + t.notOk(common.isThisLocalCall(node1), 'is not this local call') + t.notOk(common.isSuperLocalCall(node1), 'is not super local call') + t.notOk(common.isExternalDirectCall(node1), 'is not ExternalDirectCall') + t.notOk(common.isLibraryCall(node1), 'is not LibraryCall') + + t.equals(common.getFunctionCallType(node1), 'function (uint256,uint256) pure returns (uint256)', 'Extracts right type') + + t.equals(common.getFunctionCallTypeParameterType(node1), 'uint256,uint256', 'Extracts param right type') +}) + +test('staticAnalysisCommon: function call with of function with function parameter', function (t) { + t.plan(2) + var node1 = { + 'attributes': { + 'argumentTypes': null, + 'isConstant': false, + 'isLValue': false, + 'isPure': false, + 'isStructConstructorCall': false, + 'lValueRequested': false, + 'names': [ + null + ], + 'type': 'uint256', + 'type_conversion': false + }, + 'children': [ + { + 'attributes': { + 'argumentTypes': [ + { + 'typeIdentifier': 't_function_internal_pure$_t_uint256_$_t_uint256_$returns$_t_uint256_$', + 'typeString': 'function (uint256,uint256) pure returns (uint256)' + }, + { + 'typeIdentifier': 't_uint256', + 'typeString': 'uint256' + }, + { + 'typeIdentifier': 't_uint256', + 'typeString': 'uint256' + } + ], + 'overloadedDeclarations': [ + null + ], + 'referencedDeclaration': 40, + 'type': 'function (function (uint256,uint256) pure returns (uint256),uint256,uint256) pure returns (uint256)', + 'value': 'eval' + }, + 'id': 49, + 'name': 'Identifier', + 'src': '361:4:0' + }, + { + 'attributes': { + 'argumentTypes': null, + 'overloadedDeclarations': [ + null + ], + 'referencedDeclaration': 15, + 'type': 'function (uint256,uint256) pure returns (uint256)', + 'value': 'plus' + }, + 'id': 50, + 'name': 'Identifier', + 'src': '366:4:0' + }, + { + 'attributes': { + 'argumentTypes': null, + 'overloadedDeclarations': [ + null + ], + 'referencedDeclaration': 42, + 'type': 'uint256', + 'value': 'x' + }, + 'id': 51, + 'name': 'Identifier', + 'src': '372:1:0' + }, + { + 'attributes': { + 'argumentTypes': null, + 'overloadedDeclarations': [ + null + ], + 'referencedDeclaration': 44, + 'type': 'uint256', + 'value': 'y' + }, + 'id': 52, + 'name': 'Identifier', + 'src': '375:1:0' + } + ], + 'id': 53, + 'name': 'FunctionCall', + 'src': '361:16:0' + } + + t.equals(common.getFunctionCallType(node1), 'function (function (uint256,uint256) pure returns (uint256),uint256,uint256) pure returns (uint256)', 'Extracts right type') + + t.equals(common.getFunctionCallTypeParameterType(node1), 'function (uint256,uint256) pure returns (uint256),uint256,uint256', 'Extracts param right type') +}) + +test('staticAnalysisCommon: require call', function (t) { + t.plan(3) + var node = {'attributes': {'argumentTypes': null, 'isConstant': false, 'isLValue': false, 'isPure': false, 'isStructConstructorCall': false, 'lValueRequested': false, 'names': [null], 'type': 'tuple()', 'type_conversion': false}, 'children': [{'attributes': {'argumentTypes': [{'typeIdentifier': 't_bool', 'typeString': 'bool'}, {'typeIdentifier': 't_stringliteral_80efd193f332877914d93edb0b3ef5c6a7eecd00c6251c3fd7f146b60b40e6cd', 'typeString': 'literal_string \'fuu\''}], 'overloadedDeclarations': [90, 91], 'referencedDeclaration': 91, 'type': 'function (bool,string memory) pure', 'value': 'require'}, 'id': 50, 'name': 'Identifier', 'src': '462:7:0'}, {'attributes': {'argumentTypes': null, 'commonType': {'typeIdentifier': 't_address', 'typeString': 'address'}, 'isConstant': false, 'isLValue': false, 'isPure': false, 'lValueRequested': false, 'operator': '==', 'type': 'bool'}, 'children': [{'attributes': {'argumentTypes': null, 'isConstant': false, 'isLValue': false, 'isPure': false, 'lValueRequested': false, 'member_name': 'sender', 'referencedDeclaration': null, 'type': 'address'}, 'children': [{'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 87, 'type': 'msg', 'value': 'msg'}, 'id': 51, 'name': 'Identifier', 'src': '470:3:0'}], 'id': 52, 'name': 'MemberAccess', 'src': '470:10:0'}, {'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 10, 'type': 'address', 'value': 'owner'}, 'id': 53, 'name': 'Identifier', 'src': '484:5:0'}], 'id': 54, 'name': 'BinaryOperation', 'src': '470:19:0'}, {'attributes': {'argumentTypes': null, 'hexvalue': '667575', 'isConstant': false, 'isLValue': false, 'isPure': true, 'lValueRequested': false, 'subdenomination': null, 'token': 'string', 'type': 'literal_string \'fuu\'', 'value': 'fuu'}, 'id': 55, 'name': 'Literal', 'src': '491:5:0'}], 'id': 56, 'name': 'FunctionCall', 'src': '462:35:0'} + + t.equals(common.isRequireCall(node), true) + t.equals(common.getFunctionCallType(node), 'function (bool,string memory) pure', 'Extracts right type') + t.equals(common.getFunctionCallTypeParameterType(node), 'bool,string memory', 'Extracts param right type') +}) + +test('staticAnalysisCommon: isDeleteOfDynamicArray', function (t) { + t.plan(2) + var node = {'attributes': {'argumentTypes': null, 'isConstant': false, 'isLValue': false, 'isPure': false, 'lValueRequested': false, 'operator': 'delete', 'prefix': true, 'type': 'tuple()'}, 'children': [{'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 4, 'type': 'uint256[] storage ref', 'value': 'users'}, 'id': 58, 'name': 'Identifier', 'src': '514:5:0'}], 'id': 59, 'name': 'UnaryOperation', 'src': '507:12:0'} + t.equals(common.isDeleteOfDynamicArray(node), true) + t.equals(common.isDynamicArrayAccess(node.children[0]), true, 'Extracts right type') +}) + +test('staticAnalysisCommon: isAbiNamespaceCall', function (t) { + t.plan(8) + var node1 = {'attributes': {'argumentTypes': null, 'isConstant': false, 'isLValue': false, 'isPure': false, 'isStructConstructorCall': false, 'lValueRequested': false, 'names': [null], 'type': 'bytes memory', 'type_conversion': false}, 'children': [{'attributes': {'argumentTypes': [{'typeIdentifier': 't_uint256', 'typeString': 'uint256'}, {'typeIdentifier': 't_uint256', 'typeString': 'uint256'}], 'isConstant': false, 'isLValue': false, 'isPure': false, 'lValueRequested': false, 'member_name': 'encode', 'referencedDeclaration': null, 'type': 'function () pure returns (bytes memory)'}, 'children': [{'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 64, 'type': 'abi', 'value': 'abi'}, 'id': 26, 'name': 'Identifier', 'src': '245: 3:0'}], 'id': 28, 'name': 'MemberAccess', 'src': '245:10:0'}, {'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 7, 'type': 'uint256', 'value': 'a'}, 'id': 29, 'name': 'Identifier', 'src': '256:1:0'}, {'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 15, 'type': 'uint256', 'value': 'b'}, 'id': 30, 'name': 'Identifier', 'src': '258:1:0'}], 'id': 31, 'name': 'FunctionCall', 'src': '245:15:0'} + var node2 = {'attributes': {'argumentTypes': null, 'isConstant': false, 'isLValue': false, 'isPure': false, 'isStructConstructorCall': false, 'lValueRequested': false, 'names': [null], 'type': 'bytes memory', 'type_conversion': false}, 'children': [{'attributes': {'argumentTypes': [{'typeIdentifier': 't_uint256', 'typeString': 'uint256'}, {'typeIdentifier': 't_uint256', 'typeString': 'uint256'}], 'isConstant': false, 'isLValue': false, 'isPure': false, 'lValueRequested': false, 'member_name': 'encodePacked', 'referencedDeclaration': null, 'type': 'function () pure returns (bytes memory)'}, 'children': [{'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 64, 'type': 'abi', 'value': 'abi'}, 'id': 33, 'name': 'Identifier', 'src': '279:3:0'}], 'id': 35, 'name': 'MemberAccess', 'src': '279:16:0'}, {'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 7, 'type': 'uint256', 'value': 'a'}, 'id': 36, 'name': 'Identifier', 'src': '296:1:0'}, {'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 15, 'type': 'uint256', 'value': 'b'}, 'id': 37, 'name': 'Identifier', 'src': '298:1:0'}], 'id': 38, 'name': 'FunctionCall', 'src': '279:21:0'} + var node3 = {'attributes': {'argumentTypes': null, 'isConstant': false, 'isLValue': false, 'isPure': false, 'isStructConstructorCall': false, 'lValueRequested': false, 'names': [null], 'type': 'bytes memory', 'type_conversion': false}, 'children': [{'attributes': {'argumentTypes': [{'typeIdentifier': 't_bytes4', 'typeString': 'bytes4'}, {'typeIdentifier': 't_uint256', 'typeString': 'uint256'}, {'typeIdentifier': 't_uint256', 'typeString': 'uint256'}], 'isConstant': false, 'isLValue': false, 'isPure': false, 'lValueRequested': false, 'member_name': 'encodeWithSelector', 'referencedDeclaration': null, 'type': 'function (bytes4) pure returns (bytes memory)'}, 'children': [{'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 64, 'type': 'abi', 'value': 'abi'}, 'id': 40, 'name': 'Identifier', 'src': '319:3:0'}], 'id': 42, 'name': 'MemberAccess', 'src': '319:22:0'}, {'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 19, 'type': 'bytes4', 'value': 'selector'}, 'id': 43, 'name': 'Identifier', 'src': '342:8:0'}, {'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 7, 'type': 'uint256', 'value': 'a'}, 'id': 44, 'name': 'Identifier', 'src': '352:1:0'}, {'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 15, 'type': 'uint256', 'value': 'b'}, 'id': 45, 'name': 'Identifier', 'src': '355:1:0'}], 'id': 46, 'name': 'FunctionCall', 'src': '319:38:0'} + var node4 = {'attributes': {'argumentTypes': null, 'isConstant': false, 'isLValue': false, 'isPure': false, 'isStructConstructorCall': false, 'lValueRequested': false, 'names': [null], 'type': 'bytes memory', 'type_conversion': false}, 'children': [{'attributes': {'argumentTypes': [{'typeIdentifier': 't_string_memory_ptr', 'typeString': 'string memory'}, {'typeIdentifier': 't_uint256', 'typeString': 'uint256'}, {'typeIdentifier': 't_uint256', 'typeString': 'uint256'}], 'isConstant': false, 'isLValue': false, 'isPure': false, 'lValueRequested': false, 'member_name': 'encodeWithSignature', 'referencedDeclaration': null, 'type': 'function (string memory) pure returns (bytes memory)'}, 'children': [{'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 64, 'type': 'abi', 'value': 'abi'}, 'id': 48, 'name': 'Identifier', 'src': '367:3:0'}], 'id': 50, 'name': 'MemberAccess', 'src': '367:23:0'}, {'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 11, 'type': 'string memory', 'value': 'sig'}, 'id': 51, 'name': 'Identifier', 'src': '391:3:0'}, {'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 7, 'type': 'uint256', 'value': 'a'}, 'id': 52, 'name': 'Identifier', 'src': '396:1:0'}, {'attributes': {'argumentTypes': null, 'overloadedDeclarations': [null], 'referencedDeclaration': 15, 'type': 'uint256', 'value': 'b'}, 'id': 53, 'name': 'Identifier', 'src': '399:1:0'}], 'id': 54, 'name': 'FunctionCall', 'src': '367:34:0'} + + t.equals(common.isAbiNamespaceCall(node1), true, 'encode abi') + t.equals(common.isAbiNamespaceCall(node2), true, 'encodePacked abi') + t.equals(common.isAbiNamespaceCall(node3), true, 'encodeWithSelector abi') + t.equals(common.isAbiNamespaceCall(node4), true, 'encodeWithSignature abi') + + t.equals(common.isBuiltinFunctionCall(node1), true, 'encode Builtin') + t.equals(common.isBuiltinFunctionCall(node2), true, 'encodePacked Builtin') + t.equals(common.isBuiltinFunctionCall(node3), true, 'encodeWithSelector Builtin') + t.equals(common.isBuiltinFunctionCall(node4), true, 'encodeWithSignature Builtin') +}) diff --git a/remix-analyzer/test/analysis/staticAnalysisIntegration-test.js b/remix-analyzer/test/analysis/staticAnalysisIntegration-test.js new file mode 100644 index 0000000000..3b1f31308a --- /dev/null +++ b/remix-analyzer/test/analysis/staticAnalysisIntegration-test.js @@ -0,0 +1,634 @@ +var test = require('tape') +var remixLib = require('remix-lib') + +var StatRunner = require('../../src/analysis/staticAnalysisRunner') +var compilerInput = remixLib.helpers.compiler.compilerInput + +var compiler = require('solc') + +var fs = require('fs') +var path = require('path') + +var testFiles = [ + 'KingOfTheEtherThrone.sol', + 'assembly.sol', + 'ballot.sol', + 'ballot_reentrant.sol', + 'ballot_withoutWarnings.sol', + 'cross_contract.sol', + 'inheritance.sol', + 'modifier1.sol', + 'modifier2.sol', + 'notReentrant.sol', + 'structReentrant.sol', + 'thisLocal.sol', + 'globals.sol', + 'library.sol', + 'transfer.sol', + 'ctor.sol', + 'forgottenReturn.sol', + 'selfdestruct.sol', + 'deleteDynamicArray.sol', + 'blockLevelCompare.sol', + 'intDivisionTruncate.sol' +] + +var testFileAsts = {} + +testFiles.forEach((fileName) => { + var content = fs.readFileSync(path.join(__dirname, 'test-contracts', fileName), 'utf8') + testFileAsts[fileName] = JSON.parse(compiler.compileStandardWrapper(compilerInput(content))) +}) + +test('Integration test thisLocal.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/thisLocal') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 0, + 'assembly.sol': 0, + 'ballot.sol': 0, + 'ballot_reentrant.sol': 1, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 0, + 'inheritance.sol': 0, + 'modifier1.sol': 0, + 'modifier2.sol': 0, + 'notReentrant.sol': 0, + 'structReentrant.sol': 0, + 'thisLocal.sol': 1, + 'globals.sol': 0, + 'library.sol': 0, + 'transfer.sol': 0, + 'ctor.sol': 0, + 'forgottenReturn.sol': 0, + 'selfdestruct.sol': 0, + 'deleteDynamicArray.sol': 0, + 'blockLevelCompare.sol': 0, + 'intDivisionTruncate.sol': 0 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of this local warnings`) + }) +}) + +test('Integration test checksEffectsInteraction.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/checksEffectsInteraction') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 1, + 'assembly.sol': 1, + 'ballot.sol': 0, + 'ballot_reentrant.sol': 1, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 0, + 'inheritance.sol': 1, + 'modifier1.sol': 0, + 'modifier2.sol': 0, + 'notReentrant.sol': 0, + 'structReentrant.sol': 1, + 'thisLocal.sol': 0, + 'globals.sol': 1, + 'library.sol': 1, + 'transfer.sol': 1, + 'ctor.sol': 0, + 'forgottenReturn.sol': 0, + 'selfdestruct.sol': 0, + 'deleteDynamicArray.sol': 0, + 'blockLevelCompare.sol': 0, + 'intDivisionTruncate.sol': 0 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of checks-effects-interaction warnings`) + }) +}) + +test('Integration test constantFunctions.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/constantFunctions') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 0, + 'assembly.sol': 0, + 'ballot.sol': 0, + 'ballot_reentrant.sol': 0, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 1, + 'inheritance.sol': 0, + 'modifier1.sol': 1, + 'modifier2.sol': 0, + 'notReentrant.sol': 0, + 'structReentrant.sol': 1, + 'thisLocal.sol': 1, + 'globals.sol': 0, + 'library.sol': 3, + 'transfer.sol': 0, + 'ctor.sol': 0, + 'forgottenReturn.sol': 0, + 'selfdestruct.sol': 1, + 'deleteDynamicArray.sol': 0, + 'blockLevelCompare.sol': 0, + 'intDivisionTruncate.sol': 0 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of constant warnings`) + }) +}) + +test('Integration test inlineAssembly.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/inlineAssembly') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 0, + 'assembly.sol': 2, + 'ballot.sol': 0, + 'ballot_reentrant.sol': 0, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 0, + 'inheritance.sol': 0, + 'modifier1.sol': 0, + 'modifier2.sol': 0, + 'notReentrant.sol': 0, + 'structReentrant.sol': 0, + 'thisLocal.sol': 0, + 'globals.sol': 0, + 'library.sol': 0, + 'transfer.sol': 0, + 'ctor.sol': 0, + 'forgottenReturn.sol': 0, + 'selfdestruct.sol': 0, + 'deleteDynamicArray.sol': 0, + 'blockLevelCompare.sol': 0, + 'intDivisionTruncate.sol': 0 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of inline assembly warnings`) + }) +}) + +test('Integration test txOrigin.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/txOrigin') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 0, + 'assembly.sol': 1, + 'ballot.sol': 0, + 'ballot_reentrant.sol': 0, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 0, + 'inheritance.sol': 0, + 'modifier1.sol': 0, + 'modifier2.sol': 0, + 'notReentrant.sol': 0, + 'structReentrant.sol': 0, + 'thisLocal.sol': 0, + 'globals.sol': 1, + 'library.sol': 0, + 'transfer.sol': 0, + 'ctor.sol': 0, + 'forgottenReturn.sol': 0, + 'selfdestruct.sol': 0, + 'deleteDynamicArray.sol': 0, + 'blockLevelCompare.sol': 0, + 'intDivisionTruncate.sol': 0 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of tx.origin warnings`) + }) +}) + +test('Integration test gasCosts.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/gasCosts') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 2, + 'assembly.sol': 2, + 'ballot.sol': 3, + 'ballot_reentrant.sol': 2, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 1, + 'inheritance.sol': 1, + 'modifier1.sol': 0, + 'modifier2.sol': 1, + 'notReentrant.sol': 1, + 'structReentrant.sol': 1, + 'thisLocal.sol': 1, + 'globals.sol': 1, + 'library.sol': 1, + 'transfer.sol': 1, + 'ctor.sol': 0, + 'forgottenReturn.sol': 3, + 'selfdestruct.sol': 0, + 'deleteDynamicArray.sol': 2, + 'blockLevelCompare.sol': 1, + 'intDivisionTruncate.sol': 1 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of gasCost warnings`) + }) +}) + +test('Integration test similarVariableNames.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/similarVariableNames') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 0, + 'assembly.sol': 0, + 'ballot.sol': 2, + 'ballot_reentrant.sol': 3, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 0, + 'inheritance.sol': 0, + 'modifier1.sol': 0, + 'modifier2.sol': 0, + 'notReentrant.sol': 1, + 'structReentrant.sol': 0, + 'thisLocal.sol': 0, + 'globals.sol': 0, + 'library.sol': 0, + 'transfer.sol': 0, + 'ctor.sol': 1, + 'forgottenReturn.sol': 0, + 'selfdestruct.sol': 0, + 'deleteDynamicArray.sol': 1, + 'blockLevelCompare.sol': 0, + 'intDivisionTruncate.sol': 0 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of similarVariableNames warnings`) + }) +}) + +test('Integration test inlineAssembly.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/inlineAssembly') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 0, + 'assembly.sol': 2, + 'ballot.sol': 0, + 'ballot_reentrant.sol': 0, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 0, + 'inheritance.sol': 0, + 'modifier1.sol': 0, + 'modifier2.sol': 0, + 'notReentrant.sol': 0, + 'structReentrant.sol': 0, + 'thisLocal.sol': 0, + 'globals.sol': 0, + 'library.sol': 0, + 'transfer.sol': 0, + 'ctor.sol': 0, + 'forgottenReturn.sol': 0, + 'selfdestruct.sol': 0, + 'deleteDynamicArray.sol': 0, + 'blockLevelCompare.sol': 0, + 'intDivisionTruncate.sol': 0 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of inlineAssembly warnings`) + }) +}) + +test('Integration test blockTimestamp.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/blockTimestamp') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 1, + 'assembly.sol': 0, + 'ballot.sol': 0, + 'ballot_reentrant.sol': 3, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 0, + 'inheritance.sol': 0, + 'modifier1.sol': 0, + 'modifier2.sol': 0, + 'notReentrant.sol': 0, + 'structReentrant.sol': 0, + 'thisLocal.sol': 0, + 'globals.sol': 2, + 'library.sol': 0, + 'transfer.sol': 0, + 'ctor.sol': 0, + 'forgottenReturn.sol': 0, + 'selfdestruct.sol': 0, + 'deleteDynamicArray.sol': 0, + 'blockLevelCompare.sol': 0, + 'intDivisionTruncate.sol': 0 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of blockTimestamp warnings`) + }) +}) + +test('Integration test lowLevelCalls.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/lowLevelCalls') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 1, + 'assembly.sol': 1, + 'ballot.sol': 0, + 'ballot_reentrant.sol': 7, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 1, + 'inheritance.sol': 1, + 'modifier1.sol': 0, + 'modifier2.sol': 0, + 'notReentrant.sol': 1, + 'structReentrant.sol': 1, + 'thisLocal.sol': 2, + 'globals.sol': 1, + 'library.sol': 1, + 'transfer.sol': 0, + 'ctor.sol': 0, + 'forgottenReturn.sol': 0, + 'selfdestruct.sol': 0, + 'deleteDynamicArray.sol': 0, + 'blockLevelCompare.sol': 0, + 'intDivisionTruncate.sol': 0 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of lowLevelCalls warnings`) + }) +}) + +test('Integration test blockBlockhash.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/blockBlockhash') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 0, + 'assembly.sol': 0, + 'ballot.sol': 0, + 'ballot_reentrant.sol': 0, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 0, + 'inheritance.sol': 0, + 'modifier1.sol': 0, + 'modifier2.sol': 0, + 'notReentrant.sol': 0, + 'structReentrant.sol': 0, + 'thisLocal.sol': 0, + 'globals.sol': 0, // was 1 !! @TODO + 'library.sol': 0, + 'transfer.sol': 0, + 'ctor.sol': 0, + 'forgottenReturn.sol': 0, + 'selfdestruct.sol': 0, + 'deleteDynamicArray.sol': 0, + 'blockLevelCompare.sol': 0, + 'intDivisionTruncate.sol': 0 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of blockBlockhash warnings`) + }) +}) + +test('Integration test noReturn.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/noReturn') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 0, + 'assembly.sol': 1, + 'ballot.sol': 0, + 'ballot_reentrant.sol': 0, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 0, + 'inheritance.sol': 0, + 'modifier1.sol': 1, + 'modifier2.sol': 0, + 'notReentrant.sol': 0, + 'structReentrant.sol': 0, + 'thisLocal.sol': 1, + 'globals.sol': 0, + 'library.sol': 0, + 'transfer.sol': 0, + 'ctor.sol': 0, + 'forgottenReturn.sol': 1, + 'selfdestruct.sol': 0, + 'deleteDynamicArray.sol': 0, + 'blockLevelCompare.sol': 0, + 'intDivisionTruncate.sol': 0 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of noReturn warnings`) + }) +}) + +test('Integration test selfdestruct.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/selfdestruct') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 0, + 'assembly.sol': 0, + 'ballot.sol': 0, + 'ballot_reentrant.sol': 0, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 0, + 'inheritance.sol': 0, + 'modifier1.sol': 0, + 'modifier2.sol': 0, + 'notReentrant.sol': 0, + 'structReentrant.sol': 0, + 'thisLocal.sol': 0, + 'globals.sol': 1, + 'library.sol': 0, + 'transfer.sol': 0, + 'ctor.sol': 0, + 'forgottenReturn.sol': 0, + 'selfdestruct.sol': 2, + 'deleteDynamicArray.sol': 0, + 'blockLevelCompare.sol': 0, + 'intDivisionTruncate.sol': 1 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of selfdestruct warnings`) + }) +}) + +test('Integration test guardConditions.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/guardConditions') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 0, + 'assembly.sol': 1, + 'ballot.sol': 0, + 'ballot_reentrant.sol': 0, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 0, + 'inheritance.sol': 0, + 'modifier1.sol': 0, + 'modifier2.sol': 0, + 'notReentrant.sol': 0, + 'structReentrant.sol': 0, + 'thisLocal.sol': 0, + 'globals.sol': 1, + 'library.sol': 0, + 'transfer.sol': 0, + 'ctor.sol': 0, + 'forgottenReturn.sol': 0, + 'selfdestruct.sol': 0, + 'deleteDynamicArray.sol': 1, + 'blockLevelCompare.sol': 0, + 'intDivisionTruncate.sol': 1 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of guardCondition warnings`) + }) +}) + +test('Integration test deleteDynamicArrays.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/deleteDynamicArrays') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 0, + 'assembly.sol': 0, + 'ballot.sol': 0, + 'ballot_reentrant.sol': 0, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 0, + 'inheritance.sol': 0, + 'modifier1.sol': 0, + 'modifier2.sol': 0, + 'notReentrant.sol': 0, + 'structReentrant.sol': 0, + 'thisLocal.sol': 0, + 'globals.sol': 0, + 'library.sol': 0, + 'transfer.sol': 0, + 'ctor.sol': 0, + 'forgottenReturn.sol': 0, + 'selfdestruct.sol': 0, + 'deleteDynamicArray.sol': 2, + 'blockLevelCompare.sol': 0, + 'intDivisionTruncate.sol': 0 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of deleteDynamicArrays warnings`) + }) +}) + +test('Integration test assignAndCompare.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/assignAndCompare') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 0, + 'assembly.sol': 0, + 'ballot.sol': 0, + 'ballot_reentrant.sol': 0, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 0, + 'inheritance.sol': 0, + 'modifier1.sol': 0, + 'modifier2.sol': 0, + 'notReentrant.sol': 0, + 'structReentrant.sol': 0, + 'thisLocal.sol': 0, + 'globals.sol': 0, + 'library.sol': 0, + 'transfer.sol': 0, + 'ctor.sol': 0, + 'forgottenReturn.sol': 0, + 'selfdestruct.sol': 0, + 'deleteDynamicArray.sol': 0, + 'blockLevelCompare.sol': 8, + 'intDivisionTruncate.sol': 0 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of assignAndCompare warnings`) + }) +}) + +test('Integration test intDivisionTruncate.js', function (t) { + t.plan(testFiles.length) + + var module = require('../../src/analysis/modules/intDivisionTruncate') + + var lengthCheck = { + 'KingOfTheEtherThrone.sol': 0, + 'assembly.sol': 0, + 'ballot.sol': 0, + 'ballot_reentrant.sol': 0, + 'ballot_withoutWarnings.sol': 0, + 'cross_contract.sol': 0, + 'inheritance.sol': 0, + 'modifier1.sol': 0, + 'modifier2.sol': 0, + 'notReentrant.sol': 0, + 'structReentrant.sol': 0, + 'thisLocal.sol': 0, + 'globals.sol': 0, + 'library.sol': 0, + 'transfer.sol': 0, + 'ctor.sol': 0, + 'forgottenReturn.sol': 0, + 'selfdestruct.sol': 0, + 'deleteDynamicArray.sol': 0, + 'blockLevelCompare.sol': 0, + 'intDivisionTruncate.sol': 2 + } + + runModuleOnFiles(module, t, (file, report) => { + t.equal(report.length, lengthCheck[file], `${file} has right amount of intDivisionTruncate warnings`) + }) +}) + +// #################### Helpers +function runModuleOnFiles (module, t, cb) { + var statRunner = new StatRunner() + + testFiles.forEach((fileName) => { + statRunner.runWithModuleList(testFileAsts[fileName], [{ name: module.name, mod: new module.Module() }], (reports) => { + let report = reports[0].report + if (report.some((x) => x['warning'].includes('INTERNAL ERROR'))) { + t.comment('Error while executing Module: ' + JSON.stringify(report)) + } + cb(fileName, report) + }) + }) +} diff --git a/remix-analyzer/test/analysis/staticAnalysisIssues-test.js b/remix-analyzer/test/analysis/staticAnalysisIssues-test.js new file mode 100644 index 0000000000..eb7b97a2ef --- /dev/null +++ b/remix-analyzer/test/analysis/staticAnalysisIssues-test.js @@ -0,0 +1,34 @@ +var test = require('tape') +var remixLib = require('remix-lib') + +var StatRunner = require('../../src/analysis/staticAnalysisRunner') +var compilerInput = remixLib.helpers.compiler.compilerInput + +var compiler = require('solc') + +var fs = require('fs') +var path = require('path') + +function compile (fileName) { + var content = fs.readFileSync(path.join(__dirname, 'test-contracts', fileName), 'utf8') + return JSON.parse(compiler.compileStandardWrapper(compilerInput(content))) +} + +test('staticAnalysisIssues.functionParameterPassingError', function (t) { + // https://github.com/ethereum/remix-ide/issues/889#issuecomment-351746474 + t.plan(2) + var res = compile('functionParameters.sol') + + var module = require('../../src/analysis/modules/checksEffectsInteraction') + + var statRunner = new StatRunner() + + t.doesNotThrow(() => { + statRunner.runWithModuleList(res, [{ name: module.name, mod: new module.Module() }], (reports) => { + }) + }, true, 'Analysis should not throw') + + statRunner.runWithModuleList(res, [{ name: module.name, mod: new module.Module() }], (reports) => { + t.ok(!reports.some((mod) => mod.report.some((rep) => rep.warning.includes('INTERNAL ERROR')), 'Should not have internal errors')) + }) +}) diff --git a/remix-analyzer/test/analysis/test-contracts/KingOfTheEtherThrone.sol b/remix-analyzer/test/analysis/test-contracts/KingOfTheEtherThrone.sol new file mode 100644 index 0000000000..70cfb08327 --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/KingOfTheEtherThrone.sol @@ -0,0 +1,23 @@ +// return value send +contract KingOfTheEtherThrone{ + struct Monarch { + // address of the king . + address ethAddr ; + string name ; + // how much he pays to previous king + uint claimPrice ; + uint coronationTimestamp; + } + Monarch public currentMonarch ; + + function claimThrone ( string name ) { + address wizardAddress; + uint compensation = 100; + uint valuePaid = 10; + + if ( currentMonarch.ethAddr != wizardAddress ) + if (currentMonarch.ethAddr.send( compensation )) throw; + + currentMonarch = Monarch(msg.sender,name,valuePaid,block.timestamp); + } +} diff --git a/remix-analyzer/test/analysis/test-contracts/assembly.sol b/remix-analyzer/test/analysis/test-contracts/assembly.sol new file mode 100644 index 0000000000..fb647502eb --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/assembly.sol @@ -0,0 +1,30 @@ + pragma solidity ^0.4.9; + contract test { + + address owner; + + function at(address _addr) returns (bytes o_code) { + assert(_addr != 0x0); + assembly { + // retrieve the size of the code, this needs assembly + let size := extcodesize(_addr) + // allocate output byte array - this could also be done without assembly + // by using o_code = new bytes(size) + o_code := mload(0x40) + // new "memory end" including padding + mstore(0x40, add(o_code, and(add(add(size, 0x20), 0x1f), not(0x1f)))) + // store length in memory + mstore(o_code, size) + // actually retrieve the code, this needs assembly + extcodecopy(_addr, add(o_code, 0x20), 0, size) + } + } + + function bla() { + require(tx.origin == owner); + msg.sender.send(19); + assembly { + + } + } + } diff --git a/remix-analyzer/test/analysis/test-contracts/ballot.sol b/remix-analyzer/test/analysis/test-contracts/ballot.sol new file mode 100644 index 0000000000..e1e9f676fe --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/ballot.sol @@ -0,0 +1,145 @@ +pragma solidity ^0.4.0; + +/// @title Voting with delegation. +contract Ballot { + // This declares a new complex type which will + // be used for variables later. + // It will represent a single voter. + struct Voter { + uint weight; // weight is accumulated by delegation + bool voted; // if true, that person already voted + address delegate; // person delegated to + uint vote; // index of the voted proposal + } + + // This is a type for a single proposal. + struct Proposal + { + bytes32 name; // short name (up to 32 bytes) + uint voteCount; // number of accumulated votes + } + + address public chairperson; + + // This declares a state variable that + // stores a `Voter` struct for each possible address. + mapping(address => Voter) public voters; + + // A dynamically-sized array of `Proposal` structs. + Proposal[] public proposals; + + /// Create a new ballot to choose one of `proposalNames`. + function Ballot(bytes32[] proposalNames) { + chairperson = msg.sender; + voters[chairperson].weight = 1; + + // For each of the provided proposal names, + // create a new proposal object and add it + // to the end of the array. + for (uint i = 0; i < proposalNames.length; i++) { + // `Proposal({...})` creates a temporary + // Proposal object and `proposals.push(...)` + // appends it to the end of `proposals`. + proposals.push(Proposal({ + name: proposalNames[i], + voteCount: 0 + })); + } + } + + // Give `voter` the right to vote on this ballot. + // May only be called by `chairperson`. + function giveRightToVote(address voter) { + if (msg.sender != chairperson || voters[voter].voted) { + // `throw` terminates and reverts all changes to + // the state and to Ether balances. It is often + // a good idea to use this if functions are + // called incorrectly. But watch out, this + // will also consume all provided gas. + throw; + } + voters[voter].weight = 1; + } + + /// Delegate your vote to the voter `to`. + function delegate(address to) { + // assigns reference + Voter sender = voters[msg.sender]; + if (sender.voted) + throw; + + // Forward the delegation as long as + // `to` also delegated. + // In general, such loops are very dangerous, + // because if they run too long, they might + // need more gas than is available in a block. + // In this case, the delegation will not be executed, + // but in other situations, such loops might + // cause a contract to get "stuck" completely. + while ( + voters[to].delegate != address(0) && + voters[to].delegate != msg.sender + ) { + to = voters[to].delegate; + } + + // We found a loop in the delegation, not allowed. + if (to == msg.sender) { + throw; + } + + // Since `sender` is a reference, this + // modifies `voters[msg.sender].voted` + sender.voted = true; + sender.delegate = to; + Voter delegate = voters[to]; + if (delegate.voted) { + // If the delegate already voted, + // directly add to the number of votes + proposals[delegate.vote].voteCount += sender.weight; + } else { + // If the delegate did not vote yet, + // add to her weight. + delegate.weight += sender.weight; + } + } + + /// Give your vote (including votes delegated to you) + /// to proposal `proposals[proposal].name`. + function vote(uint proposal) { + Voter sender = voters[msg.sender]; + if (sender.voted) + throw; + sender.voted = true; + sender.vote = proposal; + + // If `proposal` is out of the range of the array, + // this will throw automatically and revert all + // changes. + proposals[proposal].voteCount += sender.weight; + } + + /// @dev Computes the winning proposal taking all + /// previous votes into account. + function winningProposal() constant + returns (uint winningProposal) + { + uint winningVoteCount = 0; + for (uint p = 0; p < proposals.length; p++) { + if (proposals[p].voteCount > winningVoteCount) { + winningVoteCount = proposals[p].voteCount; + winningProposal = p; + } + } + } + + // Calls winningProposal() function to get the index + // of the winner contained in the proposals array and then + // returns the name of the winner + function winnerName() constant + returns (bytes32 winnerName) + { + winnerName = proposals[winningProposal()].name; + } +} + diff --git a/remix-analyzer/test/analysis/test-contracts/ballot_reentrant.sol b/remix-analyzer/test/analysis/test-contracts/ballot_reentrant.sol new file mode 100644 index 0000000000..a695b09e19 --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/ballot_reentrant.sol @@ -0,0 +1,101 @@ +pragma solidity ^0.4.0; + +contract InfoFeed { + function info() payable returns (uint ret); + function call1(uint a) payable returns (bool); +} + + +contract Ballot { + + InfoFeed feed; + + struct Voter { + uint weight; + bool voted; + uint8 vote; + address delegate; + } + struct Proposal { + uint voteCount; + } + + address chairperson; + mapping(address => Voter) voters; + Proposal[] proposals; + + function send1(address a) { + giveRightToVote(a,a); + } + + /// Create a new ballot with $(_numProposals) different proposals. + function Ballot(uint8 _numProposals) { + address d; + if(!d.delegatecall.gas(800)('function_name', 'arg1', 'arg2')) throw; + if(!d.callcode.gas(800)('function_name', 'arg1', 'arg2')) throw; + if(!d.call.value(10).gas(800)('function_name', 'arg1', 'arg2')) throw; + if(!d.call.value(10).gas(800)('function_name', 'arg1', 'arg2')) throw; + + + + if(!msg.sender.send(1 wei)) throw; + if(!d.call('function_name', 'arg1', 'arg2')) throw; + + + uint a = now; + uint c = block.timestamp; + if(block.timestamp < 100){} + chairperson = msg.sender; + voters[chairperson].weight = 1; + proposals.length = _numProposals; + if(!d.send(1 wei)) throw; + feed.info.value(10).gas(800)(); + + feed.call1(1); + + this.send1(d); + } + + + /// Give $(voter) the right to vote on this ballot. + /// May only be called by $(chairperson). + function giveRightToVote(address voter, address b) payable returns (bool){ + if (msg.sender != chairperson || voters[voter].voted) return; + voters[voter].weight = 1; + return true; + } + + /// Delegate your vote to the voter $(to). + function delegate(address to) { + Voter sender = voters[msg.sender]; // assigns reference + if (sender.voted) return; + while (voters[to].delegate != address(0) && voters[to].delegate != msg.sender) + to = voters[to].delegate; + if (to == msg.sender) return; + sender.voted = true; + sender.delegate = to; + Voter delegate = voters[to]; + if (delegate.voted) + proposals[delegate.vote].voteCount += sender.weight; + else + delegate.weight += sender.weight; + } + + /// Give a single vote to proposal $(proposal). + function vote(uint8 proposal) { + Voter sender = voters[msg.sender]; + if (sender.voted || proposal >= proposals.length) return; + sender.voted = true; + sender.vote = proposal; + proposals[proposal].voteCount += sender.weight; + } + + function winningProposal() constant returns (uint8 winningProposal) { + uint256 winningVoteCount = 0; + for (uint8 proposal = 0; proposal < proposals.length; proposal++) + if (proposals[proposal].voteCount > winningVoteCount) { + winningVoteCount = proposals[proposal].voteCount; + winningProposal = proposal; + } + } +} diff --git a/remix-analyzer/test/analysis/test-contracts/ballot_withoutWarnings.sol b/remix-analyzer/test/analysis/test-contracts/ballot_withoutWarnings.sol new file mode 100644 index 0000000000..e273b3da4f --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/ballot_withoutWarnings.sol @@ -0,0 +1,31 @@ +pragma solidity ^0.4.0; + +/// @title Voting with delegation. +contract Ballot { + + struct Proposal + { + bytes32 name; // short name (up to 32 bytes) + uint voteCount; // number of accumulated votes + } + + // A dynamically-sized array of `Proposal` structs. + Proposal[] public proposals; + + /// @dev Computes the winning proposal taking all + /// previous votes into account. + function winningProposal() constant + returns (uint winningProposal) + { + winningProposal = 0; + } + + // Calls winningProposal() function to get the index + // of the winner contained in the proposals array and then + // returns the name of the winner + function winnerName() constant + returns (bytes32 winnerName) + { + winnerName = proposals[winningProposal()].name; + } +} diff --git a/remix-analyzer/test/analysis/test-contracts/blockLevelCompare.sol b/remix-analyzer/test/analysis/test-contracts/blockLevelCompare.sol new file mode 100644 index 0000000000..14c4eb9d7a --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/blockLevelCompare.sol @@ -0,0 +1,45 @@ +pragma solidity ^0.4.22; +contract grr { + bool breaker; + + function() public { + uint a = 1; + string memory sig = "withdraw()"; + uint b = 3; + + bytes4 selector = bytes4(keccak256(sig)); + + abi.encode(a,b); + + abi.encodePacked(a,b); + + a = -b; + + a == b; + + if(a == b) { + abi.encodeWithSelector(selector, a, b); + abi.encodeWithSignature(sig, a, b); + } + + if(b < 4) { a == b; } + + if(b > 4) b == a; + + while(true) a == b; + + for(int i = 0; i < 3; i++) b == a; + + while(false) { + int c = 3; + uint(c) + a; + + c == 5; + + } + + a + b; + breaker = false; + } + +} \ No newline at end of file diff --git a/remix-analyzer/test/analysis/test-contracts/cross_contract.sol b/remix-analyzer/test/analysis/test-contracts/cross_contract.sol new file mode 100644 index 0000000000..426c8d444d --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/cross_contract.sol @@ -0,0 +1,19 @@ +pragma solidity ^0.4.0; + +contract a { + + uint x; + + function foo() { + x++; + } +} + +contract b { + a x; + function bar() constant { + address a; + a.send(100 wei); + x.foo(); + } +} diff --git a/remix-analyzer/test/analysis/test-contracts/ctor.sol b/remix-analyzer/test/analysis/test-contracts/ctor.sol new file mode 100644 index 0000000000..b250c02626 --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/ctor.sol @@ -0,0 +1,8 @@ +contract c { + uint x; + uint x_abc; + function c(uint _x, uint _abc) { + x=_x; + x_abc=_abc; + } +} \ No newline at end of file diff --git a/remix-analyzer/test/analysis/test-contracts/deleteDynamicArray.sol b/remix-analyzer/test/analysis/test-contracts/deleteDynamicArray.sol new file mode 100644 index 0000000000..bb179b21f6 --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/deleteDynamicArray.sol @@ -0,0 +1,37 @@ +pragma solidity ^0.4.22; +contract arr { + uint[] users; + + bytes access_rights_per_user; + + uint user_index; + + address owner; + + string grr = "message"; + + uint[100] last_100_users; + + constructor(address owner1) public { + owner = owner1; + user_index = 0; + } + + function addUser(uint id, byte rights) public{ + users[user_index] = id; + last_100_users[user_index % 100] = id; + access_rights_per_user[user_index] = rights; + user_index++; + } + + function resetState() public{ + require(msg.sender == owner, grr); + delete users; + delete access_rights_per_user; + delete last_100_users; + } + + function bla(string bal) public { + grr = bal; + } +} \ No newline at end of file diff --git a/remix-analyzer/test/analysis/test-contracts/forgottenReturn.sol b/remix-analyzer/test/analysis/test-contracts/forgottenReturn.sol new file mode 100644 index 0000000000..eb3df75e44 --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/forgottenReturn.sol @@ -0,0 +1,13 @@ +contract Sheep { + string public name; + string public dna; + bool g = true; + function Sheep(string _name, string _dna) { + name = _name; + dna = _dna; + } + + function geneticallyEngineer(string _dna) returns (bool) { + dna = _dna; + } +} \ No newline at end of file diff --git a/remix-analyzer/test/analysis/test-contracts/functionParameters.sol b/remix-analyzer/test/analysis/test-contracts/functionParameters.sol new file mode 100644 index 0000000000..f1a9f4a0f0 --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/functionParameters.sol @@ -0,0 +1,16 @@ +pragma solidity ^0.4.18; + +contract B { + function plus(uint a, uint b) pure internal returns (uint) { + return a + b; + } + + function eval(function (uint, uint) pure internal returns (uint) f, uint x, uint y) pure internal returns (uint) { + return f(x, y); + } + + function calc(uint x, uint y) pure public returns (uint) { + return eval(plus, x, y); + // return plus(x, y); + } +} \ No newline at end of file diff --git a/remix-analyzer/test/analysis/test-contracts/globals.sol b/remix-analyzer/test/analysis/test-contracts/globals.sol new file mode 100644 index 0000000000..7df37c757f --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/globals.sol @@ -0,0 +1,58 @@ +pragma solidity ^0.4.9; +contract bla { + uint brr; + function duper() { + brr++; + } +} + +contract a is bla { + + function blub() { + brr++; + } + + function r () { + address a; + bytes32 hash; + uint8 v; + bytes32 r; + bytes32 s; + + block.blockhash(1); + block.coinbase; + block.difficulty; + block.gaslimit; + block.number; + block.timestamp; + msg.data; + msg.gas; + msg.sender; + msg.value; + now; + tx.gasprice; + tx.origin; + // assert(1 == 2); + // require(1 == 1); + keccak256(a); + sha3(a); + sha256(a); + ripemd160(a); + ecrecover(hash, v, r, s); + addmod(1, 2, 2); + mulmod(4,4,12); + + a.balance; + blub(); + a.send(a.balance); + + + + super.duper(); + //a.transfer(a.balance); + selfdestruct(a); + //revert(); + assert(a.balance == 0); + } + +} \ No newline at end of file diff --git a/remix-analyzer/test/analysis/test-contracts/inheritance.sol b/remix-analyzer/test/analysis/test-contracts/inheritance.sol new file mode 100644 index 0000000000..1491e8fa9f --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/inheritance.sol @@ -0,0 +1,40 @@ +pragma solidity ^0.4.9; + +contract r { + function s() constant {} +} + +contract a is r { + uint x = 1; + + function getX() constant returns (uint) { + return x; + } +} + +contract b is a { + uint y = 2; + uint x = 3; + + + function getY(uint z, bool r) returns (uint) { + return y++; + } + + function getY(string storage n) internal constant returns (uint) { return 10; } + +} + +contract c is b { + string x; + + function d() returns (uint a, uint b) { + //d(); + //sha3("bla"); + msg.sender.call.gas(200000).value(this.balance)(bytes4(sha3("pay()"))); + //x++; + getY(x); + a = getX() + getY(1, false); + b = getX() + getY(x); + } +} diff --git a/remix-analyzer/test/analysis/test-contracts/intDivisionTruncate.sol b/remix-analyzer/test/analysis/test-contracts/intDivisionTruncate.sol new file mode 100644 index 0000000000..a7c30fbfd1 --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/intDivisionTruncate.sol @@ -0,0 +1,28 @@ +pragma solidity ^0.4.19; + +contract CharityCampaign { + mapping (address => uint) contributions; + int128 feePercentage; + uint p2; + address processor; + address beneficiary; + + function CharityCampaign(address _beneficiary, int128 _feePercentage) public { + processor = msg.sender; + beneficiary = _beneficiary; + feePercentage = _feePercentage; + } + + function contribute() payable public returns (uint feeCollected) { + uint fee = msg.value * uint256(feePercentage / 100); + fee = msg.value * (p2 / 100); + contributions[msg.sender] = msg.value - fee; + processor.transfer(fee); + return fee; + } + + function endCampaign() public { + require(msg.sender == processor || msg.sender == beneficiary); + selfdestruct(beneficiary); + } +} \ No newline at end of file diff --git a/remix-analyzer/test/analysis/test-contracts/library.sol b/remix-analyzer/test/analysis/test-contracts/library.sol new file mode 100644 index 0000000000..3ca1454d49 --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/library.sol @@ -0,0 +1,54 @@ +pragma solidity ^0.4.0; + +library Set { + // We define a new struct datatype that will be used to + // hold its data in the calling contract. + struct Data { mapping(uint => bool) flags; } + + // Note that the first parameter is of type "storage + // reference" and thus only its storage address and not + // its contents is passed as part of the call. This is a + // special feature of library functions. It is idiomatic + // to call the first parameter 'self', if the function can + // be seen as a method of that object. + function insert(Data storage self, uint value) + returns (bool) + { + if (self.flags[value]) + return false; // already there + self.flags[value] = true; + + return true; + } + + function remove(Data storage self, uint value) + returns (bool) + { + if (!self.flags[value]) + return false; // not there + self.flags[value] = false; + return true; + } + + function contains(Data storage self, uint value) + returns (bool) + { + return self.flags[value]; + } +} + + +contract C { + Set.Data knownValues; + + function register(uint value) { + // The library functions can be called without a + // specific instance of the library, since the + // "instance" will be the current contract. + address a; + a.send(10 wei); + if (!Set.insert(knownValues, value)) + throw; + } + // In this contract, we can also directly access knownValues.flags, if we want. +} \ No newline at end of file diff --git a/remix-analyzer/test/analysis/test-contracts/modifier1.sol b/remix-analyzer/test/analysis/test-contracts/modifier1.sol new file mode 100644 index 0000000000..a755d763b1 --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/modifier1.sol @@ -0,0 +1,17 @@ +pragma solidity ^0.4.0; + +contract test { + + address owner; + + modifier onlyOwner { + var a = 0; + if (msg.sender != owner) + throw; + _; + } + + function b(address a) onlyOwner returns (bool) { + + } +} \ No newline at end of file diff --git a/remix-analyzer/test/analysis/test-contracts/modifier2.sol b/remix-analyzer/test/analysis/test-contracts/modifier2.sol new file mode 100644 index 0000000000..44db1617c7 --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/modifier2.sol @@ -0,0 +1,28 @@ +pragma solidity ^0.4.0; + +contract owned { + + uint r=0; + + modifier ntimes(uint n) { + for(uint i=0;i uint) shares; + /// Withdraw your share. + function withdraw() { + var share = shares[msg.sender]; + shares[msg.sender] = 0; + if (!msg.sender.send(share)) + throw; + } +} diff --git a/remix-analyzer/test/analysis/test-contracts/reentrant.sol b/remix-analyzer/test/analysis/test-contracts/reentrant.sol new file mode 100644 index 0000000000..896395e1a6 --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/reentrant.sol @@ -0,0 +1,49 @@ +pragma solidity ^0.4.0; + +contract InfoFeed { + uint c; + function info() constant returns (uint ret) {return c;} + function call1(uint a) constant returns (bool) {return true;} +} + +// THIS CONTRACT CONTAINS A BUG - DO NOT USE +contract Fund { + /// Mapping of ether shares of the contract. + //mapping(address => uint) shares; + /// Withdraw your share. + + uint c = 0; + function withdraw() constant { + InfoFeed f; + + + //shares[msg.sender] /= 1; + + f.info(); + + //if (msg.sender.send(shares[msg.sender])) throw; + // shares[msg.sender] = 0; + + + b(true, false); + //shares[msg.sender]++; + //c++; + + } + mapping(address => uint) shares; + + function b(bool a, bool b) returns (bool) { + mapping(address => uint) c = shares; + c[msg.sender] = 0; + //f(); + //withdraw(); + //shares[msg.sender]++; + //c++; + return true; + } + + function f() { + c++; + withdraw(); + } +} diff --git a/remix-analyzer/test/analysis/test-contracts/selfdestruct.sol b/remix-analyzer/test/analysis/test-contracts/selfdestruct.sol new file mode 100644 index 0000000000..d2e31dfdb0 --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/selfdestruct.sol @@ -0,0 +1,12 @@ +contract sd { + + function() public payable { } + + function c () public constant { + selfdestruct(address(0xdeadbeef)); + } + + function b () public payable { + selfdestruct(address(0xdeadbeef)); + } +} \ No newline at end of file diff --git a/remix-analyzer/test/analysis/test-contracts/structReentrant.sol b/remix-analyzer/test/analysis/test-contracts/structReentrant.sol new file mode 100644 index 0000000000..95952ff1ca --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/structReentrant.sol @@ -0,0 +1,36 @@ +pragma solidity ^0.4.9; + +contract Ballot { + + struct Voter { + uint weight; + bool voted; + uint8 vote; + address delegate; + baz foo; + } + + struct baz{ + uint bar; + } + + mapping(address => Voter) voters; + + /// Create a new ballot with $(_numProposals) different proposals. + function bla(address a) { + Voter x = voters[a]; + + if (!a.send(10)) + throw; + + //voters[a] = Voter(10,true,1,a); + //x.foo.bar *= 100; + bli(x); + } + + //function bla(){} + + function bli(Voter storage x) private { + x.foo.bar++; + } +} diff --git a/remix-analyzer/test/analysis/test-contracts/thisLocal.sol b/remix-analyzer/test/analysis/test-contracts/thisLocal.sol new file mode 100644 index 0000000000..e31cf0dbaf --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/thisLocal.sol @@ -0,0 +1,16 @@ +pragma solidity ^0.4.0; + +contract test { + + function (){ + address x; + this.b(x); + x.call('something'); + x.send(1 wei); + + } + + function b(address a) returns (bool) { + + } +} diff --git a/remix-analyzer/test/analysis/test-contracts/transfer.sol b/remix-analyzer/test/analysis/test-contracts/transfer.sol new file mode 100644 index 0000000000..49ddd515bf --- /dev/null +++ b/remix-analyzer/test/analysis/test-contracts/transfer.sol @@ -0,0 +1,7 @@ +contract c { + uint x; + function f(address r) { + r.transfer(1); + x = 2; + } +} \ No newline at end of file diff --git a/remix-analyzer/test/tests.js b/remix-analyzer/test/tests.js new file mode 100644 index 0000000000..0df3206d12 --- /dev/null +++ b/remix-analyzer/test/tests.js @@ -0,0 +1,4 @@ + +require('./analysis/staticAnalysisCommon-test.js') +require('./analysis/staticAnalysisIntegration-test.js') +require('./analysis/staticAnalysisIssues-test.js') diff --git a/remix-debug/README.md b/remix-debug/README.md new file mode 100644 index 0000000000..f198c26d75 --- /dev/null +++ b/remix-debug/README.md @@ -0,0 +1,232 @@ +# `remix-debug` + +remix-debug wrap other remix-* libraries and can be used to debug Ethereum transactions. + ++ [Installation](#installation) ++ [Development](#development) + +## Installation + + +```bash +npm install remix-debug +``` + +## Development + +```javascript +var Debugger = require('remix-debug').EthDebugger +var BreakpointManager = require('remix-debug').EthDebugger + +var debugger = new Debugger({ + compilationResult: () => { + return compilationResult // that helps resolving source location + } +}) + +debugger.addProvider(web3, 'web3') +debugger.switchProvider('web3') + +var breakPointManager = new remixCore.code.BreakpointManager(this.debugger, (sourceLocation) => { + // return offsetToLineColumn +}) +debugger.setBreakpointManager(breakPointManager) +breakPointManager.add({fileName, row}) +breakPointManager.add({fileName, row}) + +debugger.debug() + +// this.traceManager.getCurrentCalledAddressAt + +debugger.event.register('newTraceLoaded', () => { + // start doing basic stuff like retrieving step details + debugger.traceManager.getCallStackAt(34, (error, callstack) => {}) +}) + +debugger.callTree.register('callTreeReady', () => { + // start doing more complex stuff like resolvng local variables + breakPointManager.jumpNextBreakpoint(true) + + var storageView = debugger.storageViewAt(38, , + storageView.storageSlot(0, (error, storage) => {}) + storageView.storageRange(error, storage) => {}) // retrieve 0 => 1000 slots + + debugger.extractStateAt(23, (error, state) => { + debugger.decodeStateAt(23, state, (error, decodedState) => {}) + }) + + debugger.sourceLocationFromVMTraceIndex(, 23, (error, location) => { + debugger.decodeLocalsAt(23, location, (error, decodedlocals) => {}) + }) + + debugger.extractLocalsAt(23, (null, locals) => {}) + +}) +``` + +## Library + +Provides: + +```javascript +{ + code: { + CodeManager: CodeManager, + BreakpointManager: BreakpointManager + }, + storage: { + StorageViewer: StorageViewer, + StorageResolver: StorageResolver + }, + trace: { + TraceManager: TraceManager + } +} +``` + + +TraceManager is a convenient way to access a VM Trace and resolve some value from it. + +`TraceManager()` : + +`function resolveTrace(stepIndex, tx)` + +`function init(stepIndex, tx)` + +`function inRange(stepIndex, tx)` + +`function isLoaded(stepIndex, tx)` + +`function getLength(stepIndex, tx)` + +`function accumulateStorageChanges(stepIndex, tx)` + +`function getAddresses(stepIndex, tx)` + +`function getCallDataAt(stepIndex, tx)` + +`function getCallStackAt(stepIndex, tx)` + +`function getStackAt(stepIndex, tx)` + +`function getLastCallChangeSince(stepIndex, tx)` + +`function getCurrentCalledAddressAt(stepIndex, tx)` + +`function getContractCreationCode(stepIndex, tx)` + +`function getMemoryAt(stepIndex, tx)` + +`function getCurrentPC(stepIndex, tx)` + +`function getReturnValue(stepIndex, tx)` + +`function getCurrentStep(stepIndex, tx)` + +`function getMemExpand(stepIndex, tx)` + +`function getStepCost(stepIndex, tx)` + +`function getRemainingGas(stepIndex, tx)` + +`function getStepCost(stepIndex, tx)` + +`function isCreationStep(stepIndex, tx)` + +`function findStepOverBack(stepIndex, tx)` + +`function findStepOverForward(stepIndex, tx)` + +`function findStepOverBack(stepIndex, tx)` + +`function findNextCall(stepIndex, tx)` + +`function findStepOut(stepIndex, tx)` + +`function checkRequestedStep(stepIndex, tx)` + +`function waterfall(stepIndex, tx)` + + +- - - - + +`CodeManager(_traceManager)` : + +`function getCode(stepIndex, tx)` : +Resolve the code of the given @arg stepIndex and trigger appropriate event + +`function resolveStep(address, cb)` : +Retrieve the code located at the given @arg address + +`function getFunctionFromStep(stepIndex, sourceMap, ast)` : +Retrieve the called function for the current vm step + +`function getInstructionIndex(address, step, callback)` : +Retrieve the instruction index of the given @arg step + +`function getFunctionFromPC(address, pc, sourceMap, ast)` : +Retrieve the called function for the given @arg pc and @arg address + +- - - - + +`BreakpointManager(_ethdebugger, _locationToRowConverter)` : + +`function jumpNextBreakpoint(defaultToLimit)` : +start looking for the next breakpoint + +`function jumpPreviousBreakpoint(defaultToLimit)` : +start looking for the previous breakpoint + +`function jump(direction, defaultToLimit)` : +start looking for the previous or next breakpoint + +`function hasBreakpointAtLine((fileIndex, line)` : +check the given pair fileIndex/line against registered breakpoints + +`function hasBreakpoint()` : +return true if current manager has breakpoint + +`function add(sourceLocation)` : +add a new breakpoint to the manager + +`function remove(sourceLocation)` : +remove a breakpoint from the manager + +- - - - + +`StorageViewer(_context, _storageResolver, _traceManager)` : + +`function storageRange(defaultToLimit)` : +return the storage for the current context (address and vm trace index) + +`function storageSlot(defaultToLimit)` : +return a slot value for the current context (address and vm trace index) + +`function isComplete(direction, defaultToLimit)` : +return True if the storage at @arg address is complete + +`function initialMappingsLocation((fileIndex, line)` : +return all the possible mappings locations for the current context (cached) do not return state changes during the current transaction + +`function mappingsLocation()` : +return all the possible mappings locations for the current context (cached) and current mapping slot. returns state changes during the current transaction + +`function extractMappingsLocationChanges(sourceLocation)` : +retrieve mapping location changes from the storage changes. + +- - - - + +`StorageResolver()` : + +`function storageRange(tx, stepIndex, address, callback)` : +return the storage for the current context (address and vm trace index) + +`function initialPreimagesMappings(tx, stepIndex, address, callback)` : +return a slot value for the current context (address and vm trace index) + +`function storageSlot(slot, tx, stepIndex, address, callback)` : +return True if the storage at @arg address is complete + +`function isComplete(address)` : +return all the possible mappings locations for the current context (cached) do not return state changes during the current transaction + diff --git a/remix-debug/index.js b/remix-debug/index.js new file mode 100644 index 0000000000..1f0f85d278 --- /dev/null +++ b/remix-debug/index.js @@ -0,0 +1,39 @@ +'use strict' +var EthDebugger = require('./src/Ethdebugger') + +var CodeManager = require('./src/code/codeManager') +var BreakpointManager = require('./src/code/breakpointManager') +var StorageViewer = require('./src/storage/storageViewer') +var StorageResolver = require('./src/storage/storageResolver') +var TraceManager = require('./src/trace/traceManager') + +/* + Use of breakPointManager : + + var breakPointManager = new BreakpointManager(this.debugger, (sourceLocation) => { + return line/column from offset (sourceLocation) + }) + this.debugger.setBreakpointManager(breakPointManager) +*/ +module.exports = { + EthDebugger: EthDebugger, + /** + * constructor + * + * @param {Object} _debugger - type of EthDebugger + * @return {Function} _locationToRowConverter - function implemented by editor which return a column/line position for a char source location + */ + BreakpointManager: BreakpointManager, + code: { + CodeManager: CodeManager, + BreakpointManager: BreakpointManager + }, + storage: { + StorageViewer: StorageViewer, + StorageResolver: StorageResolver + }, + trace: { + TraceManager: TraceManager + } +} + diff --git a/remix-debug/package.json b/remix-debug/package.json new file mode 100644 index 0000000000..e275a6e8f2 --- /dev/null +++ b/remix-debug/package.json @@ -0,0 +1,116 @@ +{ + "name": "remix-debug", + "version": "0.0.9", + "description": "Ethereum IDE and tools for the web", + "contributors": [ + { + "name": "Yann Levreau", + "email": "yann@ethdev.com" + }, + { + "name": "Liana Husikyan", + "email": "liana@ethdev.com" + } + ], + "main": "./index.js", + "dependencies": { + "babel-eslint": "^7.1.1", + "babel-plugin-transform-object-assign": "^6.22.0", + "babel-plugin-yo-yoify": "^0.3.3", + "babel-polyfill": "^6.22.0", + "babel-preset-env": "^1.6.1", + "babel-preset-es2015": "^6.24.0", + "babel-preset-stage-0": "^6.24.1", + "babelify": "^7.3.0", + "ethereumjs-util": "^4.5.0", + "ethereumjs-vm": "^2.3.3", + "notify-error": "^1.2.0", + "npm-run-all": "^4.1.2", + "fast-async": "^6.1.2", + "remix-lib": "^0.2.9", + "solc": "https://github.com/ethereum/solc-js" + }, + "devDependencies": { + "standard": "^7.0.1", + "standard-reporter": "^1.0.5", + "tape": "^4.6.0" + }, + "scripts": { + "build": "mkdirp build; browserify index.js > build/app.js", + "lint": "standard | notify-error", + "downloadsolc": "cd node_modules/solc && (test -e soljson.js || wget --no-check-certificate https://solc-bin.ethereum.org/soljson.js) && cd ..", + "test": "standard && npm run downloadsolc && tape ./test/tests.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ethereum/remix.git" + }, + "author": "cpp-ethereum team", + "license": "MIT", + "bugs": { + "url": "https://github.com/ethereum/remix/issues" + }, + "homepage": "https://github.com/ethereum/remix#readme", + "standard": { + "ignore": [ + "node_modules/*", + "build/*", + "test/resources/*" + ], + "parser": "babel-eslint" + }, + "babel": { + "plugins": [ + "transform-es2015-template-literals", + "transform-es2015-literals", + "transform-es2015-function-name", + "transform-es2015-arrow-functions", + "transform-es2015-block-scoped-functions", + "transform-es2015-classes", + "transform-es2015-object-super", + "transform-es2015-shorthand-properties", + "transform-es2015-duplicate-keys", + "transform-es2015-computed-properties", + "transform-es2015-for-of", + "transform-es2015-sticky-regex", + "transform-es2015-unicode-regex", + "check-es2015-constants", + "transform-es2015-spread", + "transform-es2015-parameters", + "transform-es2015-destructuring", + "transform-es2015-block-scoping", + "transform-object-assign" + ] + }, + "browserify": { + "transform": [ + [ + "babelify", + { + "sourceMapsAbsolute": false, + "sourceMaps": true, + "plugins": [ + [ + [ + "fast-async", + { + "runtimePatten": null, + "compiler": { + "promises": true, + "es7": true, + "noRuntime": true, + "wrapAwait": true + } + } + ], + "transform-object-assign" + ] + ], + "presets": [ + "es2015" + ] + } + ] + ] + } +} diff --git a/remix-debug/src/Ethdebugger.js b/remix-debug/src/Ethdebugger.js new file mode 100644 index 0000000000..6e2c8997dd --- /dev/null +++ b/remix-debug/src/Ethdebugger.js @@ -0,0 +1,240 @@ +'use strict' + +var CodeManager = require('./code/codeManager') +var StorageViewer = require('./storage/storageViewer') +var StorageResolver = require('./storage/storageResolver') +var TraceManager = require('./trace/traceManager') + +var SolidityProxy = require('./decoder/solidityProxy') +var stateDecoder = require('./decoder/stateDecoder') +var localDecoder = require('./decoder/localDecoder') +var InternalCallTree = require('./decoder/internalCallTree') + +var remixLib = require('remix-lib') +var traceHelper = remixLib.helpers.trace +var init = remixLib.init +var executionContext = remixLib.execution.executionContext +var EventManager = remixLib.EventManager +var Web3Providers = remixLib.vm.Web3Providers +var DummyProvider = remixLib.vm.DummyProvider + +/** + * Ethdebugger is a wrapper around a few classes that helps debugging a transaction + * + * - Web3Providers - define which environment (web3) the transaction will be retrieved from + * - TraceManager - Load / Analyze the trace and retrieve details of specific test + * - CodeManager - Retrieve loaded byte code and help to resolve AST item from vmtrace index + * - SolidityProxy - Basically used to extract state variable from AST + * - Breakpoint Manager - Used to add / remove / jumpto breakpoint + * - InternalCallTree - Used to retrieved local variables + * - StorageResolver - Help resolving the storage accross different steps + * + * @param {Map} opts - { function compilationResult } // + */ +function Ethdebugger (opts) { + this.opts = opts || {} + if (!this.opts.compilationResult) this.opts.compilationResult = () => { return null } + + this.web3 = opts.web3 + + this.event = new EventManager() + + this.tx + + this.web3Providers = new Web3Providers() + this.addProvider('DUMMYWEB3', new DummyProvider()) + this.switchProvider('DUMMYWEB3') + + this.traceManager = new TraceManager({web3: this.web3}) + this.codeManager = new CodeManager(this.traceManager) + this.solidityProxy = new SolidityProxy(this.traceManager, this.codeManager) + this.storageResolver = null + + this.callTree = new InternalCallTree(this.event, this.traceManager, this.solidityProxy, this.codeManager, { includeLocalVariables: true }) +} + +Ethdebugger.prototype.setManagers = function () { + this.traceManager = new TraceManager({web3: this.web3}) + this.codeManager = new CodeManager(this.traceManager) + this.solidityProxy = new SolidityProxy(this.traceManager, this.codeManager) + this.storageResolver = null + + this.callTree = new InternalCallTree(this.event, this.traceManager, this.solidityProxy, this.codeManager, { includeLocalVariables: true }) +} + +Ethdebugger.prototype.resolveStep = function (index) { + this.codeManager.resolveStep(index, this.tx) +} + +Ethdebugger.prototype.setCompilationResult = function (compilationResult) { + if (compilationResult && compilationResult.sources && compilationResult.contracts) { + this.solidityProxy.reset(compilationResult) + } else { + this.solidityProxy.reset({}) + } +} + +/* resolve source location */ +Ethdebugger.prototype.sourceLocationFromVMTraceIndex = function (address, stepIndex, callback) { + this.callTree.sourceLocationTracker.getSourceLocationFromVMTraceIndex(address, stepIndex, this.solidityProxy.contracts, (error, rawLocation) => { + callback(error, rawLocation) + }) +} + +Ethdebugger.prototype.sourceLocationFromInstructionIndex = function (address, instIndex, callback) { + this.debugger.callTree.sourceLocationTracker.getSourceLocationFromInstructionIndex(address, instIndex, this.solidityProxy.contracts, function (error, rawLocation) { + callback(error, rawLocation) + }) +} + +/* breakpoint */ +Ethdebugger.prototype.setBreakpointManager = function (breakpointManager) { + this.breakpointManager = breakpointManager +} + +/* decode locals */ +Ethdebugger.prototype.extractLocalsAt = function (step, callback) { + callback(null, this.callTree.findScope(step)) +} + +Ethdebugger.prototype.decodeLocalsAt = function (step, sourceLocation, callback) { + this.traceManager.waterfall([ + this.traceManager.getStackAt, + this.traceManager.getMemoryAt, + this.traceManager.getCurrentCalledAddressAt], + step, + (error, result) => { + if (!error) { + var stack = result[0].value + var memory = result[1].value + try { + var storageViewer = new StorageViewer({ + stepIndex: step, + tx: this.tx, + address: result[2].value + }, this.storageResolver, this.traceManager) + localDecoder.solidityLocals(step, this.callTree, stack, memory, storageViewer, sourceLocation).then((locals) => { + if (!locals.error) { + callback(null, locals) + } else { + callback(locals.error) + } + }) + } catch (e) { + callback(e.message) + } + } else { + callback(error) + } + }) +} + +/* decode state */ +Ethdebugger.prototype.extractStateAt = function (step, callback) { + this.solidityProxy.extractStateVariablesAt(step, function (error, stateVars) { + callback(error, stateVars) + }) +} + +Ethdebugger.prototype.decodeStateAt = function (step, stateVars, callback) { + this.traceManager.getCurrentCalledAddressAt(step, (error, address) => { + if (error) return callback(error) + var storageViewer = new StorageViewer({ + stepIndex: step, + tx: this.tx, + address: address + }, this.storageResolver, this.traceManager) + stateDecoder.decodeState(stateVars, storageViewer).then((result) => { + if (!result.error) { + callback(null, result) + } else { + callback(result.error) + } + }) + }) +} + +Ethdebugger.prototype.storageViewAt = function (step, address) { + return new StorageViewer({ + stepIndex: step, + tx: this.tx, + address: address + }, this.storageResolver, this.traceManager) +} +/* set env */ +Ethdebugger.prototype.web3 = function () { + return this.web3 +} + +Ethdebugger.prototype.addProvider = function (type, obj) { + this.web3Providers.addProvider(type, obj) + this.event.trigger('providerAdded', [type]) +} + +Ethdebugger.prototype.switchProvider = function (type) { + var self = this + this.web3Providers.get(type, function (error, obj) { + if (error) { + console.log('provider ' + type + ' not defined') + } else { + self.web3 = obj + self.setManagers() + // self.traceManager.web3 = self.web3 + executionContext.detectNetwork((error, network) => { + if (error || !network) { + self.web3Debug = obj + self.web3 = obj + } else { + var webDebugNode = init.web3DebugNode(network.name) + self.web3Debug = !webDebugNode ? obj : webDebugNode + self.web3 = !webDebugNode ? obj : webDebugNode + } + self.setManagers() + }) + self.event.trigger('providerChanged', [type]) + } + }) +} + +Ethdebugger.prototype.debug = function (tx) { + this.setCompilationResult(this.opts.compilationResult()) + if (tx instanceof Object) { + this.txBrowser.load(tx.hash) + } else if (tx instanceof String) { + this.txBrowser.load(tx) + } +} + +Ethdebugger.prototype.unLoad = function () { + this.traceManager.init() + this.codeManager.clear() + this.stepManager.reset() + this.event.trigger('traceUnloaded') +} + +Ethdebugger.prototype.debug = function (tx) { + if (this.traceManager.isLoading) { + return + } + if (!tx.to) { + tx.to = traceHelper.contractCreationToken('0') + } + this.setCompilationResult(this.opts.compilationResult()) + console.log('loading trace...') + this.tx = tx + var self = this + this.traceManager.resolveTrace(tx, function (error, result) { + console.log('trace loaded ' + result) + if (result) { + self.event.trigger('newTraceLoaded', [self.traceManager.trace]) + if (self.breakpointManager && self.breakpointManager.hasBreakpoint()) { + self.breakpointManager.jumpNextBreakpoint(false) + } + self.storageResolver = new StorageResolver({web3: self.traceManager.web3}) + } else { + self.statusMessage = error ? error.message : 'Trace not loaded' + } + }) +} + +module.exports = Ethdebugger diff --git a/remix-debug/src/code/breakpointManager.js b/remix-debug/src/code/breakpointManager.js new file mode 100644 index 0000000000..4479f0ec4e --- /dev/null +++ b/remix-debug/src/code/breakpointManager.js @@ -0,0 +1,186 @@ +'use strict' +var remixLib = require('remix-lib') +var EventManager = remixLib.EventManager +var helper = remixLib.helpers.trace + +/** + * allow to manage breakpoint + * + * Trigger events: breakpointHit, breakpointAdded, breakpointRemoved + */ +class BreakpointManager { + /** + * constructor + * + * @param {Object} _debugger - type of EthDebugger + * @return {Function} _locationToRowConverter - function implemented by editor which return a column/line position for a char source location + */ + constructor (_debugger, _locationToRowConverter) { + this.event = new EventManager() + this.debugger = _debugger + this.breakpoints = {} + this.locationToRowConverter = _locationToRowConverter + this.previousLine + } + + /** + * start looking for the next breakpoint + * @param {Bool} defaultToLimit - if true jump to the end of the trace if no more breakpoint found + * + */ + async jumpNextBreakpoint (fromStep, defaultToLimit) { + this.jump(fromStep || 0, 1, defaultToLimit) + } + + /** + * start looking for the previous breakpoint + * @param {Bool} defaultToLimit - if true jump to the start of the trace if no more breakpoint found + * + */ + async jumpPreviousBreakpoint (fromStep, defaultToLimit) { + this.jump(fromStep || 0, -1, defaultToLimit) + } + + /** + * start looking for the previous or next breakpoint + * @param {Int} direction - 1 or -1 direction of the search + * @param {Bool} defaultToLimit - if true jump to the limit (end if direction is 1, beginning if direction is -1) of the trace if no more breakpoint found + * + */ + async jump (fromStep, direction, defaultToLimit) { + if (!this.locationToRowConverter) { + console.log('row converter not provided') + return + } + + function depthChange (step, trace) { + return trace[step].depth !== trace[step - 1].depth + } + + function hitLine (currentStep, sourceLocation, previousSourceLocation, self) { + // isJumpDestInstruction -> returning from a internal function call + // depthChange -> returning from an external call + // sourceLocation.start <= previousSourceLocation.start && ... -> previous src is contained in the current one + if ((helper.isJumpDestInstruction(self.debugger.traceManager.trace[currentStep]) && previousSourceLocation.jump === 'o') || + depthChange(currentStep, self.debugger.traceManager.trace) || + (sourceLocation.start <= previousSourceLocation.start && + sourceLocation.start + sourceLocation.length >= previousSourceLocation.start + previousSourceLocation.length)) { + return false + } else { + if (self.debugger.stepManager) self.debugger.stepManager.jumpTo(currentStep) + self.event.trigger('breakpointHit', [sourceLocation, currentStep]) + return true + } + } + + var sourceLocation + var previousSourceLocation + var currentStep = fromStep + direction + var lineHadBreakpoint = false + while (currentStep > 0 && currentStep < this.debugger.traceManager.trace.length) { + try { + previousSourceLocation = sourceLocation + sourceLocation = await this.debugger.callTree.extractSourceLocation(currentStep) + } catch (e) { + console.log('cannot jump to breakpoint ' + e) + return + } + var lineColumn = this.locationToRowConverter(sourceLocation) + if (this.previousLine !== lineColumn.start.line) { + if (direction === -1 && lineHadBreakpoint) { // TODO : improve this when we will build the correct structure before hand + lineHadBreakpoint = false + if (hitLine(currentStep + 1, previousSourceLocation, sourceLocation, this)) { + return + } + } + this.previousLine = lineColumn.start.line + if (this.hasBreakpointAtLine(sourceLocation.file, lineColumn.start.line)) { + lineHadBreakpoint = true + if (direction === 1) { + if (hitLine(currentStep, sourceLocation, previousSourceLocation, this)) { + return + } + } + } + } + currentStep += direction + } + this.event.trigger('NoBreakpointHit', []) + if (this.debugger.stepManager && defaultToLimit) { + if (direction === -1) { + this.debugger.stepManager.jumpTo(0) + } else if (direction === 1) { + this.debugger.stepManager.jumpTo(this.debugger.traceManager.trace.length - 1) + } + } + } + + /** + * check the given pair fileIndex/line against registered breakpoints + * + * @param {Int} fileIndex - index of the file content (from the compilation result) + * @param {Int} line - line number where looking for breakpoint + * @return {Bool} return true if the given @arg fileIndex @arg line refers to a breakpoint + */ + hasBreakpointAtLine (fileIndex, line) { + var filename = this.debugger.solidityProxy.fileNameFromIndex(fileIndex) + if (filename && this.breakpoints[filename]) { + var sources = this.breakpoints[filename] + for (var k in sources) { + var source = sources[k] + if (line === source.row) { + return true + } + } + } + return false + } + + /** + * return true if current manager has breakpoint + * + * @return {Bool} true if breapoint registered + */ + hasBreakpoint () { + for (var k in this.breakpoints) { + if (this.breakpoints[k].length) { + return true + } + } + return false + } + + /** + * add a new breakpoint to the manager + * + * @param {Object} sourceLocation - position of the breakpoint { file: '', row: '', row: ' raw.length) { + for (var j = opcode.pushData.length; j < length; j++) { + opcode.pushData.push(0) + } + } + i += length + } + code.push(opcode) + } + return code + }, + pad: function (num, size) { var s = num + '' while (s.length < size) s = '0' + s diff --git a/remix-debug/src/code/disassembler.js b/remix-debug/src/code/disassembler.js new file mode 100644 index 0000000000..69c6082c65 --- /dev/null +++ b/remix-debug/src/code/disassembler.js @@ -0,0 +1,58 @@ +'use strict' + +var parseCode = require('./codeUtils').parseCode +var remixLib = require('remix-lib') +var util = remixLib.util + +var createExpressions = function (instructions) { + var expressions = [] + var labels = 0 + for (var i = 0; i < instructions.length; i++) { + var expr = instructions[i] + expr.functional = false + if (expr.name === 'JUMPDEST') { + expr.label = 'label' + (++labels) + } else if (expr.name.slice(0, 3) === 'DUP') { + } else if (expr.name.slice(0, 4) === 'SWAP') { + } else if (expr.out <= 1 && expr.in <= expressions.length) { + var error = false + for (var j = 0; j < expr.in && !error; j++) { + var arg = expressions[expressions.length - j - 1] + if (!arg.functional || arg.out !== 1) { + error = true + break + } + } + if (!error) { + expr.args = expressions.splice(expressions.length - expr.in) + expr.functional = true + } + } + expressions.push(expr) + } + return expressions +} + +var toString = function (expr) { + if (expr.name.slice(0, 4) === 'PUSH') { + return util.hexConvert(expr.pushData) + } else if (expr.name === 'JUMPDEST') { + return expr.label + ':' + } else if (expr.args) { + return expr.name.toLowerCase() + '(' + expr.args.reverse().map(toString).join(', ') + ')' + } else { + return expr.name.toLowerCase() + } +} + +var disassemble = function (input) { + var code = parseCode(util.hexToIntArray(input)) + return createExpressions(code).map(toString).join('\n') +} + +module.exports = { + /** + * Disassembler that turns bytecode (as a hex string) into Solidity inline assembly. + */ + disassemble: disassemble +} diff --git a/remix-debug/src/code/opcodes.js b/remix-debug/src/code/opcodes.js new file mode 100644 index 0000000000..765149dbbf --- /dev/null +++ b/remix-debug/src/code/opcodes.js @@ -0,0 +1,184 @@ +'use strict' +var codes = { + // 0x0 range - arithmetic ops + // name, baseCost, off stack, on stack, dynamic, async + 0x00: ['STOP', 0, 0, 0, false], + 0x01: ['ADD', 3, 2, 1, false], + 0x02: ['MUL', 5, 2, 1, false], + 0x03: ['SUB', 3, 2, 1, false], + 0x04: ['DIV', 5, 2, 1, false], + 0x05: ['SDIV', 5, 2, 1, false], + 0x06: ['MOD', 5, 2, 1, false], + 0x07: ['SMOD', 5, 2, 1, false], + 0x08: ['ADDMOD', 8, 3, 1, false], + 0x09: ['MULMOD', 8, 3, 1, false], + 0x0a: ['EXP', 10, 2, 1, false], + 0x0b: ['SIGNEXTEND', 5, 2, 1, false], + + // 0x10 range - bit ops + 0x10: ['LT', 3, 2, 1, false], + 0x11: ['GT', 3, 2, 1, false], + 0x12: ['SLT', 3, 2, 1, false], + 0x13: ['SGT', 3, 2, 1, false], + 0x14: ['EQ', 3, 2, 1, false], + 0x15: ['ISZERO', 3, 1, 1, false], + 0x16: ['AND', 3, 2, 1, false], + 0x17: ['OR', 3, 2, 1, false], + 0x18: ['XOR', 3, 2, 1, false], + 0x19: ['NOT', 3, 1, 1, false], + 0x1a: ['BYTE', 3, 2, 1, false], + + // 0x20 range - crypto + 0x20: ['SHA3', 30, 2, 1, false], + + // 0x30 range - closure state + 0x30: ['ADDRESS', 2, 0, 1, true], + 0x31: ['BALANCE', 400, 1, 1, true, true], + 0x32: ['ORIGIN', 2, 0, 1, true], + 0x33: ['CALLER', 2, 0, 1, true], + 0x34: ['CALLVALUE', 2, 0, 1, true], + 0x35: ['CALLDATALOAD', 3, 1, 1, true], + 0x36: ['CALLDATASIZE', 2, 0, 1, true], + 0x37: ['CALLDATACOPY', 3, 3, 0, true], + 0x38: ['CODESIZE', 2, 0, 1, false], + 0x39: ['CODECOPY', 3, 3, 0, false], + 0x3a: ['GASPRICE', 2, 0, 1, false], + 0x3b: ['EXTCODESIZE', 700, 1, 1, true, true], + 0x3c: ['EXTCODECOPY', 700, 4, 0, true, true], + 0x3d: ['RETURNDATASIZE', 2, 0, 1, true], + 0x3e: ['RETURNDATACOPY', 3, 3, 0, true], + + // '0x40' range - block operations + 0x40: ['BLOCKHASH', 20, 1, 1, true, true], + 0x41: ['COINBASE', 2, 0, 1, true], + 0x42: ['TIMESTAMP', 2, 0, 1, true], + 0x43: ['NUMBER', 2, 0, 1, true], + 0x44: ['DIFFICULTY', 2, 0, 1, true], + 0x45: ['GASLIMIT', 2, 0, 1, true], + + // 0x50 range - 'storage' and execution + 0x50: ['POP', 2, 1, 0, false], + 0x51: ['MLOAD', 3, 1, 1, false], + 0x52: ['MSTORE', 3, 2, 0, false], + 0x53: ['MSTORE8', 3, 2, 0, false], + 0x54: ['SLOAD', 200, 1, 1, true, true], + 0x55: ['SSTORE', 0, 2, 0, true, true], + 0x56: ['JUMP', 8, 1, 0, false], + 0x57: ['JUMPI', 10, 2, 0, false], + 0x58: ['PC', 2, 0, 1, false], + 0x59: ['MSIZE', 2, 0, 1, false], + 0x5a: ['GAS', 2, 0, 1, false], + 0x5b: ['JUMPDEST', 1, 0, 0, false], + + // 0x60, range + 0x60: ['PUSH1', 3, 0, 1, false], + 0x61: ['PUSH2', 3, 0, 1, false], + 0x62: ['PUSH3', 3, 0, 1, false], + 0x63: ['PUSH4', 3, 0, 1, false], + 0x64: ['PUSH5', 3, 0, 1, false], + 0x65: ['PUSH6', 3, 0, 1, false], + 0x66: ['PUSH7', 3, 0, 1, false], + 0x67: ['PUSH8', 3, 0, 1, false], + 0x68: ['PUSH9', 3, 0, 1, false], + 0x69: ['PUSH10', 3, 0, 1, false], + 0x6a: ['PUSH11', 3, 0, 1, false], + 0x6b: ['PUSH12', 3, 0, 1, false], + 0x6c: ['PUSH13', 3, 0, 1, false], + 0x6d: ['PUSH14', 3, 0, 1, false], + 0x6e: ['PUSH15', 3, 0, 1, false], + 0x6f: ['PUSH16', 3, 0, 1, false], + 0x70: ['PUSH17', 3, 0, 1, false], + 0x71: ['PUSH18', 3, 0, 1, false], + 0x72: ['PUSH19', 3, 0, 1, false], + 0x73: ['PUSH20', 3, 0, 1, false], + 0x74: ['PUSH21', 3, 0, 1, false], + 0x75: ['PUSH22', 3, 0, 1, false], + 0x76: ['PUSH23', 3, 0, 1, false], + 0x77: ['PUSH24', 3, 0, 1, false], + 0x78: ['PUSH25', 3, 0, 1, false], + 0x79: ['PUSH26', 3, 0, 1, false], + 0x7a: ['PUSH27', 3, 0, 1, false], + 0x7b: ['PUSH28', 3, 0, 1, false], + 0x7c: ['PUSH29', 3, 0, 1, false], + 0x7d: ['PUSH30', 3, 0, 1, false], + 0x7e: ['PUSH31', 3, 0, 1, false], + 0x7f: ['PUSH32', 3, 0, 1, false], + + 0x80: ['DUP1', 3, 0, 1, false], + 0x81: ['DUP2', 3, 0, 1, false], + 0x82: ['DUP3', 3, 0, 1, false], + 0x83: ['DUP4', 3, 0, 1, false], + 0x84: ['DUP5', 3, 0, 1, false], + 0x85: ['DUP6', 3, 0, 1, false], + 0x86: ['DUP7', 3, 0, 1, false], + 0x87: ['DUP8', 3, 0, 1, false], + 0x88: ['DUP9', 3, 0, 1, false], + 0x89: ['DUP10', 3, 0, 1, false], + 0x8a: ['DUP11', 3, 0, 1, false], + 0x8b: ['DUP12', 3, 0, 1, false], + 0x8c: ['DUP13', 3, 0, 1, false], + 0x8d: ['DUP14', 3, 0, 1, false], + 0x8e: ['DUP15', 3, 0, 1, false], + 0x8f: ['DUP16', 3, 0, 1, false], + + 0x90: ['SWAP1', 3, 0, 0, false], + 0x91: ['SWAP2', 3, 0, 0, false], + 0x92: ['SWAP3', 3, 0, 0, false], + 0x93: ['SWAP4', 3, 0, 0, false], + 0x94: ['SWAP5', 3, 0, 0, false], + 0x95: ['SWAP6', 3, 0, 0, false], + 0x96: ['SWAP7', 3, 0, 0, false], + 0x97: ['SWAP8', 3, 0, 0, false], + 0x98: ['SWAP9', 3, 0, 0, false], + 0x99: ['SWAP10', 3, 0, 0, false], + 0x9a: ['SWAP11', 3, 0, 0, false], + 0x9b: ['SWAP12', 3, 0, 0, false], + 0x9c: ['SWAP13', 3, 0, 0, false], + 0x9d: ['SWAP14', 3, 0, 0, false], + 0x9e: ['SWAP15', 3, 0, 0, false], + 0x9f: ['SWAP16', 3, 0, 0, false], + + 0xa0: ['LOG0', 375, 2, 0, false], + 0xa1: ['LOG1', 375, 3, 0, false], + 0xa2: ['LOG2', 375, 4, 0, false], + 0xa3: ['LOG3', 375, 5, 0, false], + 0xa4: ['LOG4', 375, 6, 0, false], + + // '0xf0' range - closures + 0xf0: ['CREATE', 32000, 3, 1, true, true], + 0xf1: ['CALL', 700, 7, 1, true, true], + 0xf2: ['CALLCODE', 700, 7, 1, true, true], + 0xf3: ['RETURN', 0, 2, 0, false], + 0xf4: ['DELEGATECALL', 700, 6, 1, true, true], + 0xfa: ['STATICCALL', 700, 6, 1, true, true], + 0xfd: ['REVERT', 0, 2, 0, false], + + // '0x70', range - other + 0xfe: ['INVALID', 0, 0, 0, false], + 0xff: ['SELFDESTRUCT', 5000, 1, 0, false, true] +} + +module.exports = function (op, full) { + var code = codes[op] ? codes[op] : ['INVALID', 0, 0, 0, false, false] + var opcode = code[0] + + if (full) { + if (opcode === 'LOG') { + opcode += op - 0xa0 + } + + if (opcode === 'PUSH') { + opcode += op - 0x5f + } + + if (opcode === 'DUP') { + opcode += op - 0x7f + } + + if (opcode === 'SWAP') { + opcode += op - 0x8f + } + } + + return {name: opcode, fee: code[1], in: code[2], out: code[3], dynamic: code[4], async: code[5]} +} diff --git a/remix-debug/src/decoder/astHelper.js b/remix-debug/src/decoder/astHelper.js new file mode 100644 index 0000000000..88b3a63165 --- /dev/null +++ b/remix-debug/src/decoder/astHelper.js @@ -0,0 +1,105 @@ +'use strict' +var remixLib = require('remix-lib') +var AstWalker = remixLib.AstWalker + +/** + * return all contract definitions of the given @astList + * + * @param {Object} sourcesList - sources list (containing root AST node) + * @return {Object} - returns a mapping from AST node ids to AST nodes for the contracts + */ +function extractContractDefinitions (sourcesList) { + var ret = { + contractsById: {}, + contractsByName: {}, + sourcesByContract: {} + } + var walker = new AstWalker() + for (var k in sourcesList) { + walker.walk(sourcesList[k].legacyAST, { 'ContractDefinition': function (node) { + ret.contractsById[node.id] = node + ret.sourcesByContract[node.id] = k + ret.contractsByName[k + ':' + node.attributes.name] = node + return false + }}) + } + return ret +} + +/** + * returns the linearized base contracts of the contract @arg id + * + * @param {Int} id - contract id to resolve + * @param {Map} contracts - all contracts defined in the current context + * @return {Array} - array of base contracts in derived to base order as AST nodes. + */ +function getLinearizedBaseContracts (id, contractsById) { + return contractsById[id].attributes.linearizedBaseContracts.map(function (id) { return contractsById[id] }) +} + +/** + * return state var and type definition of the given contract + * + * @param {String} contractName - contract for which state var should be resolved + * @param {Object} sourcesList - sources list (containing root AST node) + * @param {Object} [contracts] - map of contract definitions (contains contractsById, contractsByName) + * @return {Object} - return an object containing: stateItems - list of all the children node of the @arg contractName + * stateVariables - list of all the variable declaration of the @arg contractName + */ +function extractStateDefinitions (contractName, sourcesList, contracts) { + if (!contracts) { + contracts = extractContractDefinitions(sourcesList) + } + var node = contracts.contractsByName[contractName] + if (node) { + var stateItems = [] + var stateVar = [] + var baseContracts = getLinearizedBaseContracts(node.id, contracts.contractsById) + baseContracts.reverse() + for (var k in baseContracts) { + var ctr = baseContracts[k] + for (var i in ctr.children) { + var item = ctr.children[i] + stateItems.push(item) + if (item.name === 'VariableDeclaration') { + stateVar.push(item) + } + } + } + return { + stateDefinitions: stateItems, + stateVariables: stateVar + } + } + return null +} + +/** + * return state var and type definition of all the contracts from the given @args sourcesList + * + * @param {Object} sourcesList - sources list (containing root AST node) + * @param {Object} [contracts] - map of contract definitions (contains contractsById, contractsByName) + * @return {Object} - returns a mapping between contract name and contract state + */ +function extractStatesDefinitions (sourcesList, contracts) { + if (!contracts) { + contracts = extractContractDefinitions(sourcesList) + } + var ret = {} + for (var contract in contracts.contractsById) { + var name = contracts.contractsById[contract].attributes.name + var source = contracts.sourcesByContract[contract] + var fullName = source + ':' + name + var state = extractStateDefinitions(fullName, sourcesList, contracts) + ret[fullName] = state + ret[name] = state // solc < 0.4.9 + } + return ret +} + +module.exports = { + extractStatesDefinitions: extractStatesDefinitions, + extractStateDefinitions: extractStateDefinitions, + extractContractDefinitions: extractContractDefinitions, + getLinearizedBaseContracts: getLinearizedBaseContracts +} diff --git a/remix-debug/src/decoder/decodeInfo.js b/remix-debug/src/decoder/decodeInfo.js new file mode 100644 index 0000000000..e65f46ec28 --- /dev/null +++ b/remix-debug/src/decoder/decodeInfo.js @@ -0,0 +1,384 @@ +'use strict' + +var AddressType = require('./types/Address') +var ArrayType = require('./types/ArrayType') +var BoolType = require('./types/Bool') +var BytesType = require('./types/DynamicByteArray') +var BytesXType = require('./types/FixedByteArray') +var EnumType = require('./types/Enum') +var StringType = require('./types/StringType') +var StructType = require('./types/Struct') +var IntType = require('./types/Int') +var UintType = require('./types/Uint') +var MappingType = require('./types/Mapping') +var util = require('./types/util') + +/** + * mapping decode the given @arg type + * + * @param {String} type - type given by the AST + * @return {Object} returns decoded info about the current type: { storageBytes, typeName} + */ +function mapping (type, stateDefinitions, contractName) { + var match = type.match(/mapping\((.*?)=>(.*)\)$/) + var keyTypeName = match[1].trim() + var valueTypeName = match[2].trim() + + var keyType = parseType(keyTypeName, stateDefinitions, contractName, 'storage') + var valueType = parseType(valueTypeName, stateDefinitions, contractName, 'storage') + + var underlyingTypes = { + 'keyType': keyType, + 'valueType': valueType + } + return new MappingType(underlyingTypes, 'location', util.removeLocation(type)) +} + +/** + * Uint decode the given @arg type + * + * @param {String} type - type given by the AST (e.g uint256, uint32) + * @return {Object} returns decoded info about the current type: { storageBytes, typeName} + */ +function uint (type) { + type === 'uint' ? 'uint256' : type + var storageBytes = parseInt(type.replace('uint', '')) / 8 + return new UintType(storageBytes) +} + +/** + * Int decode the given @arg type + * + * @param {String} type - type given by the AST (e.g int256, int32) + * @return {Object} returns decoded info about the current type: { storageBytes, typeName} + */ +function int (type) { + type === 'int' ? 'int256' : type + var storageBytes = parseInt(type.replace('int', '')) / 8 + return new IntType(storageBytes) +} + +/** + * Address decode the given @arg type + * + * @param {String} type - type given by the AST (e.g address) + * @return {Object} returns decoded info about the current type: { storageBytes, typeName} + */ +function address (type) { + return new AddressType() +} + +/** + * Bool decode the given @arg type + * + * @param {String} type - type given by the AST (e.g bool) + * @return {Object} returns decoded info about the current type: { storageBytes, typeName} + */ +function bool (type) { + return new BoolType() +} + +/** + * DynamicByteArray decode the given @arg type + * + * @param {String} type - type given by the AST (e.g bytes storage ref) + * @param {null} stateDefinitions - all state definitions given by the AST (including struct and enum type declaration) for all contracts + * @param {null} contractName - contract the @args typeName belongs to + * @param {String} location - location of the data (storage ref| storage pointer| memory| calldata) + * @return {Object} returns decoded info about the current type: { storageBytes, typeName} + */ +function dynamicByteArray (type, stateDefinitions, contractName, location) { + if (!location) { + location = util.extractLocation(type) + } + if (location) { + return new BytesType(location) + } else { + return null + } +} + +/** + * FixedByteArray decode the given @arg type + * + * @param {String} type - type given by the AST (e.g bytes16) + * @return {Object} returns decoded info about the current type: { storageBytes, typeName} + */ +function fixedByteArray (type) { + var storageBytes = parseInt(type.replace('bytes', '')) + return new BytesXType(storageBytes) +} + +/** + * StringType decode the given @arg type + * + * @param {String} type - type given by the AST (e.g string storage ref) + * @param {null} stateDefinitions - all state definitions given by the AST (including struct and enum type declaration) for all contracts + * @param {null} contractName - contract the @args typeName belongs to + * @param {String} location - location of the data (storage ref| storage pointer| memory| calldata) + * @return {Object} returns decoded info about the current type: { storageBytes, typeName} + */ +function stringType (type, stateDefinitions, contractName, location) { + if (!location) { + location = util.extractLocation(type) + } + if (location) { + return new StringType(location) + } else { + return null + } +} + +/** + * ArrayType decode the given @arg type + * + * @param {String} type - type given by the AST (e.g int256[] storage ref, int256[] storage ref[] storage ref) + * @param {Object} stateDefinitions - all state definitions given by the AST (including struct and enum type declaration) for all contracts + * @param {String} contractName - contract the @args typeName belongs to + * @param {String} location - location of the data (storage ref| storage pointer| memory| calldata) + * @return {Object} returns decoded info about the current type: { storageBytes, typeName, arraySize, subArray} + */ +function array (type, stateDefinitions, contractName, location) { + var arraySize + var match = type.match(/(.*)\[(.*?)\]( storage ref| storage pointer| memory| calldata)?$/) + if (!match) { + console.log('unable to parse type ' + type) + return null + } + if (!location) { + location = match[3].trim() + } + arraySize = match[2] === '' ? 'dynamic' : parseInt(match[2]) + var underlyingType = parseType(match[1], stateDefinitions, contractName, location) + if (underlyingType === null) { + console.log('unable to parse type ' + type) + return null + } + return new ArrayType(underlyingType, arraySize, location) +} + +/** + * Enum decode the given @arg type + * + * @param {String} type - type given by the AST (e.g enum enumDef) + * @param {Object} stateDefinitions - all state definitions given by the AST (including struct and enum type declaration) for all contracts + * @param {String} contractName - contract the @args typeName belongs to + * @return {Object} returns decoded info about the current type: { storageBytes, typeName, enum} + */ +function enumType (type, stateDefinitions, contractName) { + var match = type.match(/enum (.*)/) + var enumDef = getEnum(match[1], stateDefinitions, contractName) + if (enumDef === null) { + console.log('unable to retrieve decode info of ' + type) + return null + } + return new EnumType(enumDef) +} + +/** + * Struct decode the given @arg type + * + * @param {String} type - type given by the AST (e.g struct structDef storage ref) + * @param {Object} stateDefinitions - all state definitions given by the AST (including struct and enum type declaration) for all contracts + * @param {String} contractName - contract the @args typeName belongs to + * @param {String} location - location of the data (storage ref| storage pointer| memory| calldata) + * @return {Object} returns decoded info about the current type: { storageBytes, typeName, members} + */ +function struct (type, stateDefinitions, contractName, location) { + var match = type.match(/struct (\S*?)( storage ref| storage pointer| memory| calldata)?$/) + if (match) { + if (!location) { + location = match[2].trim() + } + var memberDetails = getStructMembers(match[1], stateDefinitions, contractName, location) // type is used to extract the ast struct definition + if (!memberDetails) return null + return new StructType(memberDetails, location, match[1]) + } else { + return null + } +} + +/** + * retrieve enum declaration of the given @arg type + * + * @param {String} type - type given by the AST (e.g enum enumDef) + * @param {Object} stateDefinitions - all state declarations given by the AST (including struct and enum type declaration) for all contracts + * @param {String} contractName - contract the @args typeName belongs to + * @return {Array} - containing all value declaration of the current enum type + */ +function getEnum (type, stateDefinitions, contractName) { + var split = type.split('.') + if (!split.length) { + type = contractName + '.' + type + } else { + contractName = split[0] + } + var state = stateDefinitions[contractName] + if (state) { + for (var dec of state.stateDefinitions) { + if (dec.attributes && dec.attributes.name && type === contractName + '.' + dec.attributes.name) { + return dec + } + } + } + return null +} + +/** + * retrieve memebers declared in the given @arg tye + * + * @param {String} typeName - name of the struct type (e.g struct ) + * @param {Object} stateDefinitions - all state definition given by the AST (including struct and enum type declaration) for all contracts + * @param {String} contractName - contract the @args typeName belongs to + * @param {String} location - location of the data (storage ref| storage pointer| memory| calldata) + * @return {Array} containing all members of the current struct type + */ +function getStructMembers (type, stateDefinitions, contractName, location) { + var split = type.split('.') + if (!split.length) { + type = contractName + '.' + type + } else { + contractName = split[0] + } + var state = stateDefinitions[contractName] + if (state) { + for (var dec of state.stateDefinitions) { + if (dec.name === 'StructDefinition' && type === contractName + '.' + dec.attributes.name) { + var offsets = computeOffsets(dec.children, stateDefinitions, contractName, location) + if (!offsets) { + return null + } + return { + members: offsets.typesOffsets, + storageSlots: offsets.endLocation.slot + } + } + } + } + return null +} + +/** + * parse the full type + * + * @param {String} fullType - type given by the AST (ex: uint[2] storage ref[2]) + * @return {String} returns the token type (used to instanciate the right decoder) (uint[2] storage ref[2] will return 'array', uint256 will return uintX) + */ +function typeClass (fullType) { + fullType = util.removeLocation(fullType) + if (fullType.lastIndexOf(']') === fullType.length - 1) { + return 'array' + } + if (fullType.indexOf('mapping') === 0) { + return 'mapping' + } + if (fullType.indexOf(' ') !== -1) { + fullType = fullType.split(' ')[0] + } + var char = fullType.indexOf('bytes') === 0 ? 'X' : '' + return fullType.replace(/[0-9]+/g, char) +} + +/** + * parse the type and return an object representing the type + * + * @param {Object} type - type name given by the ast node + * @param {Object} stateDefinitions - all state stateDefinitions given by the AST (including struct and enum type declaration) for all contracts + * @param {String} contractName - contract the @args typeName belongs to + * @param {String} location - location of the data (storage ref| storage pointer| memory| calldata) + * @return {Object} - return the corresponding decoder or null on error + */ +function parseType (type, stateDefinitions, contractName, location) { + var decodeInfos = { + 'contract': address, + 'address': address, + 'array': array, + 'bool': bool, + 'bytes': dynamicByteArray, + 'bytesX': fixedByteArray, + 'enum': enumType, + 'string': stringType, + 'struct': struct, + 'int': int, + 'uint': uint, + 'mapping': mapping + } + var currentType = typeClass(type) + if (currentType === null) { + console.log('unable to retrieve decode info of ' + type) + return null + } + if (decodeInfos[currentType]) { + return decodeInfos[currentType](type, stateDefinitions, contractName, location) + } else { + return null + } +} + +/** + * compute offset (slot offset and byte offset of the @arg list of types) + * + * @param {Array} types - list of types + * @param {Object} stateDefinitions - all state definitions given by the AST (including struct and enum type declaration) for all contracts + * @param {String} contractName - contract the @args typeName belongs to + * @param {String} location - location of the data (storage ref| storage pointer| memory| calldata) + * @return {Array} - return an array of types item: {name, type, location}. location defines the byte offset and slot offset + */ +function computeOffsets (types, stateDefinitions, contractName, location) { + var ret = [] + var storagelocation = { + offset: 0, + slot: 0 + } + for (var i in types) { + var variable = types[i] + var type = parseType(variable.attributes.type, stateDefinitions, contractName, location) + if (!type) { + console.log('unable to retrieve decode info of ' + variable.attributes.type) + return null + } + if (!variable.attributes.constant && storagelocation.offset + type.storageBytes > 32) { + storagelocation.slot++ + storagelocation.offset = 0 + } + ret.push({ + name: variable.attributes.name, + type: type, + constant: variable.attributes.constant, + storagelocation: { + offset: variable.attributes.constant ? 0 : storagelocation.offset, + slot: variable.attributes.constant ? 0 : storagelocation.slot + } + }) + if (!variable.attributes.constant) { + if (type.storageSlots === 1 && storagelocation.offset + type.storageBytes <= 32) { + storagelocation.offset += type.storageBytes + } else { + storagelocation.slot += type.storageSlots + storagelocation.offset = 0 + } + } + } + if (storagelocation.offset > 0) { + storagelocation.slot++ + } + return { + typesOffsets: ret, + endLocation: storagelocation + } +} + +module.exports = { + parseType: parseType, + computeOffsets: computeOffsets, + Uint: uint, + Address: address, + Bool: bool, + DynamicByteArray: dynamicByteArray, + FixedByteArray: fixedByteArray, + Int: int, + String: stringType, + Array: array, + Enum: enumType, + Struct: struct +} diff --git a/remix-debug/src/decoder/internalCallTree.js b/remix-debug/src/decoder/internalCallTree.js new file mode 100644 index 0000000000..1f2559c0d8 --- /dev/null +++ b/remix-debug/src/decoder/internalCallTree.js @@ -0,0 +1,293 @@ +'use strict' +var remixLib = require('remix-lib') +var SourceLocationTracker = remixLib.SourceLocationTracker +var AstWalker = remixLib.AstWalker +var EventManager = remixLib.EventManager +var decodeInfo = require('./decodeInfo') +var util = remixLib.util +var traceHelper = remixLib.helpers.trace +var typesUtil = require('./types/util.js') + +/** + * Tree representing internal jump into function. + * Triggers `callTreeReady` event when tree is ready + * Triggers `callTreeBuildFailed` event when tree fails to build + */ +class InternalCallTree { + /** + * constructor + * + * @param {Object} debuggerEvent - event declared by the debugger (EthDebugger) + * @param {Object} traceManager - trace manager + * @param {Object} solidityProxy - solidity proxy + * @param {Object} codeManager - code manager + * @param {Object} opts - { includeLocalVariables } + */ + constructor (debuggerEvent, traceManager, solidityProxy, codeManager, opts) { + this.includeLocalVariables = opts.includeLocalVariables + this.event = new EventManager() + this.solidityProxy = solidityProxy + this.traceManager = traceManager + this.sourceLocationTracker = new SourceLocationTracker(codeManager) + debuggerEvent.register('newTraceLoaded', (trace) => { + this.reset() + if (!this.solidityProxy.loaded()) { + this.event.trigger('callTreeBuildFailed', ['compilation result not loaded. Cannot build internal call tree']) + } else { + buildTree(this, 0, '', true).then((result) => { + if (result.error) { + this.event.trigger('callTreeBuildFailed', [result.error]) + } else { + console.log('ready') + createReducedTrace(this, traceManager.trace.length - 1) + this.event.trigger('callTreeReady', [this.scopes, this.scopeStarts]) + } + }, (reason) => { + console.log('analyzing trace falls ' + reason) + this.event.trigger('callTreeNotReady', [reason]) + }) + } + }) + } + + /** + * reset tree + * + */ + reset () { + /* + scopes: map of scopes defined by range in the vmtrace {firstStep, lastStep, locals}. Keys represent the level of deepness (scopeId) + */ + this.scopes = {} + /* + scopeStart: represent start of a new scope. Keys are index in the vmtrace, values are scopeId + */ + this.sourceLocationTracker.clearCache() + this.functionCallStack = [] + this.scopeStarts = {} + this.variableDeclarationByFile = {} + this.functionDefinitionByFile = {} + this.astWalker = new AstWalker() + this.reducedTrace = [] + } + + /** + * find the scope given @arg vmTraceIndex + * + * @param {Int} vmtraceIndex - index on the vm trace + */ + findScope (vmtraceIndex) { + var scopes = Object.keys(this.scopeStarts) + if (!scopes.length) { + return null + } + var scopeId = util.findLowerBoundValue(vmtraceIndex, scopes) + scopeId = this.scopeStarts[scopeId] + var scope = this.scopes[scopeId] + while (scope.lastStep && scope.lastStep < vmtraceIndex && scope.firstStep > 0) { + var matched = scopeId.match(/(.\d|\d)$/) + scopeId = scopeId.replace(matched[1], '') + scope = this.scopes[scopeId] + } + return scope + } + + extractSourceLocation (step) { + var self = this + return new Promise(function (resolve, reject) { + self.traceManager.getCurrentCalledAddressAt(step, (error, address) => { + if (!error) { + self.sourceLocationTracker.getSourceLocationFromVMTraceIndex(address, step, self.solidityProxy.contracts, (error, sourceLocation) => { + if (!error) { + return resolve(sourceLocation) + } else { + return reject('InternalCallTree - Cannot retrieve sourcelocation for step ' + step + ' ' + error) + } + }) + } else { + return reject('InternalCallTree - Cannot retrieve address for step ' + step + ' ' + error) + } + }) + }) + } +} + +async function buildTree (tree, step, scopeId, isExternalCall) { + let subScope = 1 + tree.scopeStarts[step] = scopeId + tree.scopes[scopeId] = { firstStep: step, locals: {} } + + function callDepthChange (step, trace) { + if (step + 1 < trace.length) { + return trace[step].depth !== trace[step + 1].depth + } + return false + } + + function includedSource (source, included) { + return (included.start !== -1 && + included.length !== -1 && + included.file !== -1 && + included.start >= source.start && + included.start + included.length <= source.start + source.length && + included.file === source.file) + } + + var currentSourceLocation = {start: -1, length: -1, file: -1} + var previousSourceLocation = currentSourceLocation + while (step < tree.traceManager.trace.length) { + var sourceLocation + var newLocation = false + try { + sourceLocation = await tree.extractSourceLocation(step) + if (!includedSource(sourceLocation, currentSourceLocation)) { + tree.reducedTrace.push(step) + currentSourceLocation = sourceLocation + newLocation = true + } + } catch (e) { + return { outStep: step, error: 'InternalCallTree - Error resolving source location. ' + step + ' ' + e } + } + if (!sourceLocation) { + return { outStep: step, error: 'InternalCallTree - No source Location. ' + step } + } + var isCallInstruction = traceHelper.isCallInstruction(tree.traceManager.trace[step]) + if (isCallInstruction || sourceLocation.jump === 'i') { + try { + var externalCallResult = await buildTree(tree, step + 1, scopeId === '' ? subScope.toString() : scopeId + '.' + subScope, isCallInstruction) + if (externalCallResult.error) { + return { outStep: step, error: 'InternalCallTree - ' + externalCallResult.error } + } else { + step = externalCallResult.outStep + subScope++ + } + } catch (e) { + return { outStep: step, error: 'InternalCallTree - ' + e.message } + } + } else if ((isExternalCall && callDepthChange(step, tree.traceManager.trace)) || (!isExternalCall && sourceLocation.jump === 'o')) { + tree.scopes[scopeId].lastStep = step + return { outStep: step + 1 } + } else { + if (tree.includeLocalVariables) { + includeVariableDeclaration(tree, step, sourceLocation, scopeId, newLocation, previousSourceLocation) + } + previousSourceLocation = sourceLocation + step++ + } + } + return { outStep: step } +} + +function createReducedTrace (tree, index) { + tree.reducedTrace.push(index) +} + +function includeVariableDeclaration (tree, step, sourceLocation, scopeId, newLocation, previousSourceLocation) { + var variableDeclaration = resolveVariableDeclaration(tree, step, sourceLocation) + if (variableDeclaration && !tree.scopes[scopeId].locals[variableDeclaration.attributes.name]) { + tree.traceManager.getStackAt(step, (error, stack) => { + if (!error) { + tree.solidityProxy.contractNameAt(step, (error, contractName) => { // cached + if (!error) { + var states = tree.solidityProxy.extractStatesDefinitions() + var location = typesUtil.extractLocationFromAstVariable(variableDeclaration) + location = location === 'default' ? 'storage' : location + tree.scopes[scopeId].locals[variableDeclaration.attributes.name] = { + name: variableDeclaration.attributes.name, + type: decodeInfo.parseType(variableDeclaration.attributes.type, states, contractName, location), + stackDepth: stack.length, + sourceLocation: sourceLocation + } + } + }) + } + }) + } + var functionDefinition = resolveFunctionDefinition(tree, step, previousSourceLocation) + if (functionDefinition && (newLocation && traceHelper.isJumpDestInstruction(tree.traceManager.trace[step - 1]) || functionDefinition.attributes.isConstructor)) { + tree.functionCallStack.push(step) + // means: the previous location was a function definition && JUMPDEST + // => we are at the beginning of the function and input/output are setup + tree.solidityProxy.contractNameAt(step, (error, contractName) => { // cached + if (!error) { + tree.traceManager.getStackAt(step, (error, stack) => { + if (!error) { + var states = tree.solidityProxy.extractStatesDefinitions() + // input params + addParams(functionDefinition.children[0], tree, scopeId, states, contractName, previousSourceLocation, stack.length, functionDefinition.children[0].children.length, -1) + // output params + addParams(functionDefinition.children[1], tree, scopeId, states, contractName, previousSourceLocation, stack.length, 0, 1) + } + }) + } + }) + } +} + +function resolveVariableDeclaration (tree, step, sourceLocation) { + if (!tree.variableDeclarationByFile[sourceLocation.file]) { + var ast = tree.solidityProxy.ast(sourceLocation) + if (ast) { + tree.variableDeclarationByFile[sourceLocation.file] = extractVariableDeclarations(ast, tree.astWalker) + } else { + console.log('Ast not found for step ' + step + '. file ' + sourceLocation.file) + return null + } + } + return tree.variableDeclarationByFile[sourceLocation.file][sourceLocation.start + ':' + sourceLocation.length + ':' + sourceLocation.file] +} + +function resolveFunctionDefinition (tree, step, sourceLocation) { + if (!tree.functionDefinitionByFile[sourceLocation.file]) { + var ast = tree.solidityProxy.ast(sourceLocation) + if (ast) { + tree.functionDefinitionByFile[sourceLocation.file] = extractFunctionDefinitions(ast, tree.astWalker) + } else { + console.log('Ast not found for step ' + step + '. file ' + sourceLocation.file) + return null + } + } + return tree.functionDefinitionByFile[sourceLocation.file][sourceLocation.start + ':' + sourceLocation.length + ':' + sourceLocation.file] +} + +function extractVariableDeclarations (ast, astWalker) { + var ret = {} + astWalker.walk(ast, (node) => { + if (node.name === 'VariableDeclaration') { + ret[node.src] = node + } + return true + }) + return ret +} + +function extractFunctionDefinitions (ast, astWalker) { + var ret = {} + astWalker.walk(ast, (node) => { + if (node.name === 'FunctionDefinition') { + ret[node.src] = node + } + return true + }) + return ret +} + +function addParams (parameterList, tree, scopeId, states, contractName, sourceLocation, stackLength, stackPosition, dir) { + for (var inputParam in parameterList.children) { + var param = parameterList.children[inputParam] + var stackDepth = stackLength + (dir * stackPosition) + if (stackDepth >= 0) { + var location = typesUtil.extractLocationFromAstVariable(param) + location = location === 'default' ? 'memory' : location + tree.scopes[scopeId].locals[param.attributes.name] = { + name: param.attributes.name, + type: decodeInfo.parseType(param.attributes.type, states, contractName, location), + stackDepth: stackDepth, + sourceLocation: sourceLocation + } + } + stackPosition += dir + } +} + +module.exports = InternalCallTree diff --git a/remix-debug/src/decoder/localDecoder.js b/remix-debug/src/decoder/localDecoder.js new file mode 100644 index 0000000000..650f5bcad3 --- /dev/null +++ b/remix-debug/src/decoder/localDecoder.js @@ -0,0 +1,40 @@ +'use strict' + +async function solidityLocals (vmtraceIndex, internalTreeCall, stack, memory, storageResolver, currentSourceLocation) { + var scope = internalTreeCall.findScope(vmtraceIndex) + if (!scope) { + var error = { 'message': 'Can\'t display locals. reason: compilation result might not have been provided' } + throw error + } + var locals = {} + memory = formatMemory(memory) + var anonymousIncr = 1 + for (var local in scope.locals) { + var variable = scope.locals[local] + if (variable.stackDepth < stack.length && variable.sourceLocation.start <= currentSourceLocation.start) { + var name = variable.name + if (name === '') { + name = '<' + anonymousIncr + '>' + anonymousIncr++ + } + try { + locals[name] = await variable.type.decodeFromStack(variable.stackDepth, stack, memory, storageResolver) + } catch (e) { + console.log(e) + locals[name] = '' + } + } + } + return locals +} + +function formatMemory (memory) { + if (memory instanceof Array) { + memory = memory.join('').replace(/0x/g, '') + } + return memory +} + +module.exports = { + solidityLocals: solidityLocals +} diff --git a/remix-debug/src/decoder/solidityProxy.js b/remix-debug/src/decoder/solidityProxy.js new file mode 100644 index 0000000000..63a42670e3 --- /dev/null +++ b/remix-debug/src/decoder/solidityProxy.js @@ -0,0 +1,161 @@ +'use strict' +var remixLib = require('remix-lib') +var traceHelper = remixLib.helpers.trace +var stateDecoder = require('./stateDecoder') +var astHelper = require('./astHelper') +var util = remixLib.util + +class SolidityProxy { + constructor (traceManager, codeManager) { + this.cache = new Cache() + this.reset({}) + this.traceManager = traceManager + this.codeManager = codeManager + } + + /** + * reset the cache and apply a new @arg compilationResult + * + * @param {Object} compilationResult - result os a compilatiion (diectly returned by the compiler) + */ + reset (compilationResult) { + this.sources = compilationResult.sources + this.contracts = compilationResult.contracts + this.cache.reset() + } + + /** + * check if the object has been properly loaded + * + * @return {Bool} - returns true if a compilation result has been applied + */ + loaded () { + return this.contracts !== undefined + } + + /** + * retrieve the compiled contract name at the @arg vmTraceIndex (cached) + * + * @param {Int} vmTraceIndex - index in the vm trave where to resolve the executed contract name + * @param {Function} cb - callback returns (error, contractName) + */ + contractNameAt (vmTraceIndex, cb) { + this.traceManager.getCurrentCalledAddressAt(vmTraceIndex, (error, address) => { + if (error) { + cb(error) + } else { + if (this.cache.contractNameByAddress[address]) { + cb(null, this.cache.contractNameByAddress[address]) + } else { + this.codeManager.getCode(address, (error, code) => { + if (error) { + cb(error) + } else { + var contractName = contractNameFromCode(this.contracts, code.bytecode, address) + this.cache.contractNameByAddress[address] = contractName + cb(null, contractName) + } + }) + } + } + }) + } + + /** + * extract the state variables of the given compiled @arg contractName (cached) + * + * @param {String} contractName - name of the contract to retrieve state variables from + * @return {Object} - returns state variables of @args contractName + */ + extractStatesDefinitions () { + if (!this.cache.contractDeclarations) { + this.cache.contractDeclarations = astHelper.extractContractDefinitions(this.sources) + } + if (!this.cache.statesDefinitions) { + this.cache.statesDefinitions = astHelper.extractStatesDefinitions(this.sources, this.cache.contractDeclarations) + } + return this.cache.statesDefinitions + } + + /** + * extract the state variables of the given compiled @arg contractName (cached) + * + * @param {String} contractName - name of the contract to retrieve state variables from + * @return {Object} - returns state variables of @args contractName + */ + extractStateVariables (contractName) { + if (!this.cache.stateVariablesByContractName[contractName]) { + this.cache.stateVariablesByContractName[contractName] = stateDecoder.extractStateVariables(contractName, this.sources) + } + return this.cache.stateVariablesByContractName[contractName] + } + + /** + * extract the state variables of the given compiled @arg vmtraceIndex (cached) + * + * @param {Int} vmTraceIndex - index in the vm trave where to resolve the state variables + * @return {Object} - returns state variables of @args vmTraceIndex + */ + extractStateVariablesAt (vmtraceIndex, cb) { + this.contractNameAt(vmtraceIndex, (error, contractName) => { + if (error) { + cb(error) + } else { + cb(null, this.extractStateVariables(contractName)) + } + }) + } + + /** + * get the AST of the file declare in the @arg sourceLocation + * + * @param {Object} sourceLocation - source location containing the 'file' to retrieve the AST from + * @return {Object} - AST of the current file + */ + ast (sourceLocation) { + var file = this.fileNameFromIndex(sourceLocation.file) + if (this.sources[file]) { + return this.sources[file].legacyAST + } else { + console.log('AST not found for file id ' + sourceLocation.file) + return null + } + } + + /** + * get the filename refering to the index from the compilation result + * + * @param {Int} index - index of the filename + * @return {String} - filename + */ + fileNameFromIndex (index) { + return Object.keys(this.contracts)[index] + } +} + +function contractNameFromCode (contracts, code, address) { + var isCreation = traceHelper.isContractCreation(address) + for (var file in contracts) { + for (var contract in contracts[file]) { + var bytecode = isCreation ? contracts[file][contract].evm.bytecode.object : contracts[file][contract].evm.deployedBytecode.object + if (util.compareByteCode(code, '0x' + bytecode)) { + return contract + } + } + } + return null +} + +class Cache { + constructor () { + this.reset() + } + reset () { + this.contractNameByAddress = {} + this.stateVariablesByContractName = {} + this.contractDeclarations = null + this.statesDefinitions = null + } +} + +module.exports = SolidityProxy diff --git a/remix-debug/src/decoder/stateDecoder.js b/remix-debug/src/decoder/stateDecoder.js new file mode 100644 index 0000000000..43baa32225 --- /dev/null +++ b/remix-debug/src/decoder/stateDecoder.js @@ -0,0 +1,71 @@ +var astHelper = require('./astHelper') +var decodeInfo = require('./decodeInfo') + +/** + * decode the contract state storage + * + * @param {Array} storage location - location of all state variables + * @param {Object} storageResolver - resolve storage queries + * @return {Map} - decoded state variable + */ +async function decodeState (stateVars, storageResolver) { + var ret = {} + for (var k in stateVars) { + var stateVar = stateVars[k] + try { + var decoded = await stateVar.type.decodeFromStorage(stateVar.storagelocation, storageResolver) + decoded.constant = stateVar.constant + if (decoded.constant) { + decoded.value = '' + } + ret[stateVar.name] = decoded + } catch (e) { + console.log(e) + ret[stateVar.name] = '' + } + } + return ret +} + +/** + * return all storage location variables of the given @arg contractName + * + * @param {String} contractName - name of the contract + * @param {Object} sourcesList - sources list + * @return {Object} - return the location of all contract variables in the storage + */ +function extractStateVariables (contractName, sourcesList) { + var states = astHelper.extractStatesDefinitions(sourcesList) + if (!states[contractName]) { + return [] + } + var types = states[contractName].stateVariables + var offsets = decodeInfo.computeOffsets(types, states, contractName, 'storage') + if (!offsets) { + return [] // TODO should maybe return an error + } + return offsets.typesOffsets +} + +/** + * return the state of the given @a contractName as a json object + * + * @param {Object} storageResolver - resolve storage queries + * @param {astList} astList - AST nodes of all the sources + * @param {String} contractName - contract for which state var should be resolved + * @return {Map} - return the state of the contract + */ +async function solidityState (storageResolver, astList, contractName) { + var stateVars = extractStateVariables(contractName, astList) + try { + return await decodeState(stateVars, storageResolver) + } catch (e) { + return '' + } +} + +module.exports = { + solidityState: solidityState, + extractStateVariables: extractStateVariables, + decodeState: decodeState +} diff --git a/remix-debug/src/decoder/types/Address.js b/remix-debug/src/decoder/types/Address.js new file mode 100644 index 0000000000..9369fd2258 --- /dev/null +++ b/remix-debug/src/decoder/types/Address.js @@ -0,0 +1,19 @@ +'use strict' +var util = require('./util') +var ValueType = require('./ValueType') + +class Address extends ValueType { + constructor () { + super(1, 20, 'address') + } + + decodeValue (value) { + if (!value) { + return '0x0000000000000000000000000000000000000000' + } else { + return '0x' + util.extractHexByteSlice(value, this.storageBytes, 0).toUpperCase() + } + } +} + +module.exports = Address diff --git a/remix-debug/src/decoder/types/ArrayType.js b/remix-debug/src/decoder/types/ArrayType.js new file mode 100644 index 0000000000..d8edede107 --- /dev/null +++ b/remix-debug/src/decoder/types/ArrayType.js @@ -0,0 +1,100 @@ +'use strict' +var util = require('./util') +var remixLib = require('remix-lib') +var sha3256 = remixLib.util.sha3_256 +var BN = require('ethereumjs-util').BN +var RefType = require('./RefType') + +class ArrayType extends RefType { + + constructor (underlyingType, arraySize, location) { + var storageSlots = null + if (arraySize === 'dynamic') { + storageSlots = 1 + } else { + if (underlyingType.storageBytes < 32) { + var itemPerSlot = Math.floor(32 / underlyingType.storageBytes) + storageSlots = Math.ceil(arraySize / itemPerSlot) + } else { + storageSlots = arraySize * underlyingType.storageSlots + } + } + var size = arraySize !== 'dynamic' ? arraySize : '' + super(storageSlots, 32, underlyingType.typeName + '[' + size + ']', location) + this.underlyingType = underlyingType + this.arraySize = arraySize + } + + async decodeFromStorage (location, storageResolver) { + var ret = [] + var size = null + var slotValue + try { + slotValue = await util.extractHexValue(location, storageResolver, this.storageBytes) + } catch (e) { + console.log(e) + return { + value: '', + type: this.typeName + } + } + var currentLocation = { + offset: 0, + slot: location.slot + } + if (this.arraySize === 'dynamic') { + size = util.toBN('0x' + slotValue) + currentLocation.slot = sha3256(location.slot) + } else { + size = new BN(this.arraySize) + } + var k = util.toBN(0) + for (; k.lt(size) && k.ltn(300); k.iaddn(1)) { + try { + ret.push(await this.underlyingType.decodeFromStorage(currentLocation, storageResolver)) + } catch (e) { + return { + value: '', + type: this.typeName + } + } + if (this.underlyingType.storageSlots === 1 && location.offset + this.underlyingType.storageBytes <= 32) { + currentLocation.offset += this.underlyingType.storageBytes + if (currentLocation.offset + this.underlyingType.storageBytes > 32) { + currentLocation.offset = 0 + currentLocation.slot = '0x' + util.add(currentLocation.slot, 1).toString(16) + } + } else { + currentLocation.slot = '0x' + util.add(currentLocation.slot, this.underlyingType.storageSlots).toString(16) + currentLocation.offset = 0 + } + } + return { + value: ret, + length: '0x' + size.toString(16), + type: this.typeName + } + } + + decodeFromMemoryInternal (offset, memory) { + var ret = [] + var length = this.arraySize + if (this.arraySize === 'dynamic') { + length = memory.substr(2 * offset, 64) + length = parseInt(length, 16) + offset = offset + 32 + } + for (var k = 0; k < length; k++) { + var contentOffset = offset + ret.push(this.underlyingType.decodeFromMemory(contentOffset, memory)) + offset += 32 + } + return { + value: ret, + length: '0x' + length.toString(16), + type: this.typeName + } + } +} + +module.exports = ArrayType diff --git a/remix-debug/src/decoder/types/Bool.js b/remix-debug/src/decoder/types/Bool.js new file mode 100644 index 0000000000..32b63d061e --- /dev/null +++ b/remix-debug/src/decoder/types/Bool.js @@ -0,0 +1,20 @@ +'use strict' +var ValueType = require('./ValueType') +var util = require('./util') + +class Bool extends ValueType { + constructor () { + super(1, 1, 'bool') + } + + decodeValue (value) { + if (!value) { + return false + } else { + value = util.extractHexByteSlice(value, this.storageBytes, 0) + return value !== '00' + } + } +} + +module.exports = Bool diff --git a/remix-debug/src/decoder/types/DynamicByteArray.js b/remix-debug/src/decoder/types/DynamicByteArray.js new file mode 100644 index 0000000000..d879b5ef6a --- /dev/null +++ b/remix-debug/src/decoder/types/DynamicByteArray.js @@ -0,0 +1,80 @@ +'use strict' +var util = require('./util') +var remixLib = require('remix-lib') +var sha3256 = remixLib.util.sha3_256 +var BN = require('ethereumjs-util').BN +var RefType = require('./RefType') + +class DynamicByteArray extends RefType { + constructor (location) { + super(1, 32, 'bytes', location) + } + + async decodeFromStorage (location, storageResolver) { + var value = '0x0' + try { + value = await util.extractHexValue(location, storageResolver, this.storageBytes) + } catch (e) { + console.log(e) + return { + value: '', + type: this.typeName + } + } + var bn = new BN(value, 16) + if (bn.testn(0)) { + var length = bn.div(new BN(2)) + var dataPos = new BN(sha3256(location.slot).replace('0x', ''), 16) + var ret = '' + var currentSlot = '0x' + try { + currentSlot = await util.readFromStorage(dataPos, storageResolver) + } catch (e) { + console.log(e) + return { + value: '', + type: this.typeName + } + } + while (length.gt(ret.length) && ret.length < 32000) { + currentSlot = currentSlot.replace('0x', '') + ret += currentSlot + dataPos = dataPos.add(new BN(1)) + try { + currentSlot = await util.readFromStorage(dataPos, storageResolver) + } catch (e) { + console.log(e) + return { + value: '', + type: this.typeName + } + } + } + return { + value: '0x' + ret.replace(/(00)+$/, ''), + length: '0x' + length.toString(16), + type: this.typeName + } + } else { + var size = parseInt(value.substr(value.length - 2, 2), 16) / 2 + return { + value: '0x' + value.substr(0, size * 2), + length: '0x' + size.toString(16), + type: this.typeName + } + } + } + + decodeFromMemoryInternal (offset, memory) { + offset = 2 * offset + var length = memory.substr(offset, 64) + length = 2 * parseInt(length, 16) + return { + length: '0x' + length.toString(16), + value: '0x' + memory.substr(offset + 64, length), + type: this.typeName + } + } +} + +module.exports = DynamicByteArray diff --git a/remix-debug/src/decoder/types/Enum.js b/remix-debug/src/decoder/types/Enum.js new file mode 100644 index 0000000000..a170b61c16 --- /dev/null +++ b/remix-debug/src/decoder/types/Enum.js @@ -0,0 +1,30 @@ +'use strict' +var ValueType = require('./ValueType') + +class Enum extends ValueType { + constructor (enumDef) { + var storageBytes = 0 + var length = enumDef.children.length + while (length > 1) { + length = length / 256 + storageBytes++ + } + super(1, storageBytes, 'enum') + this.enumDef = enumDef + } + + decodeValue (value) { + if (!value) { + return this.enumDef.children[0].attributes.name + } else { + value = parseInt(value, 16) + if (this.enumDef.children.length > value) { + return this.enumDef.children[value].attributes.name + } else { + return 'INVALID_ENUM<' + value + '>' + } + } + } +} + +module.exports = Enum diff --git a/remix-debug/src/decoder/types/FixedByteArray.js b/remix-debug/src/decoder/types/FixedByteArray.js new file mode 100644 index 0000000000..3cfe1a7e4c --- /dev/null +++ b/remix-debug/src/decoder/types/FixedByteArray.js @@ -0,0 +1,14 @@ +'use strict' +var ValueType = require('./ValueType') + +class FixedByteArray extends ValueType { + constructor (storageBytes) { + super(1, storageBytes, 'bytes' + storageBytes) + } + + decodeValue (value) { + return '0x' + value.substr(0, 2 * this.storageBytes).toUpperCase() + } +} + +module.exports = FixedByteArray diff --git a/remix-debug/src/decoder/types/Int.js b/remix-debug/src/decoder/types/Int.js new file mode 100644 index 0000000000..6288899e75 --- /dev/null +++ b/remix-debug/src/decoder/types/Int.js @@ -0,0 +1,16 @@ +'use strict' +var util = require('./util') +var ValueType = require('./ValueType') + +class Int extends ValueType { + constructor (storageBytes) { + super(1, storageBytes, 'int' + storageBytes * 8) + } + + decodeValue (value) { + value = util.extractHexByteSlice(value, this.storageBytes, 0) + return util.decodeIntFromHex(value, this.storageBytes, true) + } +} + +module.exports = Int diff --git a/remix-debug/src/decoder/types/Mapping.js b/remix-debug/src/decoder/types/Mapping.js new file mode 100644 index 0000000000..d190bbb459 --- /dev/null +++ b/remix-debug/src/decoder/types/Mapping.js @@ -0,0 +1,87 @@ +'use strict' +var RefType = require('./RefType') +var util = require('./util') +var ethutil = require('ethereumjs-util') + +class Mapping extends RefType { + constructor (underlyingTypes, location, fullType) { + super(1, 32, fullType, 'storage') + this.keyType = underlyingTypes.keyType + this.valueType = underlyingTypes.valueType + this.initialDecodedState = null + } + + async decodeFromStorage (location, storageResolver) { + if (!this.initialDecodedState) { // cache the decoded initial storage + var mappingsInitialPreimages + try { + mappingsInitialPreimages = await storageResolver.initialMappingsLocation() + this.initialDecodedState = await this.decodeMappingsLocation(mappingsInitialPreimages, location, storageResolver) + } catch (e) { + return { + value: e.message, + type: this.typeName + } + } + } + var mappingPreimages = await storageResolver.mappingsLocation() + var ret = await this.decodeMappingsLocation(mappingPreimages, location, storageResolver) // fetch mapping storage changes + ret = Object.assign({}, this.initialDecodedState, ret) // merge changes + return { + value: ret, + type: this.typeName + } + } + + decodeFromMemoryInternal (offset, memory) { + // mappings can only exist in storage and not in memory + // so this should never be called + return { + value: '', + length: '0x', + type: this.typeName + } + } + + async decodeMappingsLocation (preimages, location, storageResolver) { + var mapSlot = util.normalizeHex(ethutil.bufferToHex(location.slot)) + if (!preimages[mapSlot]) { + return {} + } + var ret = {} + for (var i in preimages[mapSlot]) { + var mapLocation = getMappingLocation(i, location.slot) + var globalLocation = { + offset: location.offset, + slot: mapLocation + } + ret[i] = await this.valueType.decodeFromStorage(globalLocation, storageResolver) + console.log('global location', globalLocation, i, ret[i]) + } + return ret + } +} + +function getMappingLocation (key, position) { + // mapping storage location decribed at http://solidity.readthedocs.io/en/develop/miscellaneous.html#layout-of-state-variables-in-storage + // > the value corresponding to a mapping key k is located at keccak256(k . p) where . is concatenation. + + // key should be a hex string, and position an int + var mappingK = ethutil.toBuffer('0x' + key) + var mappingP = ethutil.intToBuffer(position) + mappingP = ethutil.setLengthLeft(mappingP, 32) + var mappingKeyBuf = concatTypedArrays(mappingK, mappingP) + var mappingKeyPreimage = '0x' + mappingKeyBuf.toString('hex') + var mappingStorageLocation = ethutil.sha3(mappingKeyPreimage) + mappingStorageLocation = new ethutil.BN(mappingStorageLocation, 16) + return mappingStorageLocation +} + +function concatTypedArrays (a, b) { // a, b TypedArray of same type + let c = new (a.constructor)(a.length + b.length) + c.set(a, 0) + c.set(b, a.length) + return c +} + +module.exports = Mapping diff --git a/remix-debug/src/decoder/types/RefType.js b/remix-debug/src/decoder/types/RefType.js new file mode 100644 index 0000000000..c33a3d18a1 --- /dev/null +++ b/remix-debug/src/decoder/types/RefType.js @@ -0,0 +1,84 @@ +'use strict' +var util = require('./util') + +class RefType { + constructor (storageSlots, storageBytes, typeName, location) { + this.location = location + this.storageSlots = storageSlots + this.storageBytes = storageBytes + this.typeName = typeName + this.basicType = 'RefType' + } + + /** + * decode the type from the stack + * + * @param {Int} stackDepth - position of the type in the stack + * @param {Array} stack - stack + * @param {String} - memory + * @param {Object} - storageResolver + * @return {Object} decoded value + */ + async decodeFromStack (stackDepth, stack, memory, storageResolver) { + if (stack.length - 1 < stackDepth) { + return { + error: '', + type: this.typeName + } + } + var offset = stack[stack.length - 1 - stackDepth] + if (this.isInStorage()) { + offset = util.toBN(offset) + try { + return await this.decodeFromStorage({ offset: 0, slot: offset }, storageResolver) + } catch (e) { + console.log(e) + return { + error: '', + type: this.typeName + } + } + } else if (this.isInMemory()) { + offset = parseInt(offset, 16) + return this.decodeFromMemoryInternal(offset, memory) + } else { + return { + error: '', + type: this.typeName + } + } + } + + /** + * decode the type from the memory + * + * @param {Int} offset - position of the ref of the type in memory + * @param {String} memory - memory + * @return {Object} decoded value + */ + decodeFromMemory (offset, memory) { + offset = memory.substr(2 * offset, 64) + offset = parseInt(offset, 16) + return this.decodeFromMemoryInternal(offset, memory) + } + + /** + * current type defined in storage + * + * @return {Bool} - return true if the type is defined in the storage + */ + isInStorage () { + return this.location.indexOf('storage') === 0 + } + + /** + * current type defined in memory + * + * @return {Bool} - return true if the type is defined in the memory + */ + isInMemory () { + return this.location.indexOf('memory') === 0 + } +} + +module.exports = RefType diff --git a/remix-debug/src/decoder/types/StringType.js b/remix-debug/src/decoder/types/StringType.js new file mode 100644 index 0000000000..f21eac4a51 --- /dev/null +++ b/remix-debug/src/decoder/types/StringType.js @@ -0,0 +1,56 @@ +'use strict' +var DynamicBytes = require('./DynamicByteArray') + +class StringType extends DynamicBytes { + constructor (location) { + super(location) + this.typeName = 'string' + } + + async decodeFromStorage (location, storageResolver) { + var decoded = '0x' + try { + decoded = await super.decodeFromStorage(location, storageResolver) + } catch (e) { + console.log(e) + return '' + } + return format(decoded) + } + + async decodeFromStack (stackDepth, stack, memory) { + try { + return await super.decodeFromStack(stackDepth, stack, memory) + } catch (e) { + console.log(e) + return '' + } + } + + decodeFromMemoryInternal (offset, memory) { + var decoded = super.decodeFromMemoryInternal(offset, memory) + return format(decoded) + } +} + +function format (decoded) { + if (decoded.error) { + return decoded + } + var value = decoded.value + value = value.replace('0x', '').replace(/(..)/g, '%$1') + var ret = { + length: decoded.length, + raw: decoded.value, + type: 'string' + } + try { + ret.value = decodeURIComponent(value) + } catch (e) { + ret.error = 'Invalid UTF8 encoding' + ret.raw = decoded.value + } + return ret +} + +module.exports = StringType diff --git a/remix-debug/src/decoder/types/Struct.js b/remix-debug/src/decoder/types/Struct.js new file mode 100644 index 0000000000..d715caf049 --- /dev/null +++ b/remix-debug/src/decoder/types/Struct.js @@ -0,0 +1,47 @@ +'use strict' +var util = require('./util') +var RefType = require('./RefType') + +class Struct extends RefType { + constructor (memberDetails, location, fullType) { + super(memberDetails.storageSlots, 32, 'struct ' + fullType, location) + this.members = memberDetails.members + } + + async decodeFromStorage (location, storageResolver) { + var ret = {} + for (var k in this.members) { + var item = this.members[k] + var globalLocation = { + offset: location.offset + item.storagelocation.offset, + slot: util.add(location.slot, item.storagelocation.slot) + } + try { + ret[item.name] = await item.type.decodeFromStorage(globalLocation, storageResolver) + } catch (e) { + console.log(e) + ret[item.name] = '' + } + } + return { + value: ret, + type: this.typeName + } + } + + decodeFromMemoryInternal (offset, memory) { + var ret = {} + this.members.map((item, i) => { + var contentOffset = offset + var member = item.type.decodeFromMemory(contentOffset, memory) + ret[item.name] = member + offset += 32 + }) + return { + value: ret, + type: this.typeName + } + } +} + +module.exports = Struct diff --git a/remix-debug/src/decoder/types/Uint.js b/remix-debug/src/decoder/types/Uint.js new file mode 100644 index 0000000000..3bdc4b4976 --- /dev/null +++ b/remix-debug/src/decoder/types/Uint.js @@ -0,0 +1,16 @@ +'use strict' +var util = require('./util') +var ValueType = require('./ValueType') + +class Uint extends ValueType { + constructor (storageBytes) { + super(1, storageBytes, 'uint' + storageBytes * 8) + } + + decodeValue (value) { + value = util.extractHexByteSlice(value, this.storageBytes, 0) + return util.decodeIntFromHex(value, this.storageBytes, false) + } +} + +module.exports = Uint diff --git a/remix-debug/src/decoder/types/ValueType.js b/remix-debug/src/decoder/types/ValueType.js new file mode 100644 index 0000000000..44c4c7b536 --- /dev/null +++ b/remix-debug/src/decoder/types/ValueType.js @@ -0,0 +1,72 @@ +'use strict' +var util = require('./util') + +class ValueType { + constructor (storageSlots, storageBytes, typeName) { + this.storageSlots = storageSlots + this.storageBytes = storageBytes + this.typeName = typeName + this.basicType = 'ValueType' + } + + /** + * decode the type with the @arg location from the storage + * + * @param {Object} location - containing offset and slot + * @param {Object} storageResolver - resolve storage queries + * @return {Object} - decoded value + */ + async decodeFromStorage (location, storageResolver) { + try { + var value = await util.extractHexValue(location, storageResolver, this.storageBytes) + return { + value: this.decodeValue(value), + type: this.typeName + } + } catch (e) { + console.log(e) + return { + value: '', + type: this.typeName + } + } + } + + /** + * decode the type from the stack + * + * @param {Int} stackDepth - position of the type in the stack + * @param {Array} stack - stack + * @param {String} - memory + * @return {Object} - decoded value + */ + async decodeFromStack (stackDepth, stack, memory) { + var value + if (stackDepth >= stack.length) { + value = this.decodeValue('') + } else { + value = this.decodeValue(stack[stack.length - 1 - stackDepth].replace('0x', '')) + } + return { + value: value, + type: this.typeName + } + } + + /** + * decode the type with the @arg offset location from the memory + * + * @param {Int} stackDepth - position of the type in the stack + * @return {String} - memory + * @return {Object} - decoded value + */ + decodeFromMemory (offset, memory) { + var value = memory.substr(2 * offset, 64) + return { + value: this.decodeValue(value), + type: this.typeName + } + } +} + +module.exports = ValueType diff --git a/remix-debug/src/decoder/types/util.js b/remix-debug/src/decoder/types/util.js new file mode 100644 index 0000000000..c7d8503b9e --- /dev/null +++ b/remix-debug/src/decoder/types/util.js @@ -0,0 +1,120 @@ +'use strict' +var ethutil = require('ethereumjs-util') +var BN = require('ethereumjs-util').BN + +module.exports = { + readFromStorage: readFromStorage, + decodeIntFromHex: decodeIntFromHex, + extractHexValue: extractHexValue, + extractHexByteSlice: extractHexByteSlice, + toBN: toBN, + add: add, + extractLocation: extractLocation, + removeLocation: removeLocation, + normalizeHex: normalizeHex, + extractLocationFromAstVariable: extractLocationFromAstVariable +} + +function decodeIntFromHex (value, byteLength, signed) { + var bigNumber = new BN(value, 16) + if (signed) { + bigNumber = bigNumber.fromTwos(8 * byteLength) + } + return bigNumber.toString(10) +} + +function readFromStorage (slot, storageResolver) { + var hexSlot = '0x' + normalizeHex(ethutil.bufferToHex(slot)) + return new Promise((resolve, reject) => { + storageResolver.storageSlot(hexSlot, (error, slot) => { + if (error) { + return reject(error) + } else { + if (!slot) { + slot = { + key: slot, + value: '' + } + } + return resolve(normalizeHex(slot.value)) + } + }) + }) +} + +/** + * @returns a hex encoded byte slice of length @arg byteLength from inside @arg slotValue. + * + * @param {String} slotValue - hex encoded value to extract the byte slice from + * @param {Int} byteLength - Length of the byte slice to extract + * @param {Int} offsetFromLSB - byte distance from the right end slot value to the right end of the byte slice + */ +function extractHexByteSlice (slotValue, byteLength, offsetFromLSB) { + var offset = slotValue.length - 2 * offsetFromLSB - 2 * byteLength + return slotValue.substr(offset, 2 * byteLength) +} + +/** + * @returns a hex encoded storage content at the given @arg location. it does not have Ox prefix but always has the full length. + * + * @param {Object} location - object containing the slot and offset of the data to extract. + * @param {Object} storageResolver - storage resolver + * @param {Int} byteLength - Length of the byte slice to extract + */ +async function extractHexValue (location, storageResolver, byteLength) { + var slotvalue + try { + slotvalue = await readFromStorage(location.slot, storageResolver) + } catch (e) { + console.log(e) + return '0x' + } + return extractHexByteSlice(slotvalue, byteLength, location.offset) +} + +function toBN (value) { + if (value instanceof BN) { + return value + } else if (value.indexOf && value.indexOf('0x') === 0) { + value = ethutil.unpad(value.replace('Ox', '')) + value = new BN(value === '' ? '0' : value, 16) + } else if (!isNaN(value)) { + value = new BN(value) + } + return value +} + +function add (value1, value2) { + return toBN(value1).add(toBN(value2)) +} + +function removeLocation (type) { + return type.replace(/( storage ref| storage pointer| memory| calldata)/g, '') +} + +function extractLocation (type) { + var match = type.match(/( storage ref| storage pointer| memory| calldata)?$/) + if (match[1] !== '') { + return match[1].trim() + } else { + return null + } +} + +function extractLocationFromAstVariable (node) { + if (node.attributes.storageLocation !== 'default') { + return node.attributes.storageLocation + } else if (node.attributes.stateVariable) { + return 'storage' + } else { + return 'default' // local variables => storage, function parameters & return values => memory, state => storage + } +} + +function normalizeHex (hex) { + hex = hex.replace('0x', '') + if (hex.length < 64) { + return (new Array(64 - hex.length + 1).join('0')) + hex + } + return hex +} diff --git a/remix-debug/src/storage/mappingPreimages.js b/remix-debug/src/storage/mappingPreimages.js new file mode 100644 index 0000000000..5f6d4a47dd --- /dev/null +++ b/remix-debug/src/storage/mappingPreimages.js @@ -0,0 +1,53 @@ + +module.exports = { + decodeMappingsKeys: decodeMappingsKeys +} + +/** + * extract the mappings location from the storage + * like { "" : { "": preimageOf1 }, { "": preimageOf2 }, ... } + * + * @param {Object} storage - storage given by storage Viewer (basically a mapping hashedkey : {key, value}) + * @param {Function} callback - calback + * @return {Map} - solidity mapping location (e.g { "" : { "": preimageOf1 }, { "": preimageOf2 }, ... }) + */ +async function decodeMappingsKeys (web3, storage, callback) { + var ret = {} + for (var hashedLoc in storage) { + var preimage + try { + preimage = await getPreimage(web3, storage[hashedLoc].key) + } catch (e) { + } + if (preimage) { + // got preimage! + // get mapping position (i.e. storage slot), its the last 32 bytes + var slotByteOffset = preimage.length - 64 + var mappingSlot = preimage.substr(slotByteOffset) + var mappingKey = preimage.substr(0, slotByteOffset) + if (!ret[mappingSlot]) { + ret[mappingSlot] = {} + } + ret[mappingSlot][mappingKey] = preimage + } + } + callback(null, ret) +} + +/** + * Uses web3 to return preimage of a key + * + * @param {String} key - key to retrieve the preimage of + * @return {String} - preimage of the given key + */ +function getPreimage (web3, key) { + return new Promise((resolve, reject) => { + web3.debug.preimage(key.indexOf('0x') === 0 ? key : '0x' + key, function (error, preimage) { + if (error) { + resolve(null) + } else { + resolve(preimage) + } + }) + }) +} diff --git a/remix-debug/src/storage/storageResolver.js b/remix-debug/src/storage/storageResolver.js new file mode 100644 index 0000000000..245a73b6a2 --- /dev/null +++ b/remix-debug/src/storage/storageResolver.js @@ -0,0 +1,166 @@ +'use strict' +var remixLib = require('remix-lib') +var traceHelper = remixLib.helpers.trace +var mappingPreimages = require('./mappingPreimages') + +/** + * Basically one instance is created for one debugging session. + * (TODO: one instance need to be shared over all the components) + */ +class StorageResolver { + constructor (options) { + this.storageByAddress = {} + this.preimagesMappingByAddress = {} + this.maxSize = 100 + this.web3 = options.web3 + this.zeroSlot = '0x0000000000000000000000000000000000000000000000000000000000000000' + } + + /** + * returns the storage for the given context (address and vm trace index) + * returns the range 0x0 => this.maxSize + * + * @param {Object} - tx - transaction + * @param {Int} - stepIndex - Index of the stop in the vm trace + * @param {String} - address - lookup address + * @param {Function} - callback - contains a map: [hashedKey] = {key, hashedKey, value} + */ + storageRange (tx, stepIndex, address, callback) { + this.storageRangeInternal(this, this.zeroSlot, tx, stepIndex, address, callback) + } + + /** + * compute the mappgings type locations for the current address (cached for a debugging session) + * note: that only retrieve the first 100 items. + * + * @param {String} address - contract address + * @param {Object} address - storage + * @return {Function} - callback + */ + initialPreimagesMappings (tx, stepIndex, address, callback) { + const self = this + if (this.preimagesMappingByAddress[address]) { + return callback(null, this.preimagesMappingByAddress[address]) + } + this.storageRange(tx, stepIndex, address, (error, storage) => { + if (error) { + return callback(error) + } + mappingPreimages.decodeMappingsKeys(self.web3, storage, (error, mappings) => { + if (error) { + callback(error) + } else { + this.preimagesMappingByAddress[address] = mappings + callback(null, mappings) + } + }) + }) + } + + /** + * return a slot value for the given context (address and vm trace index) + * + * @param {String} - slot - slot key + * @param {Object} - tx - transaction + * @param {Int} - stepIndex - Index of the stop in the vm trace + * @param {String} - address - lookup address + * @param {Function} - callback - {key, hashedKey, value} - + */ + storageSlot (slot, tx, stepIndex, address, callback) { + this.storageRangeInternal(this, slot, tx, stepIndex, address, function (error, storage) { + if (error) { + callback(error) + } else { + callback(null, storage[slot] !== undefined ? storage[slot] : null) + } + }) + } + + /** + * return True if the storage at @arg address is complete + * + * @param {String} address - contract address + * @return {Bool} - return True if the storage at @arg address is complete + */ + isComplete (address) { + return this.storageByAddress[address] && this.storageByAddress[address].complete + } + + /** + * retrieve the storage and ensure at least @arg slot is cached. + * - If @arg slot is already cached, the storage will be returned from the cache + * even if the next 1000 items are not in the cache. + * - If @arg slot is not cached, the corresponding value will be resolved and the next 1000 slots. + */ + storageRangeInternal (self, slotKey, tx, stepIndex, address, callback) { + var cached = this.fromCache(self, address) + if (cached && cached.storage[slotKey]) { // we have the current slot in the cache and maybe the next 1000... + return callback(null, cached.storage) + } + this.storageRangeWeb3Call(tx, address, slotKey, self.maxSize, (error, storage, nextKey) => { + if (error) { + return callback(error) + } + if (!storage[slotKey] && slotKey !== self.zeroSlot) { // we don't cache the zero slot (could lead to inconsistency) + storage[slotKey] = { + key: slotKey, + value: self.zeroSlot + } + } + self.toCache(self, address, storage) + if (slotKey === self.zeroSlot && !nextKey) { // only working if keys are sorted !! + self.storageByAddress[address].complete = true + } + callback(null, storage) + }) + } + + /** + * retrieve the storage from the cache. if @arg slot is defined, return only the desired slot, if not return the entire known storage + * + * @param {String} address - contract address + * @return {String} - either the entire known storage or a single value + */ + fromCache (self, address) { + if (!self.storageByAddress[address]) { + return null + } + return self.storageByAddress[address] + } + + /** + * store the result of `storageRangeAtInternal` + * + * @param {String} address - contract address + * @param {Object} storage - result of `storageRangeAtInternal`, contains {key, hashedKey, value} + */ + toCache (self, address, storage) { + if (!self.storageByAddress[address]) { + self.storageByAddress[address] = {} + } + self.storageByAddress[address].storage = Object.assign(self.storageByAddress[address].storage || {}, storage) + } + + storageRangeWeb3Call (tx, address, start, maxSize, callback) { + if (traceHelper.isContractCreation(address)) { + callback(null, {}, null) + } else { + this.web3.debug.storageRangeAt( + tx.blockHash, tx.transactionIndex === undefined ? tx.hash : tx.transactionIndex, + address, + start, + maxSize, + (error, result) => { + if (error) { + callback(error) + } else if (result.storage) { + callback(null, result.storage, result.nextKey) + } else { + callback('the storage has not been provided') + } + }) + } + } +} + +module.exports = StorageResolver diff --git a/remix-debug/src/storage/storageViewer.js b/remix-debug/src/storage/storageViewer.js new file mode 100644 index 0000000000..21af4d5f38 --- /dev/null +++ b/remix-debug/src/storage/storageViewer.js @@ -0,0 +1,132 @@ +'use strict' +var remixLib = require('remix-lib') +var util = remixLib.util +var mappingPreimages = require('./mappingPreimages') + + /** + * easier access to the storage resolver + * Basically one instance is created foreach execution step and foreach component that need it. + * (TODO: one instance need to be shared over all the components) + */ +class StorageViewer { + constructor (_context, _storageResolver, _traceManager) { + this.context = _context + this.storageResolver = _storageResolver + this.web3 = this.storageResolver.web3 + this.initialMappingsLocationPromise = null + this.currentMappingsLocationPromise = null + _traceManager.accumulateStorageChanges(this.context.stepIndex, this.context.address, {}, (error, storageChanges) => { + if (!error) { + this.storageChanges = storageChanges + } else { + console.log(error) + } + }) + } + + /** + * return the storage for the current context (address and vm trace index) + * by default now returns the range 0 => 1000 + * + * @param {Function} - callback - contains a map: [hashedKey] = {key, hashedKey, value} + */ + storageRange (callback) { + this.storageResolver.storageRange(this.context.tx, this.context.stepIndex, this.context.address, (error, storage) => { + if (error) { + callback(error) + } else { + callback(null, Object.assign({}, storage, this.storageChanges)) + } + }) + } + + /** + * return a slot value for the current context (address and vm trace index) + * @param {String} - slot - slot key (not hashed key!) + * @param {Function} - callback - {key, hashedKey, value} - + */ + storageSlot (slot, callback) { + var hashed = util.sha3_256(slot) + if (this.storageChanges[hashed]) { + return callback(null, this.storageChanges[hashed]) + } + this.storageResolver.storageSlot(hashed, this.context.tx, this.context.stepIndex, this.context.address, (error, storage) => { + if (error) { + callback(error) + } else { + callback(null, storage) + } + }) + } + + /** + * return True if the storage at @arg address is complete + * + * @param {String} address - contract address + * @return {Bool} - return True if the storage at @arg address is complete + */ + isComplete (address) { + return this.storageResolver.isComplete(address) + } + + /** + * return all the possible mappings locations for the current context (cached) do not return state changes during the current transaction + * + * @param {Function} callback + */ + async initialMappingsLocation () { + if (!this.initialMappingsLocationPromise) { + this.initialMappingsLocationPromise = new Promise((resolve, reject) => { + this.storageResolver.initialPreimagesMappings(this.context.tx, this.context.stepIndex, this.context.address, (error, initialMappingsLocation) => { + if (error) { + reject(error) + } else { + resolve(initialMappingsLocation) + } + }) + }) + } + return this.initialMappingsLocationPromise + } + + /** + * return all the possible mappings locations for the current context (cached) and current mapping slot. returns state changes during the current transaction + * + * @param {Function} callback + */ + async mappingsLocation () { + if (!this.currentMappingsLocationPromise) { + this.currentMappingsLocationPromise = new Promise((resolve, reject) => { + this.extractMappingsLocationChanges(this.storageChanges, (error, mappingsLocationChanges) => { + if (error) { + reject(error) + } else { + resolve(mappingsLocationChanges) + } + }) + }) + } + return this.currentMappingsLocationPromise + } + + /** + * retrieve mapping location changes from the storage changes. + * + * @param {Function} callback + */ + extractMappingsLocationChanges (storageChanges, callback) { + if (this.mappingsLocationChanges) { + return callback(null, this.mappingsLocationChanges) + } + mappingPreimages.decodeMappingsKeys(this.web3, storageChanges, (error, mappings) => { + if (!error) { + this.mappingsLocationChanges = mappings + return callback(null, this.mappingsLocationChanges) + } else { + callback(error) + } + }) + } +} + +module.exports = StorageViewer diff --git a/src/trace/traceAnalyser.js b/remix-debug/src/trace/traceAnalyser.js similarity index 80% rename from src/trace/traceAnalyser.js rename to remix-debug/src/trace/traceAnalyser.js index fbeafabb4e..240e1ee3dd 100644 --- a/src/trace/traceAnalyser.js +++ b/remix-debug/src/trace/traceAnalyser.js @@ -1,5 +1,6 @@ 'use strict' -var traceHelper = require('../helpers/traceHelper') +var remixLib = require('remix-lib') +var traceHelper = remixLib.helpers.trace function TraceAnalyser (_cache) { this.traceCache = _cache @@ -10,17 +11,12 @@ TraceAnalyser.prototype.analyse = function (trace, tx, callback) { this.trace = trace this.traceCache.pushStoreChanges(0, tx.to) var context = { - currentStorageAddress: tx.to, - previousStorageAddress: tx.to, + storageContext: [tx.to], currentCallIndex: 0, lastCallIndex: 0 } var callStack = [tx.to] - this.traceCache.pushCallChanges(0, 0, callStack[0]) - this.traceCache.pushCallStack(0, { - callStack: callStack.slice(0) - }) - + this.traceCache.pushCall(trace[0], 0, callStack[0], callStack.slice(0)) if (traceHelper.isContractCreation(tx.to)) { this.traceCache.pushContractCreation(tx.to, tx.input) } @@ -80,16 +76,16 @@ TraceAnalyser.prototype.buildStorage = function (index, step, context) { if (traceHelper.newContextStorage(step) && !traceHelper.isCallToPrecompiledContract(index, this.trace)) { var calledAddress = traceHelper.resolveCalledAddress(index, this.trace) if (calledAddress) { - context.currentStorageAddress = calledAddress + context.storageContext.push(calledAddress) } else { console.log('unable to build storage changes. ' + index + ' does not match with a CALL. storage changes will be corrupted') } - this.traceCache.pushStoreChanges(index + 1, context.currentStorageAddress) + this.traceCache.pushStoreChanges(index + 1, context.storageContext[context.storageContext.length - 1]) } else if (traceHelper.isSSTOREInstruction(step)) { - this.traceCache.pushStoreChanges(index + 1, context.currentStorageAddress, step.stack[step.stack.length - 1], step.stack[step.stack.length - 2]) + this.traceCache.pushStoreChanges(index + 1, context.storageContext[context.storageContext.length - 1], step.stack[step.stack.length - 1], step.stack[step.stack.length - 2]) } else if (traceHelper.isReturnInstruction(step)) { - context.currentStorageAddress = context.previousStorageAddress - this.traceCache.pushStoreChanges(index + 1, context.currentStorageAddress) + context.storageContext.pop() + this.traceCache.pushStoreChanges(index + 1, context.storageContext[context.storageContext.length - 1]) } return context } @@ -110,21 +106,15 @@ TraceAnalyser.prototype.buildDepth = function (index, step, tx, callStack, conte console.log('unable to build depth changes. ' + index + ' does not match with a CALL. depth changes will be corrupted') } } - this.traceCache.pushCallChanges(step, index + 1, newAddress) - this.traceCache.pushCallStack(index + 1, { - callStack: callStack.slice(0) - }) + this.traceCache.pushCall(step, index + 1, newAddress, callStack.slice(0)) this.buildCalldata(index, step, tx, true) this.traceCache.pushSteps(index, context.currentCallIndex) context.lastCallIndex = context.currentCallIndex context.currentCallIndex = 0 - } else if (traceHelper.isReturnInstruction(step)) { - if (index + 1 < this.trace.length) { + } else if (traceHelper.isReturnInstruction(step) || traceHelper.isStopInstruction(step) || step.error || step.invalidDepthChange) { + if (index < this.trace.length) { callStack.pop() - this.traceCache.pushCallChanges(step, index + 1) - this.traceCache.pushCallStack(index + 1, { - callStack: callStack.slice(0) - }) + this.traceCache.pushCall(step, index + 1, null, callStack.slice(0), step.error || step.invalidDepthChange) this.buildCalldata(index, step, tx, false) this.traceCache.pushSteps(index, context.currentCallIndex) context.currentCallIndex = context.lastCallIndex + 1 diff --git a/src/trace/traceCache.js b/remix-debug/src/trace/traceCache.js similarity index 55% rename from src/trace/traceCache.js rename to remix-debug/src/trace/traceCache.js index 040c0919bb..6dc98a16b2 100644 --- a/src/trace/traceCache.js +++ b/remix-debug/src/trace/traceCache.js @@ -1,4 +1,7 @@ 'use strict' +var remixLib = require('remix-lib') +var helper = remixLib.util + function TraceCache () { this.init() } @@ -7,8 +10,8 @@ TraceCache.prototype.init = function () { // ...Changes contains index in the vmtrace of the corresponding changes this.returnValues = {} - this.callChanges = [] - this.calls = {} + this.currentCall = null + this.callsTree = null this.callsData = {} this.contractCreation = {} this.steps = {} @@ -18,7 +21,6 @@ TraceCache.prototype.init = function () { this.memoryChanges = [] this.storageChanges = [] this.sstore = {} // all sstore occurence in the trace - this.callStack = {} // contains all callStack by vmtrace index (we need to rebuild it, callstack is not included in the vmtrace) } TraceCache.prototype.pushSteps = function (index, currentCallIndex) { @@ -34,11 +36,34 @@ TraceCache.prototype.pushMemoryChanges = function (value) { this.memoryChanges.push(value) } -TraceCache.prototype.pushCallChanges = function (step, value, address) { - this.callChanges.push(value) - this.calls[value] = { - op: step.op, - address: address +// outOfGas has been removed because gas left logging is apparently made differently +// in the vm/geth/eth. TODO add the error property (with about the error in all clients) +TraceCache.prototype.pushCall = function (step, index, address, callStack, reverted) { + var validReturnStep = step.op === 'RETURN' || step.op === 'STOP' + if (validReturnStep || reverted) { + if (this.currentCall) { + this.currentCall.call.return = index - 1 + if (!validReturnStep) { + this.currentCall.call.reverted = reverted + } + var parent = this.currentCall.parent + this.currentCall = parent ? { call: parent.call, parent: parent.parent } : null + } + } else { + var call = { + op: step.op, + address: address, + callStack: callStack, + calls: {}, + start: index + } + this.addresses.push(address) + if (this.currentCall) { + this.currentCall.call.calls[index] = call + } else { + this.callsTree = { call: call } + } + this.currentCall = { call: call, parent: this.currentCall } } } @@ -58,31 +83,32 @@ TraceCache.prototype.pushContractCreation = function (token, code) { this.contractCreation[token] = code } -TraceCache.prototype.pushCallStack = function (index, callStack) { - this.callStack[index] = callStack -} - TraceCache.prototype.pushStoreChanges = function (index, address, key, value) { this.sstore[index] = { 'address': address, 'key': key, - 'value': value + 'value': value, + 'hashedKey': helper.sha3_256(key) } this.storageChanges.push(index) } -TraceCache.prototype.rebuildStorage = function (address, storage, index) { +TraceCache.prototype.accumulateStorageChanges = function (index, address, storage) { + var ret = Object.assign({}, storage) for (var k in this.storageChanges) { var changesIndex = this.storageChanges[k] if (changesIndex > index) { - return storage + return ret } var sstore = this.sstore[changesIndex] if (sstore.address === address && sstore.key) { - storage[sstore.key] = sstore.value + ret[sstore.hashedKey] = { + key: sstore.key, + value: sstore.value + } } } - return storage + return ret } module.exports = TraceCache diff --git a/src/trace/traceManager.js b/remix-debug/src/trace/traceManager.js similarity index 67% rename from src/trace/traceManager.js rename to remix-debug/src/trace/traceManager.js index 8d9215fd77..8db9b95157 100644 --- a/src/trace/traceManager.js +++ b/remix-debug/src/trace/traceManager.js @@ -3,15 +3,17 @@ var TraceAnalyser = require('./traceAnalyser') var TraceRetriever = require('./traceRetriever') var TraceCache = require('./traceCache') var TraceStepManager = require('./traceStepManager') -var traceHelper = require('../helpers/traceHelper') -var util = require('../helpers/global') +var remixLib = require('remix-lib') +var traceHelper = remixLib.helpers.trace +var util = remixLib.util -function TraceManager () { +function TraceManager (options) { + this.web3 = options.web3 this.isLoading = false this.trace = null this.traceCache = new TraceCache() this.traceAnalyser = new TraceAnalyser(this.traceCache) - this.traceRetriever = new TraceRetriever() + this.traceRetriever = new TraceRetriever({web3: this.web3}) this.traceStepManager = new TraceStepManager(this.traceAnalyser) this.tx } @@ -20,7 +22,7 @@ function TraceManager () { TraceManager.prototype.resolveTrace = function (tx, callback) { this.tx = tx this.init() - if (!util.web3) callback('web3 not loaded', false) + if (!this.web3) callback('web3 not loaded', false) this.isLoading = true var self = this this.traceRetriever.getTrace(tx.hash, function (error, result) { @@ -73,48 +75,13 @@ TraceManager.prototype.getLength = function (callback) { } } -TraceManager.prototype.getStorageAt = function (stepIndex, tx, callback, address) { - var check = this.checkRequestedStep(stepIndex) - if (check) { - return callback(check, null) - } - if (!address) { - var stoChange = traceHelper.findLowerBound(stepIndex, this.traceCache.storageChanges) - if (stoChange === undefined) return callback('no storage found', null) - address = this.traceCache.sstore[stoChange].address - } - var storage = {} - storage = this.traceCache.rebuildStorage(address, storage, stepIndex) +TraceManager.prototype.accumulateStorageChanges = function (index, address, storageOrigin, callback) { + var storage = this.traceCache.accumulateStorageChanges(index, address, storageOrigin) callback(null, storage) - /* - // TODO: use it if we need the full storage to be loaded - var self = this - if (this.traceRetriever.debugStorageAtAvailable()) { - var address = this.traceCache.sstore[stoChange].address - this.traceRetriever.getStorage(tx, address, function (error, result) { - if (error) { - console.log(error) - callback(error, null) - } else { - var storage = self.traceCache.rebuildStorage(address, result, stepIndex) - callback(null, storage) - } - }) - } else { - callback(null, this.trace[stoChange].storage) - } - */ } TraceManager.prototype.getAddresses = function (callback) { - var addresses = [ this.tx.to ] - for (var k in this.traceCache.calls) { - var address = this.traceCache.calls[k].address - if (address && addresses.join('').indexOf(address) === -1) { - addresses.push(address) - } - } - callback(null, addresses) + callback(null, this.traceCache.addresses) } TraceManager.prototype.getCallDataAt = function (stepIndex, callback) { @@ -122,19 +89,29 @@ TraceManager.prototype.getCallDataAt = function (stepIndex, callback) { if (check) { return callback(check, null) } - var callDataChange = traceHelper.findLowerBound(stepIndex, this.traceCache.callDataChanges) - if (callDataChange === undefined) return callback('no calldata found', null) + var callDataChange = util.findLowerBoundValue(stepIndex, this.traceCache.callDataChanges) + if (callDataChange === null) return callback('no calldata found', null) callback(null, [this.traceCache.callsData[callDataChange]]) } +TraceManager.prototype.buildCallPath = function (stepIndex, callback) { + var check = this.checkRequestedStep(stepIndex) + if (check) { + return callback(check, null) + } + var callsPath = util.buildCallPath(stepIndex, this.traceCache.callsTree.call) + if (callsPath === null) return callback('no call path built', null) + callback(null, callsPath) +} + TraceManager.prototype.getCallStackAt = function (stepIndex, callback) { var check = this.checkRequestedStep(stepIndex) if (check) { return callback(check, null) } - var callStackChange = traceHelper.findLowerBound(stepIndex, this.traceCache.callChanges) - if (callStackChange === undefined) return callback('no callstack found', null) - callback(null, this.traceCache.callStack[callStackChange].callStack) + var call = util.findCall(stepIndex, this.traceCache.callsTree.call) + if (call === null) return callback('no callstack found', null) + callback(null, call.callStack) } TraceManager.prototype.getStackAt = function (stepIndex, callback) { @@ -157,8 +134,8 @@ TraceManager.prototype.getLastCallChangeSince = function (stepIndex, callback) { if (check) { return callback(check, null) } - var callChange = traceHelper.findLowerBound(stepIndex, this.traceCache.callChanges) - if (callChange === undefined) { + var callChange = util.findCall(stepIndex, this.traceCache.callsTree.call) + if (callChange === null) { callback(null, 0) } else { callback(null, callChange) @@ -170,21 +147,14 @@ TraceManager.prototype.getCurrentCalledAddressAt = function (stepIndex, callback if (check) { return callback(check, null) } - var self = this - this.getLastCallChangeSince(stepIndex, function (error, addressIndex) { + this.getLastCallChangeSince(stepIndex, function (error, resp) { if (error) { callback(error, null) } else { - if (addressIndex === 0) { - callback(null, self.tx.to) + if (resp) { + callback(null, resp.address) } else { - var callStack = self.traceCache.callStack[addressIndex].callStack - var calledAddress = callStack[callStack.length - 1] - if (calledAddress) { - callback(null, calledAddress) - } else { - callback('unable to get current called address. ' + stepIndex + ' does not match with a CALL', null) - } + callback('unable to get current called address. ' + stepIndex + ' does not match with a CALL') } } }) @@ -203,8 +173,8 @@ TraceManager.prototype.getMemoryAt = function (stepIndex, callback) { if (check) { return callback(check, null) } - var lastChanges = traceHelper.findLowerBound(stepIndex, this.traceCache.memoryChanges) - if (lastChanges === undefined) return callback('no memory found', null) + var lastChanges = util.findLowerBoundValue(stepIndex, this.traceCache.memoryChanges) + if (lastChanges === null) return callback('no memory found', null) callback(null, this.trace[lastChanges].memory) } @@ -221,7 +191,11 @@ TraceManager.prototype.getReturnValue = function (stepIndex, callback) { if (check) { return callback(check, null) } - callback(null, this.traceCache.returnValues[stepIndex]) + if (!this.traceCache.returnValues[stepIndex]) { + callback('current step is not a return step') + } else { + callback(null, this.traceCache.returnValues[stepIndex]) + } } TraceManager.prototype.getCurrentStep = function (stepIndex, callback) { @@ -269,18 +243,14 @@ TraceManager.prototype.findStepOverForward = function (currentStep) { return this.traceStepManager.findStepOverForward(currentStep) } -TraceManager.prototype.findStepOutBack = function (currentStep) { - return this.traceStepManager.findStepOutBack(currentStep) -} - -TraceManager.prototype.findStepOutForward = function (currentStep) { - return this.traceStepManager.findStepOutForward(currentStep) -} - TraceManager.prototype.findNextCall = function (currentStep) { return this.traceStepManager.findNextCall(currentStep) } +TraceManager.prototype.findStepOut = function (currentStep) { + return this.traceStepManager.findStepOut(currentStep) +} + // util TraceManager.prototype.checkRequestedStep = function (stepIndex) { if (!this.trace) { @@ -291,4 +261,16 @@ TraceManager.prototype.checkRequestedStep = function (stepIndex) { return undefined } +TraceManager.prototype.waterfall = function (calls, stepindex, cb) { + var ret = [] + var retError = null + for (var call in calls) { + calls[call].apply(this, [stepindex, function (error, result) { + retError = error + ret.push({ error: error, value: result }) + }]) + } + cb(retError, ret) +} + module.exports = TraceManager diff --git a/remix-debug/src/trace/traceRetriever.js b/remix-debug/src/trace/traceRetriever.js new file mode 100644 index 0000000000..07e757c0f0 --- /dev/null +++ b/remix-debug/src/trace/traceRetriever.js @@ -0,0 +1,19 @@ +'use strict' + +function TraceRetriever (options) { + this.web3 = options.web3 +} + +TraceRetriever.prototype.getTrace = function (txHash, callback) { + var options = { + disableStorage: true, + disableMemory: false, + disableStack: false, + fullStorage: false + } + this.web3.debug.traceTransaction(txHash, options, function (error, result) { + callback(error, result) + }) +} + +module.exports = TraceRetriever diff --git a/remix-debug/src/trace/traceStepManager.js b/remix-debug/src/trace/traceStepManager.js new file mode 100644 index 0000000000..5b4f50a625 --- /dev/null +++ b/remix-debug/src/trace/traceStepManager.js @@ -0,0 +1,58 @@ +'use strict' +var remixLib = require('remix-lib') +var traceHelper = remixLib.helpers.trace +var util = remixLib.util + +function TraceStepManager (_traceAnalyser) { + this.traceAnalyser = _traceAnalyser +} + +TraceStepManager.prototype.isCallInstruction = function (index) { + var state = this.traceAnalyser.trace[index] + return traceHelper.isCallInstruction(state) && !traceHelper.isCallToPrecompiledContract(index, this.traceAnalyser.trace) +} + +TraceStepManager.prototype.isReturnInstruction = function (index) { + var state = this.traceAnalyser.trace[index] + return traceHelper.isReturnInstruction(state) +} + +TraceStepManager.prototype.findStepOverBack = function (currentStep) { + if (this.isReturnInstruction(currentStep)) { + var call = util.findCall(currentStep, this.traceAnalyser.traceCache.callsTree.call) + return call.start > 0 ? call.start - 1 : 0 + } else { + return currentStep > 0 ? currentStep - 1 : 0 + } +} + +TraceStepManager.prototype.findStepOverForward = function (currentStep) { + if (this.isCallInstruction(currentStep)) { + var call = util.findCall(currentStep + 1, this.traceAnalyser.traceCache.callsTree.call) + return call.return + 1 < this.traceAnalyser.trace.length ? call.return + 1 : this.traceAnalyser.trace.length - 1 + } else { + return this.traceAnalyser.trace.length >= currentStep + 1 ? currentStep + 1 : currentStep + } +} + +TraceStepManager.prototype.findNextCall = function (currentStep) { + var call = util.findCall(currentStep, this.traceAnalyser.traceCache.callsTree.call) + var subCalls = Object.keys(call.calls) + if (subCalls.length) { + var callStart = util.findLowerBound(currentStep, subCalls) + 1 + if (subCalls.length > callStart) { + return subCalls[callStart] - 1 + } else { + return currentStep + } + } else { + return currentStep + } +} + +TraceStepManager.prototype.findStepOut = function (currentStep) { + var call = util.findCall(currentStep, this.traceAnalyser.traceCache.callsTree.call) + return call.return +} + +module.exports = TraceStepManager diff --git a/test/codeManager.js b/remix-debug/test/codeManager.js similarity index 77% rename from test/codeManager.js rename to remix-debug/test/codeManager.js index 2d9ceb9da3..a0709226de 100644 --- a/test/codeManager.js +++ b/remix-debug/test/codeManager.js @@ -1,10 +1,12 @@ 'use strict' var tape = require('tape') -var Web3Providers = require('../src/web3Provider/web3Providers') +var remixLib = require('remix-lib') +var Web3Providers = remixLib.vm.Web3Providers var TraceManager = require('../src/trace/traceManager') var CodeManager = require('../src/code/codeManager') var web3Test = require('./resources/testWeb3') -var util = require('../src/helpers/global') + +let web3 = null tape('CodeManager', function (t) { var codeManager @@ -16,12 +18,12 @@ tape('CodeManager', function (t) { console.log(mes) t.fail(mes) } else { - util.web3 = obj - var traceManager = new TraceManager() + web3 = obj + var traceManager = new TraceManager({web3: web3}) codeManager = new CodeManager(traceManager) - var contractCode = util.web3.eth.getCode('0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5') + var contractCode = web3.eth.getCode('0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5') codeManager.codeResolver.cacheExecutingCode('0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5', contractCode) // so a call to web3 is not necessary - var tx = util.web3.eth.getTransaction('0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51') + var tx = web3.eth.getTransaction('0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51') traceManager.resolveTrace(tx, function (error, result) { if (error) { t.fail(' - traceManager.resolveTrace - failed ' + result) @@ -40,7 +42,7 @@ function continueTesting (t, codeManager) { t.test('CodeManager.resolveStep', function (st) { st.plan(6) - codeManager.register('indexChanged', this, function (index) { + codeManager.event.register('changed', this, function (code, address, index) { if (index === undefined || index === null) { st.fail(index) } else { @@ -48,8 +50,7 @@ function continueTesting (t, codeManager) { } }) - codeManager.register('codeChanged', this, function (code, address, index) { - console.log(address + ' ' + index + ' ' + code) + codeManager.event.register('changed', this, function (code, address, index) { if (!code) { st.fail('no codes') } else { @@ -63,7 +64,7 @@ function continueTesting (t, codeManager) { } } }) - var tx = util.web3.eth.getTransaction('0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51') + var tx = web3.eth.getTransaction('0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51') codeManager.resolveStep(0, tx) codeManager.resolveStep(70, tx) }) diff --git a/remix-debug/test/decoder/contracts/byteStorage.js b/remix-debug/test/decoder/contracts/byteStorage.js new file mode 100644 index 0000000000..dfd5e04fff --- /dev/null +++ b/remix-debug/test/decoder/contracts/byteStorage.js @@ -0,0 +1,89 @@ +'use strict' + +module.exports = { + contract: ` + contract byteStorage { + enum enum1 { e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14, e15, e16, e17, e18, e19, e20, e21, e22, e23, e24, e25, e26, e27, e28, e29, e30, e31, e32, e33, e34, e35, e36, e37, e38, e39, e40, e41, e42, e43, e44, e45, e46, e47, e48, e49, e50, e51, e52, e53, e54, e55, e56, e57, e58, e59, e60, e61, e62, e63, e64, e65, e66, e67, e68, e69, e70, e71, e72, e73, e74, e75, e76, e77, e78, e79, e80, e81, e82, e83, e84, e85, e86, e87, e88, e89, e90, e91, e92, e93, e94, e95, e96, e97, e98, e99, e100, e101, e102, e103, e104, e105, e106, e107, e108, e109, e110, e111, e112, e113, e114, e115, e116, e117, e118, e119, e120, e121, e122, e123, e124, e125, e126, e127, e128, e129, e130, e131, e132, e133, e134, e135, e136, e137, e138, e139, e140, e141, e142, e143, e144, e145, e146, e147, e148, e149, e150, e151, e152, e153, e154, e155, e156, e157, e158, e159, e160, e161, e162, e163, e164, e165, e166, e167, e168, e169, e170, e171, e172, e173, e174, e175, e176, e177, e178, e179, e180, e181, e182, e183, e184, e185, e186, e187, e188, e189, e190, e191, e192, e193, e194, e195, e196, e197, e198, e199, e200, e201, e202, e203, e204, e205, e206, e207, e208, e209, e210, e211, e212, e213, e214, e215, e216, e217, e218, e219, e220, e221, e222, e223, e224, e225, e226, e227, e228, e229, e230, e231, e232, e233, e234, e235, e236, e237, e238, e239, e240, e241, e242, e243, e244, e245, e246, e247, e248, e249, e250, e251, e252, e253, e254, e255, e256, e257, e258, e259, e260 } + + bool b1 = false; + address a1 = 0xfe350f199f244ac9a79038d254400b632a633225; + bool b2 = true; + bytes dynb1 = "dynamicbytes"; + byte stab = 0x1; + bytes1 stab1 = hex"12"; + bytes2 stab2 = hex"1579"; + bytes3 stab3 = hex"359356"; + bytes4 stab4 = hex"2375"; + bytes5 stab5 = hex"02357645"; + bytes6 stab6 = hex"324435"; + bytes7 stab7 = hex"004324"; + bytes8 stab8 = hex"324554645765"; + bytes9 stab9 = hex"03434543"; + bytes10 stab10 = hex"04543543654657"; + bytes11 stab11 = hex"54354654"; + bytes12 stab12 = hex"03"; + bytes13 stab13 = hex"03243242345435"; + bytes14 stab14 = hex"32454354354353"; + bytes15 stab15 = hex"032454434435"; + bytes16 stab16 = hex"3245435444"; + bytes17 stab17 = hex"032454343243243245"; + bytes18 stab18 = hex"0324534325435435"; + bytes19 stab19 = hex"0324543435435435"; + bytes20 stab20 = hex"032454543543AB35"; + bytes21 stab21 = hex"32454432423435"; + bytes22 stab22 = hex"324543AEF5"; + bytes23 stab23 = hex"3245435FFF"; + bytes24 stab24 = hex"3245435F"; + bytes25 stab25 = hex"3245435F"; + bytes26 stab26 = hex"3245435F"; + bytes27 stab27 = hex"03245FFFFFFF"; + bytes28 stab28 = hex"03241235"; + bytes29 stab29 = hex"0325213213"; + bytes30 stab30 = hex"03245435232423"; + bytes31 stab31 = hex"3245435123"; + bytes32 stab32 = hex"324324423432543543AB"; + enum1 enumDec = enum1.e240; + string str1 = 'short'; + string str12 = 'шеллы'; + string str2 = 'long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long'; + } +`, + storage: { + '0x0000000000000000000000000000000000000000000000000000000000000000': '0x0000000000000000000001fe350f199f244ac9a79038d254400b632a63322500', + '0x0000000000000000000000000000000000000000000000000000000000000001': '0x64796e616d696362797465730000000000000000000000000000000000000018', + '0x0000000000000000000000000000000000000000000000000000000000000002': '0x0000000043240000000032443500000002357645002375000035935615791201', + '0x0000000000000000000000000000000000000000000000000000000000000003': '0x0000000000045435436546570000000343454300000000003245546457650000', + '0x0000000000000000000000000000000000000000000000000000000000000004': '0x0000000000000000000300000000000000000000005435465400000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000005': '0x0000000000324543543543530000000000000003243242345435000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000006': '0x0032454354440000000000000000000000032454434435000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000007': '0x0000000000000000000000000000000324543432432432450000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000008': '0x0000000000000000000000000000032453432543543500000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000009': '0x0000000000000000000000000003245434354354350000000000000000000000', + '0x000000000000000000000000000000000000000000000000000000000000000a': '0x000000000000000000000000032454543543ab35000000000000000000000000', + '0x000000000000000000000000000000000000000000000000000000000000000b': '0x0000000000000000000000324544324234350000000000000000000000000000', + '0x000000000000000000000000000000000000000000000000000000000000000c': '0x00000000000000000000324543aef50000000000000000000000000000000000', + '0x000000000000000000000000000000000000000000000000000000000000000d': '0x0000000000000000003245435fff000000000000000000000000000000000000', + '0x000000000000000000000000000000000000000000000000000000000000000e': '0x00000000000000003245435f0000000000000000000000000000000000000000', + '0x000000000000000000000000000000000000000000000000000000000000000f': '0x000000000000003245435f000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000010': '0x0000000000003245435f00000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000011': '0x000000000003245fffffff000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000012': '0x0000000003241235000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000013': '0x0000000325213213000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000014': '0x0000032454352324230000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000015': '0x0032454351230000000000000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000016': '0x324324423432543543ab00000000000000000000000000000000000000000000', + '0x0000000000000000000000000000000000000000000000000000000000000017': '0x00000000000000000000000000000000000000000000000000000000000000f0', + '0x0000000000000000000000000000000000000000000000000000000000000018': '0x73686f727400000000000000000000000000000000000000000000000000000a', + '0x0000000000000000000000000000000000000000000000000000000000000019': '0xd188d0b5d0bbd0bbd18b00000000000000000000000000000000000000000014', + '0x000000000000000000000000000000000000000000000000000000000000001a': '0x000000000000000000000000000000000000000000000000000000000000023d', + '0x057c384a7d1c54f3a1b2e5e67b2617b8224fdfd1ea7234eea573a6ff665ff63e': '0x6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f', + '0x057c384a7d1c54f3a1b2e5e67b2617b8224fdfd1ea7234eea573a6ff665ff63f': '0x6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e67', + '0x057c384a7d1c54f3a1b2e5e67b2617b8224fdfd1ea7234eea573a6ff665ff640': '0x5f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f', + '0x057c384a7d1c54f3a1b2e5e67b2617b8224fdfd1ea7234eea573a6ff665ff641': '0x6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f', + '0x057c384a7d1c54f3a1b2e5e67b2617b8224fdfd1ea7234eea573a6ff665ff642': '0x6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e67', + '0x057c384a7d1c54f3a1b2e5e67b2617b8224fdfd1ea7234eea573a6ff665ff643': '0x5f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f', + '0x057c384a7d1c54f3a1b2e5e67b2617b8224fdfd1ea7234eea573a6ff665ff644': '0x6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f', + '0x057c384a7d1c54f3a1b2e5e67b2617b8224fdfd1ea7234eea573a6ff665ff645': '0x6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e67', + '0x057c384a7d1c54f3a1b2e5e67b2617b8224fdfd1ea7234eea573a6ff665ff646': '0x5f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e675f5f6c6f6e670000' + } +} diff --git a/remix-debug/test/decoder/contracts/intLocal.js b/remix-debug/test/decoder/contracts/intLocal.js new file mode 100644 index 0000000000..1bfaf7c5ec --- /dev/null +++ b/remix-debug/test/decoder/contracts/intLocal.js @@ -0,0 +1,42 @@ +'use strict' + +module.exports = { + contract: ` +contract proxy { + struct testStruct { + int one; + uint two; + } +} +contract intLocal { + function intLocal () { + proxy.testStruct memory p; + uint8 ui8 = 130; + uint16 ui16 = 456; + uint32 ui32 = 4356; + uint64 ui64 = 3543543543; + uint128 ui128 = 234567; + uint256 ui256 = 115792089237316195423570985008687907853269984665640564039457584007880697216513; + uint ui = 123545666; + int8 i8 = -45; + int16 i16 = -1234; + int32 i32 = 3455; + int64 i64 = -35566; + int128 i128 = -444444; + int256 i256 = 3434343; + int i = -32432423423; + int32 ishrink = 2; + level11(); + level12(); + level11(); + } + + function level11() { + uint8 ui8 = 123; + level12(); + } + function level12() { + uint8 ui81 = 12; + } + } +`} diff --git a/remix-debug/test/decoder/contracts/intStorage.js b/remix-debug/test/decoder/contracts/intStorage.js new file mode 100644 index 0000000000..ec2ed7719e --- /dev/null +++ b/remix-debug/test/decoder/contracts/intStorage.js @@ -0,0 +1,32 @@ +'use strict' + +module.exports = { + contract: ` + contract intStorage { + uint8 ui8 = 130; + uint16 ui16 = 456; + uint32 ui32 = 4356; + uint64 ui64 = 3543543543; + uint128 ui128 = 234567; + uint256 public ui256 = 115792089237316195423570985008687907853269984665640564039457584007880697216513; + uint ui = 123545666; + int8 i8 = -45; + int16 i16 = -1234; + int32 i32 = 3455; + int64 i64 = -35566; + int128 i128 = -444444; + int256 i256 = 3434343; + int i = -32432423423; + int32 ishrink = 2; + } +`, + fullStorage: { + '0x0000000000000000000000000000000000000000000000000000000000000000': '0x000000000000000000000000000003944700000000d3362ef70000110401c882', + '0x0000000000000000000000000000000000000000000000000000000000000001': '0xfffffffffffffffffffffffffffffffffffffffffffffffffffffff872e07e01', + '0x0000000000000000000000000000000000000000000000000000000000000002': '0x00000000000000000000000000000000000000000000000000000000075d2842', + '0x0000000000000000000000000000000000000000000000000000000000000003': '0x00fffffffffffffffffffffffffff937e4ffffffffffff751200000d7ffb2ed3', + '0x0000000000000000000000000000000000000000000000000000000000000004': '0x0000000000000000000000000000000000000000000000000000000000346767', + '0x0000000000000000000000000000000000000000000000000000000000000005': '0xfffffffffffffffffffffffffffffffffffffffffffffffffffffff872e07e01', + '0x0000000000000000000000000000000000000000000000000000000000000006': '0x0000000000000000000000000000000000000000000000000000000000000002' + } +} diff --git a/remix-debug/test/decoder/contracts/mappingStorage.js b/remix-debug/test/decoder/contracts/mappingStorage.js new file mode 100644 index 0000000000..722c36a458 --- /dev/null +++ b/remix-debug/test/decoder/contracts/mappingStorage.js @@ -0,0 +1,16 @@ +module.exports = { + contract: ` + pragma solidity ^0.4.19; + +contract SimpleMappingState { + uint _num; + mapping(string => uint) _iBreakSolidityState; + mapping(uint => uint) _iBreakSolidityStateInt; + function updateNum(uint num, string str) public { + _num = num; + _iBreakSolidityState[str] = num; + _iBreakSolidityStateInt[num] = num; + } +} + ` +} diff --git a/remix-debug/test/decoder/contracts/miscContracts.js b/remix-debug/test/decoder/contracts/miscContracts.js new file mode 100644 index 0000000000..7e8f9214be --- /dev/null +++ b/remix-debug/test/decoder/contracts/miscContracts.js @@ -0,0 +1,80 @@ +'use strict' +module.exports = ` + contract baseContract { + uint8 u; + } + + contract contractUint is baseContract { + uint256 ui; + uint ui1; + bytes16 b; + } + + contract contractStructAndArray { + struct structDef { + uint8 ui; + string str; + } + structDef structDec; + structDef[3] array; + bytes12[4] bytesArray; + } + + contract contractArray { + uint32[5] i32st; + int8[] i8dyn; + int16[][3][][4] i16dyn; + } + + contract contractEnum { + enum enumDef {item0,item1,item2,item3,item4,item5,item6,item7,item8,item9,item10,item11,item12,item13,item14,item15,item16,item17,item18,item19,item20,item21,item22,item23,item24,item25,item26,item27,item28,item29,item30,item31,item32,item33,item34,item35,item36,item37,item38,item39,item40,item41,item42,item43,item44,item45,item46,item47,item48,item49,item50,item51,item52,item53,item54,item55,item56,item57,item58,item59,item60,item61,item62,item63,item64,item65,item66,item67,item68,item69,item70,item71,item72,item73,item74,item75,item76,item77,item78,item79,item80,item81,item82,item83,item84,item85,item86,item87,item88,item89,item90,item91,item92,item93,item94,item95,item96,item97,item98,item99,item100,item101,item102,item103,item104,item105,item106,item107,item108,item109,item110,item111,item112,item113,item114,item115,item116,item117,item118,item119,item120,item121,item122,item123,item124,item125,item126,item127,item128,item129,item130,item131,item132,item133,item134,item135,item136,item137,item138,item139,item140,item141,item142,item143,item144,item145,item146,item147,item148,item149,item150,item151,item152,item153,item154,item155,item156,item157,item158,item159,item160,item161,item162,item163,item164,item165,item166,item167,item168,item169,item170,item171,item172,item173,item174,item175,item176,item177,item178,item179,item180,item181,item182,item183,item184,item185,item186,item187,item188,item189,item190,item191,item192,item193,item194,item195,item196,item197,item198,item199,item200,item201,item202,item203,item204,item205,item206,item207,item208,item209,item210,item211,item212,item213,item214,item215,item216,item217,item218,item219,item220,item221,item222,item223,item224,item225,item226,item227,item228,item229,item230,item231,item232,item233,item234,item235,item236,item237,item238,item239,item240,item241,item242,item243,item244,item245,item246,item247,item248,item249,item250,item251,item252,item253,item254,item255,item256,item257,item258,item259,item260,item261,item262,item263,item264,item265,item266,item267,item268,item269,item270,item271,item272,item273,item274,item275,item276,item277,item278,item279,item280,item281,item282,item283,item284,item285,item286,item287,item288,item289,item290,item291,item292,item293,item294,item295,item296,item297,item298,item299,item100000000} + enumDef enum1; + } + + contract contractSmallVariable { + int8 i8; + uint8 iu8; + uint16 iu18; + int32 i32; + uint ui32; + int16 i16; + } + + contract testSimpleStorage1 { + uint32 uibase1; + } + + contract testSimpleStorage is testSimpleStorage1 { + uint ui1; + uint ui2; + uint[1] ui3; + uint[][1][4] ui4; + + int16 i16; + + struct structDef { + uint ui; + string str; + } + + structDef structDec; + + structDef[3] arrayStructDec; + + int32 i32; + int16 i16_2; + + enum enumDef { + first, + second, + third + } + + enumDef enumDec; + bool boolean; + + uint[][2][][3] ui5; + + string _string; + } +` diff --git a/remix-debug/test/decoder/contracts/miscLocal.js b/remix-debug/test/decoder/contracts/miscLocal.js new file mode 100644 index 0000000000..c6a37b6e09 --- /dev/null +++ b/remix-debug/test/decoder/contracts/miscLocal.js @@ -0,0 +1,39 @@ +'use strict' + +module.exports = { + contract: ` +contract miscLocal { + enum enumDef { + one, + two, + three, + four + } + function miscLocal () { + bool boolFalse = false; + bool boolTrue = true; + enumDef testEnum; + testEnum = enumDef.three; + address sender = msg.sender; + byte _bytes1 = hex"99"; + bytes1 __bytes1 = hex"99"; + bytes2 __bytes2 = hex"99AB"; + bytes4 __bytes4 = hex"99FA"; + bytes6 __bytes6 = hex"99"; + bytes7 __bytes7 = hex"993567"; + bytes8 __bytes8 = hex"99ABD417"; + bytes9 __bytes9 = hex"99156744AF"; + bytes13 __bytes13 = hex"991234234253"; + bytes16 __bytes16 = hex"99AFAD234324"; + bytes24 __bytes24 = hex"99AFAD234324"; + bytes32 __bytes32 = hex"9999ABD41799ABD417"; + } + } + + contract miscLocal2 { + function miscLocal2 () { + bytes memory dynbytes = "dynamicbytes"; + string memory smallstring = "test_test_test"; + } + } +`} diff --git a/remix-debug/test/decoder/contracts/simpleContract.js b/remix-debug/test/decoder/contracts/simpleContract.js new file mode 100644 index 0000000000..bffa2bfe88 --- /dev/null +++ b/remix-debug/test/decoder/contracts/simpleContract.js @@ -0,0 +1,26 @@ +'use strict' +module.exports = ` + contract simpleContract { + struct structDef { + uint8 ui; + string str; + } + enum enumDef { + first, + second, + third + } + structDef structDec; + structDef[3] array; + enumDef enumDec; + } + + contract test1 { + struct str { + } + } + + contract test2 { + test1.str a; + } +` diff --git a/remix-debug/test/decoder/contracts/structArrayLocal.js b/remix-debug/test/decoder/contracts/structArrayLocal.js new file mode 100644 index 0000000000..35bf48ea69 --- /dev/null +++ b/remix-debug/test/decoder/contracts/structArrayLocal.js @@ -0,0 +1,84 @@ +'use strict' + +module.exports = { + contract: ` +contract structArrayLocal { + struct teststruct { + string a; + int b; + string c; + int d; + bool e; + } + + enum enumdef + { + one, + two, + three + } + + struct teststructArray { + string[] a; + int8[3] b; + enumdef c; + } + + function structArrayLocal () { + bytes memory bytesSimple = "test_super"; + teststruct memory e; + e.a = "test"; + e.b = 5; + string memory f = "test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_"; + e.c = "test_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_test"; + e.d = 3; + e.e = true; + + int[5] memory simpleArray; + simpleArray[0] = 45; + simpleArray[1] = 324324; + simpleArray[2] = -333; + simpleArray[3] = 5656; + simpleArray[4] = -1111; + + string[3] memory stringArray; + stringArray[0] = "long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_"; + stringArray[1] = "two"; + stringArray[2] = "three"; + + int[][3] memory dynArray; + dynArray[0] = new int[](1); + dynArray[1] = new int[](2); + dynArray[2] = new int[](3); + dynArray[0][0] = 3423423532; + dynArray[1][0] = -342343323532; + dynArray[1][1] = 23432; + dynArray[2][0] = -432432; + dynArray[2][1] = 3423423532; + dynArray[2][2] = -432432; + + teststruct[3] memory structArray; + structArray[0] = e; + + structArray[1].a = "item1 a"; + structArray[1].b = 20; + structArray[1].c = "item1 c"; + structArray[1].d = -45; + structArray[1].e = false; + + structArray[2].a = "item2 a"; + structArray[2].b = 200; + structArray[2].c = "item2 c"; + structArray[2].d = -450; + structArray[2].e = true; + + teststructArray memory arrayStruct; + arrayStruct.a = new string[](1); + arrayStruct.a[0] = "string"; + arrayStruct.b[0] = 34; + arrayStruct.b[1] = -23; + arrayStruct.b[2] = -3; + arrayStruct.c = enumdef.three; + } +} +`} diff --git a/remix-debug/test/decoder/contracts/structArrayStorage.js b/remix-debug/test/decoder/contracts/structArrayStorage.js new file mode 100644 index 0000000000..7f542733f6 --- /dev/null +++ b/remix-debug/test/decoder/contracts/structArrayStorage.js @@ -0,0 +1,204 @@ +'use strict' + +module.exports = { + contract: `contract structArrayStorage { + struct intStruct { + int8 i8; + int16 i16; + uint32 ui32; + int i256; + uint16 ui16; + int32 i32; + } + intStruct intStructDec; + + int64[7] i5; + + int64[] idyn5; + + int32[][4] dyn1; + + int32[][4][] dyn2; + + struct simpleStruct { + int8 i8; + string str; + } + simpleStruct[][3] arrayStruct; + function structArrayStorage () { + intStructDec.i8 = 32; + intStructDec.i16 = -54; + intStructDec.ui32 = 128; + intStructDec.i256 = -1243565465756; + intStructDec.ui16 = 34556; + intStructDec.i32 = -345446546; + + i5[0] = -2134; + i5[1] = 345; + i5[2] = -3246; + i5[3] = 4357; + i5[4] = 324; + i5[5] = -2344; + i5[6] = 3254; + + idyn5.length = 9; + idyn5[0] = -2134; + idyn5[1] = 345; + idyn5[2] = -3246; + idyn5[3] = 4357; + idyn5[4] = 324; + idyn5[5] = -2344; + idyn5[6] = 3254; + idyn5[7] = -254; + idyn5[8] = -2354; + + dyn1[0].length = 1; + dyn1[1].length = 3; + dyn1[2].length = 10; + dyn1[3].length = 2; + + dyn1[0][0] = 3; + dyn1[1][0] = 12; + dyn1[1][1] = -12; + dyn1[1][2] = -1234; + + dyn1[2][0] = 1; + dyn1[2][1] = 12; + dyn1[2][2] = 1235; + dyn1[2][3] = -12; + dyn1[2][4] = -123456; + dyn1[2][5] = -23435435; + dyn1[2][6] = 543543; + dyn1[2][7] = 2; + dyn1[2][8] = -1; + dyn1[2][9] = 232432; + dyn1[3][0] = 232432; + dyn1[3][1] = 232432; + + dyn2.length = 2; + dyn2[0][0].length = 3; + dyn2[0][1].length = 3; + dyn2[0][2].length = 3; + dyn2[0][3].length = 3; + dyn2[1][0].length = 3; + dyn2[1][1].length = 3; + dyn2[1][2].length = 3; + dyn2[1][3].length = 3; + + dyn2[0][0][0] = 23; + dyn2[0][0][1] = -23; + dyn2[0][0][2] = -28; + + dyn2[0][1][0] = 23; + dyn2[0][1][1] = -23; + dyn2[0][1][2] = -28; + + dyn2[0][2][0] = 23; + dyn2[0][2][1] = -23; + dyn2[0][2][2] = -28; + + dyn2[0][3][0] = 23; + dyn2[0][3][1] = -23; + dyn2[0][3][2] = -28; + + dyn2[1][0][0] = 23; + dyn2[1][0][1] = -23; + dyn2[1][0][2] = -28; + + dyn2[1][1][0] = 23; + dyn2[1][1][1] = -23; + dyn2[1][1][2] = -28; + + dyn2[1][2][0] = 23; + dyn2[1][2][1] = -23; + dyn2[1][2][2] = -28; + + dyn2[1][3][0] = 23; + dyn2[1][3][1] = -23; + dyn2[1][3][2] = -28; + + + arrayStruct[0].length = 2; + arrayStruct[1].length = 1; + arrayStruct[2].length = 3; + + arrayStruct[0][0].i8 = 34; + arrayStruct[0][0].str = 'test_str_short'; + arrayStruct[0][1].i8 = -123; + arrayStruct[0][1].str = 'test_str_long test_str_lo ngtest_str_longtest_str_ longtest_str_longtest_ str_longtest_str_l ongtest_str_long'; + + arrayStruct[1][0].i8 = 50; + arrayStruct[1][0].str = 'test_str_short'; + + arrayStruct[2][0].i8 = 60; + arrayStruct[2][0].str = 'test_str_short'; + arrayStruct[2][1].i8 = 84; + arrayStruct[2][1].str = 'test_str_long test_str_lo ngtest_str_longtest_str_ longtest_str_longtest_ str_longtest_str_l ongtest_str_long'; + arrayStruct[2][2].i8 = -34; + arrayStruct[2][2].str = 'test_str_short'; + + + } +} +`, + storage: { + '0x0000000000000000000000000000000000000000000000000000000000000000': '0x0000000000000000000000000000000000000000000000000000000080ffca20', + '0x0000000000000000000000000000000000000000000000000000000000000001': '0xfffffffffffffffffffffffffffffffffffffffffffffffffffffede75b8df64', + '0x0000000000000000000000000000000000000000000000000000000000000002': '0x0000000000000000000000000000000000000000000000000000eb68e76e86fc', + '0x0000000000000000000000000000000000000000000000000000000000000003': '0x0000000000001105fffffffffffff3520000000000000159fffffffffffff7aa', + '0x0000000000000000000000000000000000000000000000000000000000000004': '0x00000000000000000000000000000cb6fffffffffffff6d80000000000000144', + '0x0000000000000000000000000000000000000000000000000000000000000005': '0x0000000000000000000000000000000000000000000000000000000000000009', + '0x036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db0': '0x0000000000001105fffffffffffff3520000000000000159fffffffffffff7aa', + '0x036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db1': '0xffffffffffffff020000000000000cb6fffffffffffff6d80000000000000144', + '0x036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db2': '0x000000000000000000000000000000000000000000000000fffffffffffff6ce', + '0x0000000000000000000000000000000000000000000000000000000000000006': '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x0000000000000000000000000000000000000000000000000000000000000007': '0x0000000000000000000000000000000000000000000000000000000000000003', + '0x0000000000000000000000000000000000000000000000000000000000000008': '0x000000000000000000000000000000000000000000000000000000000000000a', + '0x0000000000000000000000000000000000000000000000000000000000000009': '0x0000000000000000000000000000000000000000000000000000000000000002', + '0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f': '0x0000000000000000000000000000000000000000000000000000000000000003', + '0xa66cc928b5edb82af9bd49922954155ab7b0942694bea4ce44661d9a8736c688': '0x0000000000000000000000000000000000000000fffffb2efffffff40000000c', + '0xf3f7a9fe364faab93b216da50a3214154f22a0a2b415b23a84c8169e8b636ee3': '0x0000000200084b37fe9a6755fffe1dc0fffffff4000004d30000000c00000001', + '0xf3f7a9fe364faab93b216da50a3214154f22a0a2b415b23a84c8169e8b636ee4': '0x00000000000000000000000000000000000000000000000000038bf0ffffffff', + '0x6e1540171b6c0c960b71a7020d9f60077f6af931a8bbf590da0223dacf75c7af': '0x00000000000000000000000000000000000000000000000000038bf000038bf0', + '0x000000000000000000000000000000000000000000000000000000000000000a': '0x0000000000000000000000000000000000000000000000000000000000000002', + '0xc65a7bb8d6351c1cf70c95a316cc6a92839c986682d98bc35f958f4883f9d2a8': '0x0000000000000000000000000000000000000000000000000000000000000003', + '0xc65a7bb8d6351c1cf70c95a316cc6a92839c986682d98bc35f958f4883f9d2a9': '0x0000000000000000000000000000000000000000000000000000000000000003', + '0xc65a7bb8d6351c1cf70c95a316cc6a92839c986682d98bc35f958f4883f9d2aa': '0x0000000000000000000000000000000000000000000000000000000000000003', + '0xc65a7bb8d6351c1cf70c95a316cc6a92839c986682d98bc35f958f4883f9d2ab': '0x0000000000000000000000000000000000000000000000000000000000000003', + '0xc65a7bb8d6351c1cf70c95a316cc6a92839c986682d98bc35f958f4883f9d2ac': '0x0000000000000000000000000000000000000000000000000000000000000003', + '0xc65a7bb8d6351c1cf70c95a316cc6a92839c986682d98bc35f958f4883f9d2ad': '0x0000000000000000000000000000000000000000000000000000000000000003', + '0xc65a7bb8d6351c1cf70c95a316cc6a92839c986682d98bc35f958f4883f9d2ae': '0x0000000000000000000000000000000000000000000000000000000000000003', + '0xc65a7bb8d6351c1cf70c95a316cc6a92839c986682d98bc35f958f4883f9d2af': '0x0000000000000000000000000000000000000000000000000000000000000003', + '0x410c2796757c1866e144712b649ab035b22d7295530f125d2b7bc17fa7b793b5': '0x0000000000000000000000000000000000000000ffffffe4ffffffe900000017', + '0x06b493c1ca289c5326ef56c162cd187bf96c737c2c9bbda318cc345be15042af': '0x0000000000000000000000000000000000000000ffffffe4ffffffe900000017', + '0x7a0b543a77c72a2154fae01417d93ab4a7f07c9a6bbce5febfeb9904a41b7914': '0x0000000000000000000000000000000000000000ffffffe4ffffffe900000017', + '0x5370eae143cc2f6260640bd734b0cdaf587bbcfc81362df39d56d5a29a7e663b': '0x0000000000000000000000000000000000000000ffffffe4ffffffe900000017', + '0xd5211e5652076f058928f5b24e1816690291c298b337ea927f8d0f3aabb8a05a': '0x0000000000000000000000000000000000000000ffffffe4ffffffe900000017', + '0xf232dee5d9edbb879fab95c81a3867fe42b8d79b05e9c99336c5297487f94e8d': '0x0000000000000000000000000000000000000000ffffffe4ffffffe900000017', + '0x8a82e6d20ae2c2a82dd8e575dac6354ce964fd35e3d1cdb79bb1757c6a7675b6': '0x0000000000000000000000000000000000000000ffffffe4ffffffe900000017', + '0xa9e6724ab7d0ccf2de69222bc5703c9df2049038736e6d57f437315272b76a3a': '0x0000000000000000000000000000000000000000ffffffe4ffffffe900000017', + '0x000000000000000000000000000000000000000000000000000000000000000b': '0x0000000000000000000000000000000000000000000000000000000000000002', + '0x000000000000000000000000000000000000000000000000000000000000000c': '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x000000000000000000000000000000000000000000000000000000000000000d': '0x0000000000000000000000000000000000000000000000000000000000000003', + '0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9': '0x0000000000000000000000000000000000000000000000000000000000000022', + '0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dba': '0x746573745f7374725f73686f727400000000000000000000000000000000001c', + '0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbb': '0x0000000000000000000000000000000000000000000000000000000000000085', + '0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbc': '0x00000000000000000000000000000000000000000000000000000000000000db', + '0x40abea1508c7557b93b3e219e777ce8530b60f9f8452ef1c627dbc62b53708fc': '0x746573745f7374725f6c6f6e6720746573745f7374725f6c6f206e6774657374', + '0x40abea1508c7557b93b3e219e777ce8530b60f9f8452ef1c627dbc62b53708fd': '0x5f7374725f6c6f6e67746573745f7374725f206c6f6e67746573745f7374725f', + '0x40abea1508c7557b93b3e219e777ce8530b60f9f8452ef1c627dbc62b53708fe': '0x6c6f6e67746573745f207374725f6c6f6e67746573745f7374725f6c206f6e67', + '0x40abea1508c7557b93b3e219e777ce8530b60f9f8452ef1c627dbc62b53708ff': '0x746573745f7374725f6c6f6e6700000000000000000000000000000000000000', + '0xdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c7': '0x0000000000000000000000000000000000000000000000000000000000000032', + '0xdf6966c971051c3d54ec59162606531493a51404a002842f56009d7e5cf4a8c8': '0x746573745f7374725f73686f727400000000000000000000000000000000001c', + '0xd7b6990105719101dabeb77144f2a3385c8033acd3af97e9423a695e81ad1eb5': '0x000000000000000000000000000000000000000000000000000000000000003c', + '0xd7b6990105719101dabeb77144f2a3385c8033acd3af97e9423a695e81ad1eb6': '0x746573745f7374725f73686f727400000000000000000000000000000000001c', + '0xd7b6990105719101dabeb77144f2a3385c8033acd3af97e9423a695e81ad1eb7': '0x0000000000000000000000000000000000000000000000000000000000000054', + '0xd7b6990105719101dabeb77144f2a3385c8033acd3af97e9423a695e81ad1eb8': '0x00000000000000000000000000000000000000000000000000000000000000db', + '0x87466a1ae97409dd9d9cd9368751b439509d8b3f8fc2bb47a4264e5d6fd4d324': '0x746573745f7374725f6c6f6e6720746573745f7374725f6c6f206e6774657374', + '0x87466a1ae97409dd9d9cd9368751b439509d8b3f8fc2bb47a4264e5d6fd4d325': '0x5f7374725f6c6f6e67746573745f7374725f206c6f6e67746573745f7374725f', + '0x87466a1ae97409dd9d9cd9368751b439509d8b3f8fc2bb47a4264e5d6fd4d326': '0x6c6f6e67746573745f207374725f6c6f6e67746573745f7374725f6c206f6e67', + '0x87466a1ae97409dd9d9cd9368751b439509d8b3f8fc2bb47a4264e5d6fd4d327': '0x746573745f7374725f6c6f6e6700000000000000000000000000000000000000', + '0xd7b6990105719101dabeb77144f2a3385c8033acd3af97e9423a695e81ad1eb9': '0x00000000000000000000000000000000000000000000000000000000000000de', + '0xd7b6990105719101dabeb77144f2a3385c8033acd3af97e9423a695e81ad1eba': '0x746573745f7374725f73686f727400000000000000000000000000000000001c' + } +} diff --git a/remix-debug/test/decoder/decodeInfo.js b/remix-debug/test/decoder/decodeInfo.js new file mode 100644 index 0000000000..4e100f2b79 --- /dev/null +++ b/remix-debug/test/decoder/decodeInfo.js @@ -0,0 +1,97 @@ +'use strict' +var tape = require('tape') +var compiler = require('solc') +var astHelper = require('../../src/decoder/astHelper') +var decodeInfo = require('../../src/decoder/decodeInfo') +var stateDecoder = require('../../src/decoder/stateDecoder') +var contracts = require('./contracts/miscContracts') +var simplecontracts = require('./contracts/simpleContract') +var remixLib = require('remix-lib') +var compilerInput = remixLib.helpers.compiler.compilerInput +var util = require('../../src/decoder/types/util') + +tape('solidity', function (t) { + t.test('astHelper, decodeInfo', function (st) { + var output = compiler.compileStandardWrapper(compilerInput(contracts)) + output = JSON.parse(output) + + var state = astHelper.extractStateDefinitions('test.sol:contractUint', output.sources) + var states = astHelper.extractStatesDefinitions(output.sources) + var stateDef = state.stateDefinitions + var parsedType = decodeInfo.parseType(stateDef[0].attributes.type, states, 'contractUint', util.extractLocationFromAstVariable(stateDef[0])) + checkDecodeInfo(st, parsedType, 1, 1, 'uint8') + parsedType = decodeInfo.parseType(stateDef[2].attributes.type, states, 'contractUint', util.extractLocationFromAstVariable(stateDef[2])) + checkDecodeInfo(st, parsedType, 1, 32, 'uint256') + parsedType = decodeInfo.parseType(stateDef[3].attributes.type, states, 'contractUint', util.extractLocationFromAstVariable(stateDef[3])) + checkDecodeInfo(st, parsedType, 1, 32, 'uint256') + parsedType = decodeInfo.parseType(stateDef[4].attributes.type, states, 'contractUint', util.extractLocationFromAstVariable(stateDef[4])) + checkDecodeInfo(st, parsedType, 1, 16, 'bytes16') + + state = astHelper.extractStateDefinitions('test.sol:contractStructAndArray', output.sources) + stateDef = state.stateDefinitions + parsedType = decodeInfo.parseType(stateDef[1].attributes.type, states, 'contractStructAndArray', util.extractLocationFromAstVariable(stateDef[1])) + checkDecodeInfo(st, parsedType, 2, 32, 'struct contractStructAndArray.structDef') + parsedType = decodeInfo.parseType(stateDef[2].attributes.type, states, 'contractStructAndArray', util.extractLocationFromAstVariable(stateDef[2])) + checkDecodeInfo(st, parsedType, 6, 32, 'struct contractStructAndArray.structDef[3]') + parsedType = decodeInfo.parseType(stateDef[3].attributes.type, states, 'contractStructAndArray', util.extractLocationFromAstVariable(stateDef[3])) + checkDecodeInfo(st, parsedType, 2, 32, 'bytes12[4]') + + state = astHelper.extractStateDefinitions('test.sol:contractArray', output.sources) + stateDef = state.stateDefinitions + parsedType = decodeInfo.parseType(stateDef[0].attributes.type, states, 'contractArray', util.extractLocationFromAstVariable(stateDef[0])) + checkDecodeInfo(st, parsedType, 1, 32, 'uint32[5]') + parsedType = decodeInfo.parseType(stateDef[1].attributes.type, states, 'contractArray', util.extractLocationFromAstVariable(stateDef[1])) + checkDecodeInfo(st, parsedType, 1, 32, 'int8[]') + parsedType = decodeInfo.parseType(stateDef[2].attributes.type, states, 'contractArray', util.extractLocationFromAstVariable(stateDef[2])) + checkDecodeInfo(st, parsedType, 4, 32, 'int16[][3][][4]') + + state = astHelper.extractStateDefinitions('test.sol:contractEnum', output.sources) + stateDef = state.stateDefinitions + parsedType = decodeInfo.parseType(stateDef[1].attributes.type, states, 'contractEnum') + checkDecodeInfo(st, parsedType, 1, 2, 'enum') + + state = astHelper.extractStateDefinitions('test.sol:contractSmallVariable', output.sources) + stateDef = state.stateDefinitions + parsedType = decodeInfo.parseType(stateDef[0].attributes.type, states, 'contractSmallVariable', util.extractLocationFromAstVariable(stateDef[0])) + checkDecodeInfo(st, parsedType, 1, 1, 'int8') + parsedType = decodeInfo.parseType(stateDef[1].attributes.type, states, 'contractSmallVariable', util.extractLocationFromAstVariable(stateDef[1])) + checkDecodeInfo(st, parsedType, 1, 1, 'uint8') + parsedType = decodeInfo.parseType(stateDef[2].attributes.type, states, 'contractSmallVariable', util.extractLocationFromAstVariable(stateDef[2])) + checkDecodeInfo(st, parsedType, 1, 2, 'uint16') + parsedType = decodeInfo.parseType(stateDef[3].attributes.type, states, 'contractSmallVariable', util.extractLocationFromAstVariable(stateDef[3])) + checkDecodeInfo(st, parsedType, 1, 4, 'int32') + parsedType = decodeInfo.parseType(stateDef[4].attributes.type, states, 'contractSmallVariable', util.extractLocationFromAstVariable(stateDef[4])) + checkDecodeInfo(st, parsedType, 1, 32, 'uint256') + parsedType = decodeInfo.parseType(stateDef[5].attributes.type, states, 'contractSmallVariable', util.extractLocationFromAstVariable(stateDef[5])) + checkDecodeInfo(st, parsedType, 1, 2, 'int16') + + output = compiler.compileStandardWrapper(compilerInput(simplecontracts)) + output = JSON.parse(output) + + state = astHelper.extractStateDefinitions('test.sol:simpleContract', output.sources) + states = astHelper.extractStatesDefinitions(output.sources) + stateDef = state.stateDefinitions + parsedType = decodeInfo.parseType(stateDef[2].attributes.type, states, 'simpleContract', util.extractLocationFromAstVariable(stateDef[2])) + checkDecodeInfo(st, parsedType, 2, 32, 'struct simpleContract.structDef') + parsedType = decodeInfo.parseType(stateDef[3].attributes.type, states, 'simpleContract', util.extractLocationFromAstVariable(stateDef[3])) + checkDecodeInfo(st, parsedType, 6, 32, 'struct simpleContract.structDef[3]') + parsedType = decodeInfo.parseType(stateDef[4].attributes.type, states, 'simpleContract', util.extractLocationFromAstVariable(stateDef[4])) + checkDecodeInfo(st, parsedType, 1, 1, 'enum') + + state = astHelper.extractStateDefinitions('test.sol:test2', output.sources) + stateDef = state.stateDefinitions + parsedType = decodeInfo.parseType(stateDef[0].attributes.type, states, 'test1', util.extractLocationFromAstVariable(stateDef[0])) + checkDecodeInfo(st, parsedType, 0, 32, 'struct test1.str') + + state = stateDecoder.extractStateVariables('test.sol:test2', output.sources) + checkDecodeInfo(st, parsedType, 0, 32, 'struct test1.str') + + st.end() + }) +}) + +function checkDecodeInfo (st, decodeInfo, storageSlots, storageBytes, typeName) { + st.equal(decodeInfo.storageSlots, storageSlots) + st.equal(decodeInfo.storageBytes, storageBytes) + st.equal(decodeInfo.typeName, typeName) +} diff --git a/remix-debug/test/decoder/localDecoder.js b/remix-debug/test/decoder/localDecoder.js new file mode 100644 index 0000000000..53d5006a46 --- /dev/null +++ b/remix-debug/test/decoder/localDecoder.js @@ -0,0 +1,41 @@ +'use strict' +var tape = require('tape') +var compiler = require('solc') +var intLocal = require('./contracts/intLocal') +var miscLocal = require('./contracts/miscLocal') +var structArrayLocal = require('./contracts/structArrayLocal') +var remixLib = require('remix-lib') +var vmCall = require('./vmCall') +var intLocalTest = require('./localsTests/int') +var miscLocalTest = require('./localsTests/misc') +var misc2LocalTest = require('./localsTests/misc2') +var structArrayLocalTest = require('./localsTests/structArray') +var compilerInput = remixLib.helpers.compiler.compilerInput + +tape('solidity', function (t) { + t.test('local decoder', function (st) { + var privateKey = new Buffer('dae9801649ba2d95a21e688b56f77905e5667c44ce868ec83f82e838712a2c7a', 'hex') + var vm = vmCall.initVM(st, privateKey) + test(st, vm, privateKey) + }) +}) + +function test (st, vm, privateKey) { + var output = compiler.compileStandardWrapper(compilerInput(intLocal.contract)) + output = JSON.parse(output) + intLocalTest(st, vm, privateKey, output.contracts['test.sol']['intLocal'].evm.bytecode.object, output, function () { + output = compiler.compileStandardWrapper(compilerInput(miscLocal.contract)) + output = JSON.parse(output) + miscLocalTest(st, vm, privateKey, output.contracts['test.sol']['miscLocal'].evm.bytecode.object, output, function () { + output = compiler.compileStandardWrapper(compilerInput(miscLocal.contract)) + output = JSON.parse(output) + misc2LocalTest(st, vm, privateKey, output.contracts['test.sol']['miscLocal2'].evm.bytecode.object, output, function () { + output = compiler.compileStandardWrapper(compilerInput(structArrayLocal.contract)) + output = JSON.parse(output) + structArrayLocalTest(st, vm, privateKey, output.contracts['test.sol']['structArrayLocal'].evm.bytecode.object, output, function () { + st.end() + }) + }) + }) + }) +} diff --git a/remix-debug/test/decoder/localsTests/helper.js b/remix-debug/test/decoder/localsTests/helper.js new file mode 100644 index 0000000000..867f515d85 --- /dev/null +++ b/remix-debug/test/decoder/localsTests/helper.js @@ -0,0 +1,29 @@ +'use strict' +var localDecoder = require('../../../src/decoder/localDecoder') + +/* + Decode local variable +*/ +function decodeLocal (st, index, traceManager, callTree, verifier) { + try { + traceManager.waterfall([ + traceManager.getStackAt, + traceManager.getMemoryAt], + index, + function (error, result) { + if (!error) { + localDecoder.solidityLocals(index, callTree, result[0].value, result[1].value, {}, {start: 5000}).then((locals) => { + verifier(locals) + }) + } else { + st.fail(error) + } + }) + } catch (e) { + st.fail(e.message) + } +} + +module.exports = { + decodeLocals: decodeLocal +} diff --git a/remix-debug/test/decoder/localsTests/int.js b/remix-debug/test/decoder/localsTests/int.js new file mode 100644 index 0000000000..db3a35f6c8 --- /dev/null +++ b/remix-debug/test/decoder/localsTests/int.js @@ -0,0 +1,112 @@ +'use strict' + +var TraceManager = require('../../../src/trace/traceManager') +var CodeManager = require('../../../src/code/codeManager') + +var vmCall = require('../vmCall') +var remixLib = require('remix-lib') + +var traceHelper = remixLib.helpers.trace +var SolidityProxy = require('../../../src/decoder/solidityProxy') +var InternalCallTree = require('../../../src/decoder/internalCallTree') +var EventManager = remixLib.EventManager +var helper = require('./helper') + +module.exports = function (st, vm, privateKey, contractBytecode, compilationResult, cb) { + vmCall.sendTx(vm, {nonce: 0, privateKey: privateKey}, null, 0, contractBytecode, function (error, txHash) { + if (error) { + st.fail(error) + } else { + vm.web3.eth.getTransaction(txHash, function (error, tx) { + if (error) { + st.fail(error) + } else { + tx.to = traceHelper.contractCreationToken('0') + var traceManager = new TraceManager({web3: vm.web3}) + var codeManager = new CodeManager(traceManager) + codeManager.clear() + var solidityProxy = new SolidityProxy(traceManager, codeManager) + solidityProxy.reset(compilationResult) + var debuggerEvent = new EventManager() + var callTree = new InternalCallTree(debuggerEvent, traceManager, solidityProxy, codeManager, { includeLocalVariables: true }) + callTree.event.register('callTreeBuildFailed', (error) => { + st.fail(error) + }) + callTree.event.register('callTreeNotReady', (reason) => { + st.fail(reason) + }) + callTree.event.register('callTreeReady', (scopes, scopeStarts) => { + try { + st.equals(scopeStarts[0], '') + st.equals(scopeStarts[13], '1') + st.equals(scopeStarts[103], '2') + st.equals(scopeStarts[116], '2.1') + st.equals(scopeStarts[135], '3') + st.equals(scopeStarts[151], '4') + st.equals(scopeStarts[164], '4.1') + st.equals(scopes[''].locals['ui8'].type.typeName, 'uint8') + st.equals(scopes[''].locals['ui16'].type.typeName, 'uint16') + st.equals(scopes[''].locals['ui32'].type.typeName, 'uint32') + st.equals(scopes[''].locals['ui64'].type.typeName, 'uint64') + st.equals(scopes[''].locals['ui128'].type.typeName, 'uint128') + st.equals(scopes[''].locals['ui256'].type.typeName, 'uint256') + st.equals(scopes[''].locals['ui'].type.typeName, 'uint256') + st.equals(scopes[''].locals['i8'].type.typeName, 'int8') + st.equals(scopes[''].locals['i16'].type.typeName, 'int16') + st.equals(scopes[''].locals['i32'].type.typeName, 'int32') + st.equals(scopes[''].locals['i64'].type.typeName, 'int64') + st.equals(scopes[''].locals['i128'].type.typeName, 'int128') + st.equals(scopes[''].locals['i256'].type.typeName, 'int256') + st.equals(scopes[''].locals['i'].type.typeName, 'int256') + st.equals(scopes[''].locals['ishrink'].type.typeName, 'int32') + st.equals(scopes['2'].locals['ui8'].type.typeName, 'uint8') + st.equals(scopes['2.1'].locals['ui81'].type.typeName, 'uint8') + st.equals(scopes['3'].locals['ui81'].type.typeName, 'uint8') + st.equals(scopes['4'].locals['ui8'].type.typeName, 'uint8') + st.equals(scopes['4.1'].locals['ui81'].type.typeName, 'uint8') + } catch (e) { + st.fail(e.message) + } + + helper.decodeLocals(st, 95, traceManager, callTree, function (locals) { + st.equals(Object.keys(locals).length, 16) + st.equals(locals['ui8'].value, '130') + st.equals(locals['ui16'].value, '456') + st.equals(locals['ui32'].value, '4356') + st.equals(locals['ui64'].value, '3543543543') + st.equals(locals['ui128'].value, '234567') + st.equals(locals['ui256'].value, '115792089237316195423570985008687907853269984665640564039457584007880697216513') + st.equals(locals['ui'].value, '123545666') + st.equals(locals['i8'].value, '-45') + st.equals(locals['i16'].value, '-1234') + st.equals(locals['i32'].value, '3455') + st.equals(locals['i64'].value, '-35566') + st.equals(locals['i128'].value, '-444444') + st.equals(locals['i256'].value, '3434343') + st.equals(locals['i'].value, '-32432423423') + st.equals(locals['ishrink'].value, '2') + }) + + helper.decodeLocals(st, 171, traceManager, callTree, function (locals) { + try { + st.equals(locals['ui8'].value, '123') + st.equals(Object.keys(locals).length, 1) + } catch (e) { + st.fail(e.message) + } + cb() + }) + }) + traceManager.resolveTrace(tx, (error, result) => { + if (error) { + st.fail(error) + } else { + debuggerEvent.trigger('newTraceLoaded', [traceManager.trace]) + } + }) + } + }) + } + }) +} + diff --git a/remix-debug/test/decoder/localsTests/misc.js b/remix-debug/test/decoder/localsTests/misc.js new file mode 100644 index 0000000000..9285ab31c8 --- /dev/null +++ b/remix-debug/test/decoder/localsTests/misc.js @@ -0,0 +1,78 @@ +'use strict' +var TraceManager = require('../../../src/trace/traceManager') +var CodeManager = require('../../../src/code/codeManager') +var vmCall = require('../vmCall') +var remixLib = require('remix-lib') +var traceHelper = remixLib.helpers.trace +var SolidityProxy = require('../../../src/decoder/solidityProxy') +var InternalCallTree = require('../../../src/decoder/internalCallTree') +var EventManager = remixLib.EventManager +var helper = require('./helper') + +module.exports = function (st, vm, privateKey, contractBytecode, compilationResult, cb) { + vmCall.sendTx(vm, {nonce: 0, privateKey: privateKey}, null, 0, contractBytecode, function (error, txHash) { + if (error) { + st.fail(error) + } else { + vm.web3.eth.getTransaction(txHash, function (error, tx) { + if (error) { + st.fail(error) + } else { + tx.to = traceHelper.contractCreationToken('0') + var traceManager = new TraceManager({web3: vm.web3}) + var codeManager = new CodeManager(traceManager) + codeManager.clear() + var solidityProxy = new SolidityProxy(traceManager, codeManager) + solidityProxy.reset(compilationResult) + var debuggerEvent = new EventManager() + var callTree = new InternalCallTree(debuggerEvent, traceManager, solidityProxy, codeManager, { includeLocalVariables: true }) + callTree.event.register('callTreeBuildFailed', (error) => { + st.fail(error) + }) + callTree.event.register('callTreeReady', (scopes, scopeStarts) => { + helper.decodeLocals(st, 73, traceManager, callTree, function (locals) { + try { + st.equals(locals['boolFalse'].value, false) + st.equals(locals['boolTrue'].value, true) + st.equals(locals['testEnum'].value, 'three') + st.equals(locals['sender'].value, '0x4B0897B0513FDC7C541B6D9D7E929C4E5364D2DB') + st.equals(locals['_bytes1'].value, '0x99') + st.equals(locals['__bytes1'].value, '0x99') + st.equals(locals['__bytes2'].value, '0x99AB') + st.equals(locals['__bytes4'].value, '0x99FA0000') + st.equals(locals['__bytes6'].value, '0x990000000000') + st.equals(locals['__bytes7'].value, '0x99356700000000') + st.equals(locals['__bytes8'].value, '0x99ABD41700000000') + st.equals(locals['__bytes9'].value, '0x99156744AF00000000') + st.equals(locals['__bytes13'].value, '0x99123423425300000000000000') + st.equals(locals['__bytes16'].value, '0x99AFAD23432400000000000000000000') + st.equals(locals['__bytes24'].value, '0x99AFAD234324000000000000000000000000000000000000') + st.equals(locals['__bytes32'].value, '0x9999ABD41799ABD4170000000000000000000000000000000000000000000000') + st.equals(Object.keys(locals).length, 16) + } catch (e) { + st.fail(e.message) + } + }) + + helper.decodeLocals(st, 7, traceManager, callTree, function (locals) { + try { + // st.equals(Object.keys(locals).length, 0) + st.equals(0, 0) + } catch (e) { + st.fail(e.message) + } + cb() + }) + }) + traceManager.resolveTrace(tx, (error, result) => { + if (error) { + st.fail(error) + } else { + debuggerEvent.trigger('newTraceLoaded', [traceManager.trace]) + } + }) + } + }) + } + }) +} diff --git a/remix-debug/test/decoder/localsTests/misc2.js b/remix-debug/test/decoder/localsTests/misc2.js new file mode 100644 index 0000000000..19b69eba04 --- /dev/null +++ b/remix-debug/test/decoder/localsTests/misc2.js @@ -0,0 +1,64 @@ +'use strict' +var TraceManager = require('../../../src/trace/traceManager') +var CodeManager = require('../../../src/code/codeManager') +var vmCall = require('../vmCall') +var remixLib = require('remix-lib') +var traceHelper = remixLib.helpers.trace +var SolidityProxy = require('../../../src/decoder/solidityProxy') +var InternalCallTree = require('../../../src/decoder/internalCallTree') +var EventManager = remixLib.EventManager +var helper = require('./helper') + +module.exports = function (st, vm, privateKey, contractBytecode, compilationResult, cb) { + vmCall.sendTx(vm, {nonce: 0, privateKey: privateKey}, null, 0, contractBytecode, function (error, txHash) { + if (error) { + st.fail(error) + } else { + vm.web3.eth.getTransaction(txHash, function (error, tx) { + if (error) { + st.fail(error) + } else { + tx.to = traceHelper.contractCreationToken('0') + var traceManager = new TraceManager({web3: vm.web3}) + var codeManager = new CodeManager(traceManager) + codeManager.clear() + var solidityProxy = new SolidityProxy(traceManager, codeManager) + solidityProxy.reset(compilationResult) + var debuggerEvent = new EventManager() + var callTree = new InternalCallTree(debuggerEvent, traceManager, solidityProxy, codeManager, { includeLocalVariables: true }) + callTree.event.register('callTreeBuildFailed', (error) => { + st.fail(error) + }) + callTree.event.register('callTreeReady', (scopes, scopeStarts) => { + helper.decodeLocals(st, 51, traceManager, callTree, function (locals) { + try { + st.equals(locals['dynbytes'].value, '0x64796e616d69636279746573') + st.equals(locals['smallstring'].value, 'test_test_test') + st.equals(Object.keys(locals).length, 2) + } catch (e) { + st.fail(e.message) + } + }) + + helper.decodeLocals(st, 7, traceManager, callTree, function (locals) { + try { + // st.equals(Object.keys(locals).length, 0) + st.equals(0, 0) + } catch (e) { + st.fail(e.message) + } + cb() + }) + }) + traceManager.resolveTrace(tx, (error, result) => { + if (error) { + st.fail(error) + } else { + debuggerEvent.trigger('newTraceLoaded', [traceManager.trace]) + } + }) + } + }) + } + }) +} diff --git a/remix-debug/test/decoder/localsTests/structArray.js b/remix-debug/test/decoder/localsTests/structArray.js new file mode 100644 index 0000000000..984a341546 --- /dev/null +++ b/remix-debug/test/decoder/localsTests/structArray.js @@ -0,0 +1,122 @@ +'use strict' +var TraceManager = require('../../../src/trace/traceManager') +var CodeManager = require('../../../src/code/codeManager') +var vmCall = require('../vmCall') +var remixLib = require('remix-lib') +var traceHelper = remixLib.helpers.trace +var SolidityProxy = require('../../../src/decoder/solidityProxy') +var InternalCallTree = require('../../../src/decoder/internalCallTree') +var EventManager = remixLib.EventManager +var helper = require('./helper') + +module.exports = function (st, vm, privateKey, contractBytecode, compilationResult, cb) { + vmCall.sendTx(vm, {nonce: 0, privateKey: privateKey}, null, 0, contractBytecode, function (error, txHash) { + if (error) { + st.fail(error) + } else { + vm.web3.eth.getTransaction(txHash, function (error, tx) { + if (error) { + st.fail(error) + } else { + tx.to = traceHelper.contractCreationToken('0') + var traceManager = new TraceManager({web3: vm.web3}) + var codeManager = new CodeManager(traceManager) + codeManager.clear() + var solidityProxy = new SolidityProxy(traceManager, codeManager) + solidityProxy.reset(compilationResult) + var debuggerEvent = new EventManager() + var callTree = new InternalCallTree(debuggerEvent, traceManager, solidityProxy, codeManager, { includeLocalVariables: true }) + callTree.event.register('callTreeBuildFailed', (error) => { + st.fail(error) + }) + callTree.event.register('callTreeReady', (scopes, scopeStarts) => { + helper.decodeLocals(st, 1699, traceManager, callTree, function (locals) { + try { + st.equals(locals['bytesSimple'].length, '0x14') + st.equals(locals['bytesSimple'].value, '0x746573745f7375706572') + st.equals(locals['e'].value['a'].value, 'test') + st.equals(locals['e'].value['a'].length, '0x8') + st.equals(locals['e'].value['a'].raw, '0x74657374') + st.equals(locals['e'].value['b'].value, '5') + st.equals(locals['e'].value['c'].length, '0x220') + st.equals(locals['e'].value['c'].raw, '0x746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374') + st.equals(locals['e'].value['c'].value, 'test_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_test') + st.equals(locals['e'].value['d'].value, '3') + st.equals(locals['f'].length, '0x1b8') + st.equals(locals['f'].raw, '0x746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f') + st.equals(locals['f'].value, 'test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_test_long_') + st.equals(locals['e'].value['e'].value, true) + + st.equals(locals['simpleArray'].value[0].value, '45') + st.equals(locals['simpleArray'].value[1].value, '324324') + st.equals(locals['simpleArray'].value[2].value, '-333') + st.equals(locals['simpleArray'].value[3].value, '5656') + st.equals(locals['simpleArray'].value[4].value, '-1111') + + st.equals(locals['stringArray'].value[0].value, 'long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_long_one_') + st.equals(locals['stringArray'].value[1].value, 'two') + st.equals(locals['stringArray'].value[2].value, 'three') + + st.equals(locals['dynArray'].value[0].value[0].value, '3423423532') + st.equals(locals['dynArray'].value[1].value[0].value, '-342343323532') + st.equals(locals['dynArray'].value[1].value[1].value, '23432') + st.equals(locals['dynArray'].value[2].value[0].value, '-432432') + st.equals(locals['dynArray'].value[2].value[1].value, '3423423532') + st.equals(locals['dynArray'].value[2].value[2].value, '-432432') + + st.equals(locals['structArray'].value[0].value['a'].value, 'test') + st.equals(locals['structArray'].value[0].value['a'].length, '0x8') + st.equals(locals['structArray'].value[0].value['a'].raw, '0x74657374') + st.equals(locals['structArray'].value[0].value['b'].value, '5') + st.equals(locals['structArray'].value[0].value['c'].length, '0x220') + st.equals(locals['structArray'].value[0].value['c'].raw, '0x746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374746573745f6c6f6e675f746573745f6c6f6e675f746573745f6c6f6e675f74657374') + st.equals(locals['structArray'].value[0].value['c'].value, 'test_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_testtest_long_test_long_test_long_test') + st.equals(locals['structArray'].value[0].value['d'].value, '3') + st.equals(locals['structArray'].value[0].value['e'].value, true) + + st.equals(locals['structArray'].value[1].value['a'].value, 'item1 a') + st.equals(locals['structArray'].value[1].value['b'].value, '20') + st.equals(locals['structArray'].value[1].value['c'].value, 'item1 c') + st.equals(locals['structArray'].value[1].value['d'].value, '-45') + st.equals(locals['structArray'].value[1].value['e'].value, false) + + st.equals(locals['structArray'].value[2].value['a'].value, 'item2 a') + st.equals(locals['structArray'].value[2].value['b'].value, '200') + st.equals(locals['structArray'].value[2].value['c'].value, 'item2 c') + st.equals(locals['structArray'].value[2].value['d'].value, '-450') + st.equals(locals['structArray'].value[2].value['e'].value, true) + + st.equals(locals['arrayStruct'].value.a.value[0].value, 'string') + st.equals(locals['arrayStruct'].value.b.value[0].value, '34') + st.equals(locals['arrayStruct'].value.b.value[1].value, '-23') + st.equals(locals['arrayStruct'].value.b.value[2].value, '-3') + st.equals(locals['arrayStruct'].value.c.value, 'three') + + st.equals(Object.keys(locals).length, 8) + } catch (e) { + st.fail(e.message) + } + }) + + helper.decodeLocals(st, 7, traceManager, callTree, function (locals) { + try { + st.equals(0, 0) + // st.equals(Object.keys(locals).length, 0) + } catch (e) { + st.fail(e.message) + } + cb() + }) + }) + traceManager.resolveTrace(tx, (error, result) => { + if (error) { + st.fail(error) + } else { + debuggerEvent.trigger('newTraceLoaded', [traceManager.trace]) + } + }) + } + }) + } + }) +} diff --git a/remix-debug/test/decoder/mockStorageResolver.js b/remix-debug/test/decoder/mockStorageResolver.js new file mode 100644 index 0000000000..a41d4f32f8 --- /dev/null +++ b/remix-debug/test/decoder/mockStorageResolver.js @@ -0,0 +1,39 @@ +'use strict' +var remixLib = require('remix-lib') +var util = remixLib.util + +class MockStorageResolver { + constructor (_storage) { + this.storage = {} + for (var k in _storage) { + var hashed = util.sha3_256(k) + this.storage[hashed] = { + hashed: hashed, + key: k, + value: _storage[k] + } + } + } + + storageRange (callback) { + callback(null, this.storage) + } + + storageSlot (slot, callback) { + var hashed = util.sha3_256(slot) + callback(null, this.storage[hashed]) + } + + isComplete (address) { + return true + } + + fromCache (address, slotKey) { + return this.storage[slotKey] + } + + toCache (address, storage, complete) { + } +} + +module.exports = MockStorageResolver diff --git a/remix-debug/test/decoder/stateTests/mapping.js b/remix-debug/test/decoder/stateTests/mapping.js new file mode 100644 index 0000000000..ed14992837 --- /dev/null +++ b/remix-debug/test/decoder/stateTests/mapping.js @@ -0,0 +1,75 @@ +var remixLib = require('remix-lib') +var compilerInput = remixLib.helpers.compiler.compilerInput +var compiler = require('solc') +var stateDecoder = require('../../../src/decoder/stateDecoder') +var vmCall = require('../vmCall') + +var TraceManager = require('../../../src/trace/traceManager') +var StorageResolver = require('../../../src/storage/storageResolver') +var StorageViewer = require('../../../src/storage/storageViewer') + +module.exports = function testMappingStorage (st, cb) { + var mappingStorage = require('../contracts/mappingStorage') + var privateKey = new Buffer('dae9801649ba2d95a21e688b56f77905e5667c44ce868ec83f82e838712a2c7a', 'hex') + var vm = vmCall.initVM(st, privateKey) + var output = compiler.compileStandardWrapper(compilerInput(mappingStorage.contract)) + output = JSON.parse(output) + vmCall.sendTx(vm, {nonce: 0, privateKey: privateKey}, null, 0, output.contracts['test.sol']['SimpleMappingState'].evm.bytecode.object, function (error, txHash) { + if (error) { + console.log(error) + st.end(error) + } else { + vm.web3.eth.getTransaction(txHash, (error, tx) => { + if (error) { + console.log(error) + st.end(error) + } else { + testMapping(st, vm, privateKey, tx.contractAddress, output, cb) + } + }) + } + }) +} + +function testMapping (st, vm, privateKey, contractAddress, output, cb) { + vmCall.sendTx(vm, {nonce: 1, privateKey: privateKey}, contractAddress, 0, '2fd0a83a00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001074686973206973206120737472696e6700000000000000000000000000000000', + function (error, txHash) { + if (error) { + console.log(error) + st.end(error) + } else { + console.log(txHash) + vm.web3.eth.getTransaction(txHash, (error, tx) => { + if (error) { + console.log(error) + st.end(error) + } else { + var traceManager = new TraceManager({web3: vm.web3}) + traceManager.resolveTrace(tx, () => { + var storageViewer = new StorageViewer({ + stepIndex: 213, + tx: tx, + address: contractAddress + }, new StorageResolver({web3: vm.web3}), traceManager) + var stateVars = stateDecoder.extractStateVariables('SimpleMappingState', output.sources) + stateDecoder.decodeState(stateVars, storageViewer).then((result) => { + console.log('ok', JSON.stringify(result)) + st.equal(result['_num'].value, '1') + st.equal(result['_num'].type, 'uint256') + st.equal(result['_iBreakSolidityState'].type, 'mapping(string => uint256)') + st.equal(result['_iBreakSolidityState'].value['74686973206973206120737472696e67'].value, '1') + st.equal(result['_iBreakSolidityState'].value['74686973206973206120737472696e67'].type, 'uint256') + st.equal(result['_iBreakSolidityStateInt'].type, 'mapping(uint256 => uint256)') + st.equal(result['_iBreakSolidityStateInt'].value['0000000000000000000000000000000000000000000000000000000000000001'].value, '1') + st.equal(result['_iBreakSolidityStateInt'].value['0000000000000000000000000000000000000000000000000000000000000001'].type, 'uint256') + cb() + }, (reason) => { + console.log('fail') + st.end(reason) + }) + }) + } + }) + } + }) +} diff --git a/remix-debug/test/decoder/storageDecoder.js b/remix-debug/test/decoder/storageDecoder.js new file mode 100644 index 0000000000..1c943e47fd --- /dev/null +++ b/remix-debug/test/decoder/storageDecoder.js @@ -0,0 +1,276 @@ +'use strict' +var tape = require('tape') +var compiler = require('solc') +var stateDecoder = require('../../src/decoder/stateDecoder') +var MockStorageResolver = require('./mockStorageResolver') +var remixLib = require('remix-lib') +var compilerInput = remixLib.helpers.compiler.compilerInput +var testMappingStorage = require('./stateTests/mapping') + +tape('solidity', function (t) { + t.test('storage decoder', function (st) { + testIntStorage(st, function () { + testByteStorage(st, function () { + testStructArrayStorage(st, function () { + testMappingStorage(st, function () { + st.end() + }) + }) + }) + }) + }) +}) + +function testIntStorage (st, cb) { + var intStorage = require('./contracts/intStorage') + var output = compiler.compileStandardWrapper(compilerInput(intStorage.contract)) + output = JSON.parse(output) + var mockStorageResolver + for (var storage of [intStorage.fullStorage, shrinkStorage(intStorage.fullStorage)]) { + mockStorageResolver = new MockStorageResolver(storage) + stateDecoder.solidityState(mockStorageResolver, output.sources, 'intStorage').then((decoded) => { + st.equal(decoded['ui8'].value, '130') + st.equal(decoded['ui16'].value, '456') + st.equal(decoded['ui32'].value, '4356') + st.equal(decoded['ui64'].value, '3543543543') + st.equal(decoded['ui128'].value, '234567') + st.equal(decoded['ui256'].value, '115792089237316195423570985008687907853269984665640564039457584007880697216513') + st.equal(decoded['ui'].value, '123545666') + st.equal(decoded['i8'].value, '-45') + st.equal(decoded['i16'].value, '-1234') + st.equal(decoded['i32'].value, '3455') + st.equal(decoded['i64'].value, '-35566') + st.equal(decoded['i128'].value, '-444444') + st.equal(decoded['i256'].value, '3434343') + st.equal(decoded['i'].value, '-32432423423') + st.equal(decoded['ishrink'].value, '2') + }) + } + + mockStorageResolver = new MockStorageResolver({}) + stateDecoder.solidityState(mockStorageResolver, output.sources, 'intStorage').then((decoded) => { + st.equal(decoded['ui8'].value, '0') + st.equal(decoded['ui16'].value, '0') + st.equal(decoded['ui32'].value, '0') + st.equal(decoded['ui64'].value, '0') + st.equal(decoded['ui128'].value, '0') + st.equal(decoded['ui256'].value, '0') + st.equal(decoded['ui'].value, '0') + st.equal(decoded['i8'].value, '0') + st.equal(decoded['i16'].value, '0') + st.equal(decoded['i32'].value, '0') + st.equal(decoded['i64'].value, '0') + st.equal(decoded['i128'].value, '0') + st.equal(decoded['i256'].value, '0') + st.equal(decoded['i'].value, '0') + st.equal(decoded['ishrink'].value, '0') + cb() + }) +} + +function testByteStorage (st, cb) { + var byteStorage = require('./contracts/byteStorage') + var output = compiler.compileStandardWrapper(compilerInput(byteStorage.contract)) + output = JSON.parse(output) + var mockStorageResolver + for (var storage of [byteStorage.storage, shrinkStorage(byteStorage.storage)]) { + mockStorageResolver = new MockStorageResolver(storage) + stateDecoder.solidityState(mockStorageResolver, output.sources, 'byteStorage').then((decoded) => { + st.equal(decoded['b1'].value, false) + st.equal(decoded['a1'].value, '0xFE350F199F244AC9A79038D254400B632A633225') + st.equal(decoded['b2'].value, true) + st.equal(decoded['dynb1'].value, '0x64796e616d69636279746573') + st.equal(decoded['dynb1'].length, '0xc') + st.equal(decoded['stab'].value, '0x01') + st.equal(decoded['stab1'].value, '0x12') + st.equal(decoded['stab2'].value, '0x1579') + st.equal(decoded['stab3'].value, '0x359356') + st.equal(decoded['stab4'].value, '0x23750000') + st.equal(decoded['stab5'].value, '0x0235764500') + st.equal(decoded['stab6'].value, '0x324435000000') + st.equal(decoded['stab7'].value, '0x00432400000000') + st.equal(decoded['stab8'].value, '0x3245546457650000') + st.equal(decoded['stab9'].value, '0x034345430000000000') + st.equal(decoded['stab10'].value, '0x04543543654657000000') + st.equal(decoded['stab11'].value, '0x5435465400000000000000') + st.equal(decoded['stab12'].value, '0x030000000000000000000000') + st.equal(decoded['stab13'].value, '0x03243242345435000000000000') + st.equal(decoded['stab14'].value, '0x3245435435435300000000000000') + st.equal(decoded['stab15'].value, '0x032454434435000000000000000000') + st.equal(decoded['stab16'].value, '0x32454354440000000000000000000000') + st.equal(decoded['stab17'].value, '0x0324543432432432450000000000000000') + st.equal(decoded['stab18'].value, '0x032453432543543500000000000000000000') + st.equal(decoded['stab19'].value, '0x03245434354354350000000000000000000000') + st.equal(decoded['stab20'].value, '0x032454543543AB35000000000000000000000000') + st.equal(decoded['stab21'].value, '0x324544324234350000000000000000000000000000') + st.equal(decoded['stab22'].value, '0x324543AEF50000000000000000000000000000000000') + st.equal(decoded['stab23'].value, '0x3245435FFF000000000000000000000000000000000000') + st.equal(decoded['stab24'].value, '0x3245435F0000000000000000000000000000000000000000') + st.equal(decoded['stab25'].value, '0x3245435F000000000000000000000000000000000000000000') + st.equal(decoded['stab26'].value, '0x3245435F00000000000000000000000000000000000000000000') + st.equal(decoded['stab27'].value, '0x03245FFFFFFF000000000000000000000000000000000000000000') + st.equal(decoded['stab28'].value, '0x03241235000000000000000000000000000000000000000000000000') + st.equal(decoded['stab29'].value, '0x0325213213000000000000000000000000000000000000000000000000') + st.equal(decoded['stab30'].value, '0x032454352324230000000000000000000000000000000000000000000000') + st.equal(decoded['stab31'].value, '0x32454351230000000000000000000000000000000000000000000000000000') + st.equal(decoded['stab32'].value, '0x324324423432543543AB00000000000000000000000000000000000000000000') + st.equal(decoded['enumDec'].value, 'e240') + st.equal(decoded['str1'].value, 'short') + st.equal(decoded['str12'].value, 'шеллы') + st.equal(decoded['str2'].value, 'long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long__long') + }) + } + + mockStorageResolver = new MockStorageResolver({}) + stateDecoder.solidityState(mockStorageResolver, output.sources, 'byteStorage').then((decoded) => { + st.equal(decoded['b1'].value, false) + st.equal(decoded['a1'].value, '0x0000000000000000000000000000000000000000') + st.equal(decoded['b2'].value, false) + st.equal(decoded['dynb1'].value, '0x') + st.equal(decoded['dynb1'].length, '0x0') + st.equal(decoded['stab'].value, '0x00') + st.equal(decoded['stab1'].value, '0x00') + st.equal(decoded['stab2'].value, '0x0000') + st.equal(decoded['stab3'].value, '0x000000') + st.equal(decoded['stab4'].value, '0x00000000') + st.equal(decoded['stab5'].value, '0x0000000000') + st.equal(decoded['stab6'].value, '0x000000000000') + st.equal(decoded['stab7'].value, '0x00000000000000') + st.equal(decoded['stab8'].value, '0x0000000000000000') + st.equal(decoded['stab9'].value, '0x000000000000000000') + st.equal(decoded['stab10'].value, '0x00000000000000000000') + st.equal(decoded['stab11'].value, '0x0000000000000000000000') + st.equal(decoded['stab12'].value, '0x000000000000000000000000') + st.equal(decoded['stab13'].value, '0x00000000000000000000000000') + st.equal(decoded['stab14'].value, '0x0000000000000000000000000000') + st.equal(decoded['stab15'].value, '0x000000000000000000000000000000') + st.equal(decoded['stab16'].value, '0x00000000000000000000000000000000') + st.equal(decoded['stab17'].value, '0x0000000000000000000000000000000000') + st.equal(decoded['stab18'].value, '0x000000000000000000000000000000000000') + st.equal(decoded['stab19'].value, '0x00000000000000000000000000000000000000') + st.equal(decoded['stab20'].value, '0x0000000000000000000000000000000000000000') + st.equal(decoded['stab21'].value, '0x000000000000000000000000000000000000000000') + st.equal(decoded['stab22'].value, '0x00000000000000000000000000000000000000000000') + st.equal(decoded['stab23'].value, '0x0000000000000000000000000000000000000000000000') + st.equal(decoded['stab24'].value, '0x000000000000000000000000000000000000000000000000') + st.equal(decoded['stab25'].value, '0x00000000000000000000000000000000000000000000000000') + st.equal(decoded['stab26'].value, '0x0000000000000000000000000000000000000000000000000000') + st.equal(decoded['stab27'].value, '0x000000000000000000000000000000000000000000000000000000') + st.equal(decoded['stab28'].value, '0x00000000000000000000000000000000000000000000000000000000') + st.equal(decoded['stab29'].value, '0x0000000000000000000000000000000000000000000000000000000000') + st.equal(decoded['stab30'].value, '0x000000000000000000000000000000000000000000000000000000000000') + st.equal(decoded['stab31'].value, '0x00000000000000000000000000000000000000000000000000000000000000') + st.equal(decoded['stab32'].value, '0x0000000000000000000000000000000000000000000000000000000000000000') + st.equal(decoded['enumDec'].value, 'e0') + st.equal(decoded['str1'].length, '0x0') + st.equal(decoded['str2'].length, '0x0') + st.equal(decoded['str1'].value, '') + st.equal(decoded['str12'].value, '') + st.equal(decoded['str2'].value, '') + cb() + }) +} + +function shrinkStorage (storage) { + var shrinkedStorage = {} + var regex = /0x(00)*(..)/ + for (var key in storage) { + var value = storage[key] + shrinkedStorage[key.replace(regex, '0x$2')] = value.replace(regex, '0x$2') + } + return shrinkedStorage +} + +function testStructArrayStorage (st, cb) { + var structArrayStorage = require('./contracts/structArrayStorage') + var output = compiler.compileStandardWrapper(compilerInput(structArrayStorage.contract)) + output = JSON.parse(output) + var mockStorageResolver = new MockStorageResolver(structArrayStorage.storage) + stateDecoder.solidityState(mockStorageResolver, output.sources, 'structArrayStorage').then((decoded) => { + st.equal(decoded['intStructDec'].value['i8'].value, '32') + st.equal(decoded['intStructDec'].value['i16'].value, '-54') + st.equal(decoded['intStructDec'].value['ui32'].value, '128') + st.equal(decoded['intStructDec'].value['i256'].value, '-1243565465756') + st.equal(decoded['intStructDec'].value['ui16'].value, '34556') + st.equal(decoded['intStructDec'].value['i32'].value, '-345446546') + st.equal(decoded['i5'].length, '0x7') + st.equal(decoded['i5'].value[0].value, '-2134') + st.equal(decoded['i5'].value[1].value, '345') + st.equal(decoded['i5'].value[2].value, '-3246') + st.equal(decoded['i5'].value[3].value, '4357') + st.equal(decoded['i5'].value[4].value, '324') + st.equal(decoded['i5'].value[5].value, '-2344') + st.equal(decoded['i5'].value[6].value, '3254') + st.equal(decoded['idyn5'].length, '0x9') + st.equal(decoded['idyn5'].value[0].value, '-2134') + st.equal(decoded['idyn5'].value[1].value, '345') + st.equal(decoded['idyn5'].value[2].value, '-3246') + st.equal(decoded['idyn5'].value[3].value, '4357') + st.equal(decoded['idyn5'].value[4].value, '324') + st.equal(decoded['idyn5'].value[5].value, '-2344') + st.equal(decoded['idyn5'].value[6].value, '3254') + st.equal(decoded['idyn5'].value[7].value, '-254') + st.equal(decoded['idyn5'].value[8].value, '-2354') + st.equal(decoded['dyn1'].length, '0x4') + st.equal(decoded['dyn1'].value[0].length, '0x1') + st.equal(decoded['dyn1'].value[0].value[0].value, '3') + st.equal(decoded['dyn1'].value[1].length, '0x3') + st.equal(decoded['dyn1'].value[1].value[0].value, '12') + st.equal(decoded['dyn1'].value[1].value[1].value, '-12') + st.equal(decoded['dyn1'].value[1].value[2].value, '-1234') + st.equal(decoded['dyn1'].value[2].length, '0xa') + st.equal(decoded['dyn1'].value[2].value[0].value, '1') + st.equal(decoded['dyn1'].value[2].value[1].value, '12') + st.equal(decoded['dyn1'].value[2].value[2].value, '1235') + st.equal(decoded['dyn1'].value[2].value[3].value, '-12') + st.equal(decoded['dyn1'].value[2].value[4].value, '-123456') + st.equal(decoded['dyn1'].value[2].value[5].value, '-23435435') + st.equal(decoded['dyn1'].value[2].value[6].value, '543543') + st.equal(decoded['dyn1'].value[2].value[7].value, '2') + st.equal(decoded['dyn1'].value[2].value[8].value, '-1') + st.equal(decoded['dyn1'].value[2].value[9].value, '232432') + st.equal(decoded['dyn1'].value[3].length, '0x2') + st.equal(decoded['dyn1'].value[3].value[0].value, '232432') + st.equal(decoded['dyn1'].value[3].value[0].value, '232432') + st.equal(decoded['dyn2'].length, '0x2') + st.equal(decoded['dyn2'].value[0].length, '0x4') + st.equal(decoded['dyn2'].value[0].value[0].value[0].value, '23') + st.equal(decoded['dyn2'].value[0].value[0].value[1].value, '-23') + st.equal(decoded['dyn2'].value[0].value[0].value[2].value, '-28') + st.equal(decoded['dyn2'].value[0].value[1].value[0].value, '23') + st.equal(decoded['dyn2'].value[0].value[1].value[1].value, '-23') + st.equal(decoded['dyn2'].value[0].value[1].value[2].value, '-28') + st.equal(decoded['dyn2'].value[0].value[2].value[0].value, '23') + st.equal(decoded['dyn2'].value[0].value[2].value[1].value, '-23') + st.equal(decoded['dyn2'].value[0].value[2].value[2].value, '-28') + st.equal(decoded['dyn2'].value[0].value[3].value[0].value, '23') + st.equal(decoded['dyn2'].value[0].value[3].value[1].value, '-23') + st.equal(decoded['dyn2'].value[0].value[3].value[2].value, '-28') + st.equal(decoded['dyn2'].value[1].length, '0x4') + st.equal(decoded['dyn2'].value[1].value[0].value[0].value, '23') + st.equal(decoded['dyn2'].value[1].value[0].value[1].value, '-23') + st.equal(decoded['dyn2'].value[1].value[0].value[2].value, '-28') + st.equal(decoded['dyn2'].value[1].value[1].value[0].value, '23') + st.equal(decoded['dyn2'].value[1].value[1].value[1].value, '-23') + st.equal(decoded['dyn2'].value[1].value[1].value[2].value, '-28') + st.equal(decoded['dyn2'].value[1].value[2].value[0].value, '23') + st.equal(decoded['dyn2'].value[1].value[2].value[1].value, '-23') + st.equal(decoded['dyn2'].value[1].value[2].value[2].value, '-28') + st.equal(decoded['dyn2'].value[1].value[3].value[0].value, '23') + st.equal(decoded['dyn2'].value[1].value[3].value[1].value, '-23') + st.equal(decoded['dyn2'].value[1].value[3].value[2].value, '-28') + st.equal(decoded['arrayStruct'].value[0].value[0].value.i8.value, '34') + st.equal(decoded['arrayStruct'].value[0].value[0].value.str.value, 'test_str_short') + st.equal(decoded['arrayStruct'].value[0].value[1].value.i8.value, '-123') + st.equal(decoded['arrayStruct'].value[0].value[1].value.str.value, 'test_str_long test_str_lo ngtest_str_longtest_str_ longtest_str_longtest_ str_longtest_str_l ongtest_str_long') + st.equal(decoded['arrayStruct'].value[1].value[0].value.i8.value, '50') + st.equal(decoded['arrayStruct'].value[1].value[0].value.str.value, 'test_str_short') + st.equal(decoded['arrayStruct'].value[2].value[0].value.i8.value, '60') + st.equal(decoded['arrayStruct'].value[2].value[0].value.str.value, 'test_str_short') + st.equal(decoded['arrayStruct'].value[2].value[1].value.i8.value, '84') + st.equal(decoded['arrayStruct'].value[2].value[1].value.str.value, 'test_str_long test_str_lo ngtest_str_longtest_str_ longtest_str_longtest_ str_longtest_str_l ongtest_str_long') + st.equal(decoded['arrayStruct'].value[2].value[2].value.i8.value, '-34') + st.equal(decoded['arrayStruct'].value[2].value[2].value.str.value, 'test_str_short') + cb() + }) +} diff --git a/remix-debug/test/decoder/storageLocation.js b/remix-debug/test/decoder/storageLocation.js new file mode 100644 index 0000000000..fe4dc601d3 --- /dev/null +++ b/remix-debug/test/decoder/storageLocation.js @@ -0,0 +1,59 @@ +'use strict' +var tape = require('tape') +var compiler = require('solc') +var stateDecoder = require('../../src/decoder/stateDecoder') +var contracts = require('./contracts/miscContracts') +var remixLib = require('remix-lib') +var compilerInput = remixLib.helpers.compiler.compilerInput + +tape('solidity', function (t) { + t.test('storage location', function (st) { + var output = compiler.compileStandardWrapper(compilerInput(contracts)) + output = JSON.parse(output) + var stateDec = stateDecoder.extractStateVariables('contractUint', output.sources) + checkLocation(st, stateDec[0].storagelocation, 0, 0) + checkLocation(st, stateDec[1].storagelocation, 1, 0) + checkLocation(st, stateDec[2].storagelocation, 2, 0) + checkLocation(st, stateDec[3].storagelocation, 3, 0) + + stateDec = stateDecoder.extractStateVariables('contractStructAndArray', output.sources) + checkLocation(st, stateDec[0].storagelocation, 0, 0) + checkLocation(st, stateDec[1].storagelocation, 2, 0) + checkLocation(st, stateDec[2].storagelocation, 8, 0) + + stateDec = stateDecoder.extractStateVariables('contractArray', output.sources) + checkLocation(st, stateDec[0].storagelocation, 0, 0) + checkLocation(st, stateDec[1].storagelocation, 1, 0) + checkLocation(st, stateDec[2].storagelocation, 2, 0) + + stateDec = stateDecoder.extractStateVariables('contractSmallVariable', output.sources) + checkLocation(st, stateDec[0].storagelocation, 0, 0) + checkLocation(st, stateDec[1].storagelocation, 0, 1) + checkLocation(st, stateDec[2].storagelocation, 0, 2) + checkLocation(st, stateDec[3].storagelocation, 0, 4) + checkLocation(st, stateDec[4].storagelocation, 1, 0) + checkLocation(st, stateDec[5].storagelocation, 2, 0) + + stateDec = stateDecoder.extractStateVariables('testSimpleStorage', output.sources) + checkLocation(st, stateDec[0].storagelocation, 0, 0) + checkLocation(st, stateDec[1].storagelocation, 1, 0) + checkLocation(st, stateDec[2].storagelocation, 2, 0) + checkLocation(st, stateDec[3].storagelocation, 3, 0) + checkLocation(st, stateDec[4].storagelocation, 4, 0) + checkLocation(st, stateDec[5].storagelocation, 8, 0) + checkLocation(st, stateDec[6].storagelocation, 9, 0) + checkLocation(st, stateDec[8].storagelocation, 17, 0) + checkLocation(st, stateDec[9].storagelocation, 17, 4) + checkLocation(st, stateDec[10].storagelocation, 17, 6) + checkLocation(st, stateDec[11].storagelocation, 17, 7) + checkLocation(st, stateDec[12].storagelocation, 18, 0) + checkLocation(st, stateDec[13].storagelocation, 21, 0) + + st.end() + }) +}) + +function checkLocation (st, location, slot, offset) { + st.equal(location.offset, offset) + st.equal(location.slot, slot) +} diff --git a/remix-debug/test/decoder/vmCall.js b/remix-debug/test/decoder/vmCall.js new file mode 100644 index 0000000000..a57e3114c5 --- /dev/null +++ b/remix-debug/test/decoder/vmCall.js @@ -0,0 +1,63 @@ +'use strict' +var utileth = require('ethereumjs-util') +var Tx = require('ethereumjs-tx') +var Block = require('ethereumjs-block') +var BN = require('ethereumjs-util').BN +var remixLib = require('remix-lib') + +function sendTx (vm, from, to, value, data, cb) { + var tx = new Tx({ + nonce: new BN(from.nonce++), + gasPrice: new BN(1), + gasLimit: new BN(3000000, 10), + to: to, + value: new BN(value, 10), + data: new Buffer(data, 'hex') + }) + tx.sign(from.privateKey) + var block = new Block({ + header: { + timestamp: new Date().getTime() / 1000 | 0, + number: 0 + }, + transactions: [], + uncleHeaders: [] + }) + vm.runTx({block: block, tx: tx, skipBalance: true, skipNonce: true}, function (error, result) { + setTimeout(() => { + cb(error, utileth.bufferToHex(tx.hash())) + }, 500) + }) +} + +/* + Init VM / Send Transaction +*/ +function initVM (st, privateKey) { + var utileth = require('ethereumjs-util') + var VM = require('ethereumjs-vm') + var Web3Providers = remixLib.vm.Web3Providers + var address = utileth.privateToAddress(privateKey) + var vm = new VM({ + enableHomestead: true, + activatePrecompiles: true + }) + vm.stateManager.putAccountBalance(address, 'f00000000000000001', function cb () {}) + var web3Providers = new Web3Providers() + web3Providers.addVM('VM', vm) + web3Providers.get('VM', function (error, obj) { + if (error) { + var mes = 'provider TEST not defined' + console.log(mes) + st.fail(mes) + } else { + vm.web3 = obj + } + }) + return vm +} + +module.exports = { + sendTx: sendTx, + initVM: initVM +} diff --git a/remix-debug/test/disassembler.js b/remix-debug/test/disassembler.js new file mode 100644 index 0000000000..d0c617f010 --- /dev/null +++ b/remix-debug/test/disassembler.js @@ -0,0 +1,101 @@ +'use strict' + +var tape = require('tape') +var disassemble = require('../src/code/disassembler').disassemble + +tape('Disassembler', function (t) { + t.test('empty', function (st) { + st.plan(1) + st.equal(disassemble(''), '') + }) + t.test('add', function (st) { + st.plan(1) + st.equal(disassemble('0x01'), 'add') + }) + t.test('push', function (st) { + st.plan(1) + st.equal(disassemble('0x640203'), '0x0203000000') + }) + t.test('complexcode', function (st) { + st.plan(1) + var code = '60606040526009600060005055607e8060186000396000f360606040526000357c0100000000000000000000000000000000000000000000000000000000900480630dbe671f146039576035565b6002565b3460025760486004805050604a565b005b6000600090505b600a811015607a5760006000818150548092919060010191905055505b80806001019150506051565b5b5056' + var asm = `mstore(0x40, 0x60) +0x09 +0x00 +pop(0x00) +sstore +0x7e +dup1 +0x18 +0x00 +codecopy +0x00 +return +mstore(0x40, 0x60) +calldataload(0x00) +0x0100000000000000000000000000000000000000000000000000000000 +swap1 +div +dup1 +0x0dbe671f +eq +0x39 +jumpi +jump(0x35) +label1: +jump(0x02) +label2: +jumpi(0x02, callvalue()) +0x48 +0x04 +dup1 +pop +pop +jump(0x4a) +label3: +stop() +label4: +0x00 +0x00 +swap1 +pop +label5: +0x0a +dup2 +lt +iszero +0x7a +jumpi +0x00 +0x00 +dup2 +dup2 +pop +sload +dup1 +swap3 +swap2 +swap1 +0x01 +add +swap2 +swap1 +pop +sstore +pop +label6: +dup1 +dup1 +0x01 +add +swap2 +pop +pop +jump(0x51) +label7: +label8: +pop +jump` + st.equal(disassemble(code), asm) + }) +}) diff --git a/test/init.js b/remix-debug/test/init.js similarity index 92% rename from test/init.js rename to remix-debug/test/init.js index 46db217f3a..bd8f9114ea 100644 --- a/test/init.js +++ b/remix-debug/test/init.js @@ -2,7 +2,7 @@ var init = { overrideWeb3: function (web3, web3Override) { web3.eth.getCode = web3Override.getCode web3.debug.traceTransaction = web3Override.traceTransaction - web3.debug.storageAt = web3Override.storageAt + web3.debug.storageRangeAt = web3Override.storageRangeAt web3.eth.getTransaction = web3Override.getTransaction web3.eth.getTransactionFromBlock = web3Override.getTransactionFromBlock web3.eth.getBlockNumber = web3Override.getBlockNumber diff --git a/test/resources/testWeb3.js b/remix-debug/test/resources/testWeb3.js similarity index 88% rename from test/resources/testWeb3.js rename to remix-debug/test/resources/testWeb3.js index f5d8bbb4e7..a138a81eb4 100644 --- a/test/resources/testWeb3.js +++ b/remix-debug/test/resources/testWeb3.js @@ -4,7 +4,7 @@ var web3Override = {} web3Override.eth = {} web3Override.debug = {} var data = init.readFile(require('path').resolve(__dirname, 'testWeb3.json')) -var data = JSON.parse(data) +data = JSON.parse(data) web3Override.eth.getCode = function (address, callback) { if (callback) { @@ -18,8 +18,8 @@ web3Override.debug.traceTransaction = function (txHash, options, callback) { callback(null, data.testTraces[txHash]) } -web3Override.debug.storageAt = function (blockNumber, txIndex, address, callback) { - callback(null, {}) +web3Override.debug.storageRangeAt = function (blockNumber, txIndex, address, start, maxSize, callback) { + callback(null, { storage: {}, complete: true }) } web3Override.eth.getTransaction = function (txHash, callback) { diff --git a/test/resources/testWeb3.json b/remix-debug/test/resources/testWeb3.json similarity index 100% rename from test/resources/testWeb3.json rename to remix-debug/test/resources/testWeb3.json diff --git a/remix-debug/test/tests.js b/remix-debug/test/tests.js new file mode 100644 index 0000000000..37481f6501 --- /dev/null +++ b/remix-debug/test/tests.js @@ -0,0 +1,194 @@ +'use strict' +var tape = require('tape') +var remixLib = require('remix-lib') +var compilerInput = remixLib.helpers.compiler.compilerInput +var vmCall = require('./vmCall') +var Debugger = require('../src/Ethdebugger') +var compiler = require('solc') + +require('./traceManager.js') +require('./codeManager.js') +require('./disassembler.js') + +require('./decoder/decodeInfo.js') +require('./decoder/storageLocation.js') +require('./decoder/storageDecoder.js') +require('./decoder/localDecoder.js') + +var BreakpointManager = require('../src/code/breakpointManager') + +tape('debug contract', function (t) { + t.plan(12) + var privateKey = new Buffer('dae9801649ba2d95a21e688b56f77905e5667c44ce868ec83f82e838712a2c7a', 'hex') + var vm = vmCall.initVM(t, privateKey) + var output = compiler.compileStandardWrapper(compilerInput(ballot)) + output = JSON.parse(output) + var web3VM = new remixLib.vm.Web3VMProvider() + web3VM.setVM(vm) + vmCall.sendTx(vm, {nonce: 0, privateKey: privateKey}, null, 0, output.contracts['test.sol']['Ballot'].evm.bytecode.object, (error, txHash) => { + if (error) { + t.end(error) + } else { + web3VM.eth.getTransaction(txHash, (error, tx) => { + if (error) { + t.end(error) + } else { + var debugManager = new Debugger({ + compilationResult: function () { + return output + } + }) + + debugManager.addProvider('web3vmprovider', web3VM) + debugManager.switchProvider('web3vmprovider') + + debugManager.callTree.event.register('callTreeReady', () => { + testDebugging(t, debugManager) + }) + + debugManager.debug(tx) + } + }) + } + }) +}) + + +function testDebugging (t, debugManager) { + // stack + debugManager.traceManager.getStackAt(4, (error, callstack) => { + if (error) return t.end(error) + t.equal(JSON.stringify(callstack), JSON.stringify([ '0x0000000000000000000000000000000000000000000000000000000000000000' ])) + }) + + debugManager.traceManager.getStackAt(41, (error, callstack) => { + if (error) return t.end(error) + + /* + t.equal(JSON.stringify(callstack), JSON.stringify(['0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000004b0897b0513fdc7c541b6d9d7e929c4e5364d2db', '0x0000000000000000000000000000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000000000000000000000000001', '0x000000000000000000000000000000000000000000000000000000000000002d'])) + */ + }) + + // storage + debugManager.traceManager.getCurrentCalledAddressAt(38, (error, address) => { + if (error) return t.end(error) + var storageView = debugManager.storageViewAt(38, address) + storageView.storageRange((error, storage) => { + if (error) return t.end(error) + t.equal(JSON.stringify(storage), JSON.stringify({ '0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563': { key: '0x0000000000000000000000000000000000000000000000000000000000000000', value: '0x0000000000000000000000004b0897b0513fdc7c541b6d9d7e929c4e5364d2db' } })) + }) + }) + + debugManager.extractStateAt(116, (error, state) => { + if (error) return t.end(error) + debugManager.decodeStateAt(116, state, (error, decodedState) => { + if (error) return t.end(error) + t.equal(decodedState['chairperson'].value, '0x4B0897B0513FDC7C541B6D9D7E929C4E5364D2DB') + t.equal(decodedState['chairperson'].type, 'address') + t.equal(decodedState['proposals'].value[0].value.voteCount.value, '0') + t.equal(decodedState['proposals'].value[0].value.voteCount.type, 'uint256') + t.equal(decodedState['proposals'].value[0].type, 'struct Ballot.Proposal') + t.equal(decodedState['proposals'].length, '0x1') + t.equal(decodedState['proposals'].type, 'struct Ballot.Proposal[]') + }) + }) + + debugManager.traceManager.getCurrentCalledAddressAt(104, (error, address) => { + if (error) return t.end(error) + debugManager.sourceLocationFromVMTraceIndex(address, 104, (error, location) => { + if (error) return t.end(error) + debugManager.decodeLocalsAt(104, location, (error, decodedlocals) => { + if (error) return t.end(error) + t.equal(JSON.stringify(decodedlocals), JSON.stringify({'p': {'value': '45', 'type': 'uint256'}, 'addressLocal': {'value': '0x4B0897B0513FDC7C541B6D9D7E929C4E5364D2DB', 'type': 'address'}, 'proposalsLocals': {'value': [{'value': {'voteCount': {'value': '0', 'type': 'uint256'}}, 'type': 'struct Ballot.Proposal'}], 'length': '0x1', 'type': 'struct Ballot.Proposal[]'}})) + }) + }) + }) + + var sourceMappingDecoder = new remixLib.SourceMappingDecoder() + var breakPointManager = new BreakpointManager(debugManager, (rawLocation) => { + return sourceMappingDecoder.convertOffsetToLineColumn(rawLocation, sourceMappingDecoder.getLinebreakPositions(ballot)) + }) + + breakPointManager.add({fileName: 'test.sol', row: 23}) + + breakPointManager.event.register('breakpointHit', function (sourceLocation, step) { + console.log('breakpointHit') + t.equal(JSON.stringify(sourceLocation), JSON.stringify({ start: 591, length: 1, file: 0, jump: '-' })) + t.equal(step, 75) + }) + + breakPointManager.event.register('noBreakpointHit', function () { + t.end('noBreakpointHit') + console.log('noBreakpointHit') + }) + breakPointManager.jumpNextBreakpoint(0, true) +} + +var ballot = `pragma solidity ^0.4.0; +contract Ballot { + + struct Voter { + uint weight; + bool voted; + uint8 vote; + address delegate; + } + struct Proposal { + uint voteCount; + } + + address chairperson; + mapping(address => Voter) voters; + Proposal[] proposals; + + /// Create a new ballot with $(_numProposals) different proposals. + function Ballot() public { + uint p = 45; + chairperson = msg.sender; + address addressLocal = msg.sender; // copy of state variable + voters[chairperson].weight = 1; + proposals.length = 1; + Proposal[] proposalsLocals = proposals; // copy of state variable + } + + /// Give $(toVoter) the right to vote on this ballot. + /// May only be called by $(chairperson). + function giveRightToVote(address toVoter) public { + if (msg.sender != chairperson || voters[toVoter].voted) return; + voters[toVoter].weight = 1; + } + + /// Delegate your vote to the voter $(to). + function delegate(address to) public { + Voter storage sender = voters[msg.sender]; // assigns reference + if (sender.voted) return; + while (voters[to].delegate != address(0) && voters[to].delegate != msg.sender) + to = voters[to].delegate; + if (to == msg.sender) return; + sender.voted = true; + sender.delegate = to; + Voter storage delegateTo = voters[to]; + if (delegateTo.voted) + proposals[delegateTo.vote].voteCount += sender.weight; + else + delegateTo.weight += sender.weight; + } + + /// Give a single vote to proposal $(toProposal). + function vote(uint8 toProposal) public { + Voter storage sender = voters[msg.sender]; + if (sender.voted || toProposal >= proposals.length) return; + sender.voted = true; + sender.vote = toProposal; + proposals[toProposal].voteCount += sender.weight; + } + + function winningProposal() public constant returns (uint8 _winningProposal) { + uint256 winningVoteCount = 0; + for (uint8 prop = 0; prop < proposals.length; prop++) + if (proposals[prop].voteCount > winningVoteCount) { + winningVoteCount = proposals[prop].voteCount; + _winningProposal = prop; + } + } +}` diff --git a/test/traceManager.js b/remix-debug/test/traceManager.js similarity index 87% rename from test/traceManager.js rename to remix-debug/test/traceManager.js index facb586eca..053b07ea3c 100644 --- a/test/traceManager.js +++ b/remix-debug/test/traceManager.js @@ -1,10 +1,12 @@ 'use strict' var TraceManager = require('../src/trace/traceManager') var tape = require('tape') -var Web3Providers = require('../src/web3Provider/web3Providers') -var util = require('../src/helpers/global') +var remixLib = require('remix-lib') +var Web3Providers = remixLib.vm.Web3Providers var web3Test = require('./resources/testWeb3') +let web3 = null + tape('TraceManager', function (t) { var traceManager @@ -17,15 +19,15 @@ tape('TraceManager', function (t) { console.log(mes) st.fail(mes) } else { - util.web3 = obj - traceManager = new TraceManager() + web3 = obj + traceManager = new TraceManager({web3: web3}) st.end() } }) }) t.test('TraceManager.resolveTrace', function (st) { - var tx = util.web3.eth.getTransaction('0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51') + var tx = web3.eth.getTransaction('0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51') traceManager.resolveTrace(tx, function (error, result) { if (error) { st.fail(' - traceManager.resolveTrace - failed ' + result) @@ -53,13 +55,12 @@ tape('TraceManager', function (t) { st.end() }) - t.test('TraceManager.getStorageAt', function (st) { - var tx = util.web3.eth.getTransaction('0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51') - traceManager.getStorageAt(110, tx, function (error, result) { + t.test('TraceManager.accumulateStorageChanges', function (st) { + traceManager.accumulateStorageChanges(110, '0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5', {}, function (error, result) { if (error) { st.fail(error) } else { - st.ok(result['0x00'] === '0x38') + st.ok(result['0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563'].value === '0x38') st.end() } }) @@ -125,7 +126,7 @@ tape('TraceManager', function (t) { if (error) { st.fail(error) } else { - st.ok(result === 0) + st.ok(result.start === 0) } }) @@ -134,7 +135,7 @@ tape('TraceManager', function (t) { if (error) { st.fail(error) } else { - st.ok(result === 64) + st.ok(result.start === 64) } }) @@ -143,7 +144,9 @@ tape('TraceManager', function (t) { if (error) { st.fail(error) } else { - st.ok(result === 109) + st.ok(result.start === 0) + // this was 109 before: 111 is targeting the root call (starting index 0) + // this test make more sense as it is now (109 is the index of RETURN). } }) }) @@ -275,28 +278,14 @@ tape('TraceManager', function (t) { t.test('TraceManager.findStepOverBack', function (st) { var result = traceManager.findStepOverBack(116) console.log(result) - st.ok(result === -1) + st.ok(result === 115) st.end() }) t.test('TraceManager.findStepOverForward', function (st) { var result = traceManager.findStepOverForward(66) console.log(result) - st.ok(result === 108) - st.end() - }) - - t.test('TraceManager.findStepOutBack', function (st) { - var result = traceManager.findStepOutBack(70) - console.log(result) - st.ok(result === 63) - st.end() - }) - - t.test('TraceManager.findStepOutForward', function (st) { - var result = traceManager.findStepOutForward(15) - console.log(result) - st.ok(result === 142) + st.ok(result === 67) st.end() }) diff --git a/remix-debug/test/vmCall.js b/remix-debug/test/vmCall.js new file mode 100644 index 0000000000..a57e3114c5 --- /dev/null +++ b/remix-debug/test/vmCall.js @@ -0,0 +1,63 @@ +'use strict' +var utileth = require('ethereumjs-util') +var Tx = require('ethereumjs-tx') +var Block = require('ethereumjs-block') +var BN = require('ethereumjs-util').BN +var remixLib = require('remix-lib') + +function sendTx (vm, from, to, value, data, cb) { + var tx = new Tx({ + nonce: new BN(from.nonce++), + gasPrice: new BN(1), + gasLimit: new BN(3000000, 10), + to: to, + value: new BN(value, 10), + data: new Buffer(data, 'hex') + }) + tx.sign(from.privateKey) + var block = new Block({ + header: { + timestamp: new Date().getTime() / 1000 | 0, + number: 0 + }, + transactions: [], + uncleHeaders: [] + }) + vm.runTx({block: block, tx: tx, skipBalance: true, skipNonce: true}, function (error, result) { + setTimeout(() => { + cb(error, utileth.bufferToHex(tx.hash())) + }, 500) + }) +} + +/* + Init VM / Send Transaction +*/ +function initVM (st, privateKey) { + var utileth = require('ethereumjs-util') + var VM = require('ethereumjs-vm') + var Web3Providers = remixLib.vm.Web3Providers + var address = utileth.privateToAddress(privateKey) + var vm = new VM({ + enableHomestead: true, + activatePrecompiles: true + }) + vm.stateManager.putAccountBalance(address, 'f00000000000000001', function cb () {}) + var web3Providers = new Web3Providers() + web3Providers.addVM('VM', vm) + web3Providers.get('VM', function (error, obj) { + if (error) { + var mes = 'provider TEST not defined' + console.log(mes) + st.fail(mes) + } else { + vm.web3 = obj + } + }) + return vm +} + +module.exports = { + sendTx: sendTx, + initVM: initVM +} diff --git a/remix-debugger/README.md b/remix-debugger/README.md new file mode 100644 index 0000000000..2da32212e5 --- /dev/null +++ b/remix-debugger/README.md @@ -0,0 +1,74 @@ +# `remix-debugger` +# (Remix debugger has been deprecated and is not maintained anymore - the `remix-debug` module can be used to build your own debugger) + +The Remix Debugger is a webapp to debug the Ethereum VM and transactions. + ++ [Installation](#installation) ++ [Development](#development) ++ [First steps](#firststeps) ++ [Tests](#tests) + +## Installation + +Make sure Node is [installed on your setup](https://docs.npmjs.com/getting-started/installing-node), and that a [local `geth`/`eth` node is running](../README.md#how-to-use). + +```bash +git clone https://github.com/ethereum/remix +cd remix/remix-debugger +npm install +``` + +This will build the debugger. Start it by opening `index.html` in your browser. + +## Development + +Run `npm run start_dev` to start a local webserver, accessible at `http://127.0.0.1:8080`. Your browser will reload when files are updated. + +## First steps + +Once Remix is connected to a node, you will be able to debug transactions. + +You can do that: + - using a block number and a transaction index. + - using a transaction hash. + +After loading the transaction succeeded, the hash, from and to field will show up. The VM trace is then loaded. + +The debugger itself contains several controls that allow stepping over the trace and seing the current state of a selected step: + +#### Slider and Stepping action + +The slider allows to move quickly from a state to another. + +Stepping actions are: +- Step Into Back +- Step Over Back +- Step Over Forward +- Step Into Forward +- Jump Next Call: this will select the next state that refers to a context changes - CALL, CALLCODE, DELEGATECALL, CREATE. + +#### State Viewer + +The upper right panel contains basic informations about the current step: +- VMTraceStep: the index in the trace of the current step. +- Step +- Add memory +- Gas: gas used by this step +- Remaining gas: gas left +- Loaded address: the current code loaded, refers to the executing code. + +The other 6 panels describe the current selected state: + - Instructions list: list of all the instruction that defines the current executing code. + - Stack + - Storage Changes + - Memory + - Call Data$ + - Call Stack + +## Tests + +* To run unit tests, run `npm test`. + +* For local headless browser tests: + * To install `selenium`: `npm run selenium-install` + * Every time you want to run local browser tests, run: `npm run test-browser` diff --git a/remix-debugger/assets/css/font-awesome.min.css b/remix-debugger/assets/css/font-awesome.min.css new file mode 100644 index 0000000000..540440ce89 --- /dev/null +++ b/remix-debugger/assets/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/remix-debugger/assets/fonts/FontAwesome.otf b/remix-debugger/assets/fonts/FontAwesome.otf new file mode 100644 index 0000000000..681bdd4d4c Binary files /dev/null and b/remix-debugger/assets/fonts/FontAwesome.otf differ diff --git a/remix-debugger/assets/fonts/fontawesome-webfont.eot b/remix-debugger/assets/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000000..a30335d748 Binary files /dev/null and b/remix-debugger/assets/fonts/fontawesome-webfont.eot differ diff --git a/remix-debugger/assets/fonts/fontawesome-webfont.svg b/remix-debugger/assets/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000000..6fd19abcb9 --- /dev/null +++ b/remix-debugger/assets/fonts/fontawesome-webfont.svg @@ -0,0 +1,640 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/remix-debugger/assets/fonts/fontawesome-webfont.ttf b/remix-debugger/assets/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000000..d7994e1308 Binary files /dev/null and b/remix-debugger/assets/fonts/fontawesome-webfont.ttf differ diff --git a/remix-debugger/assets/fonts/fontawesome-webfont.woff b/remix-debugger/assets/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000000..6fd4ede0f3 Binary files /dev/null and b/remix-debugger/assets/fonts/fontawesome-webfont.woff differ diff --git a/remix-debugger/assets/fonts/fontawesome-webfont.woff2 b/remix-debugger/assets/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000000..5560193ccc Binary files /dev/null and b/remix-debugger/assets/fonts/fontawesome-webfont.woff2 differ diff --git a/remix-debugger/ci/browser_tests.sh b/remix-debugger/ci/browser_tests.sh new file mode 100755 index 0000000000..b395c03481 --- /dev/null +++ b/remix-debugger/ci/browser_tests.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +set -e + +if test $(uname -s) = "Darwin" +then + OS="osx" + FILEFORMAT="zip" +else + OS="linux" + FILEFORMAT="tar.gz" +fi +SC_VERSION="4.4.11" +SAUCECONNECT_URL="https://saucelabs.com/downloads/sc-$SC_VERSION-$OS.$FILEFORMAT" +SAUCECONNECT_USERNAME="yanneth" +SAUCECONNECT_ACCESSKEY="1f5a4560-b02b-41aa-b52b-f033aad30870" +BUILD_ID=${CIRCLE_BUILD_NUM:-${TRAVIS_JOB_NUMBER}} +SAUCECONNECT_JOBIDENTIFIER="remix_tests_${BUILD_ID}" +SAUCECONNECT_READYFILE="sc.ready" +TEST_EXITCODE=0 + +npm run build +npm run serve & + +wget $SAUCECONNECT_URL +tar -zxvf sc-"$SC_VERSION"-"$OS"."$FILEFORMAT" +./sc-"$SC_VERSION"-$OS/bin/sc -u $SAUCECONNECT_USERNAME -k $SAUCECONNECT_ACCESSKEY -i $SAUCECONNECT_JOBIDENTIFIER --readyfile $SAUCECONNECT_READYFILE & +while [ ! -f $SAUCECONNECT_READYFILE ]; do + sleep .5 +done + +npm run nightwatch_remote_parallel || TEST_EXITCODE=1 + +node ci/sauceDisconnect.js $SAUCECONNECT_USERNAME $SAUCECONNECT_ACCESSKEY $SAUCECONNECT_JOBIDENTIFIER + +echo $TEST_EXITCODE +if [ $TEST_EXITCODE -eq 1 ] +then + exit 1 +fi diff --git a/ci/deploy_from_travis.sh b/remix-debugger/ci/deploy_from_travis.sh similarity index 78% rename from ci/deploy_from_travis.sh rename to remix-debugger/ci/deploy_from_travis.sh index b64364610e..e80aba8e47 100755 --- a/ci/deploy_from_travis.sh +++ b/remix-debugger/ci/deploy_from_travis.sh @@ -11,7 +11,15 @@ git rm --cached -r . echo "# Automatic build" > README.md echo "Built website from {$SHA}. See https://github.com/ethereum/remix/ for details." >> README.md # -f is needed because "build" is part of .gitignore -git add -f README.md index.html build/app.js + +# copying file to the root folder +cp remix-debugger/index.html index.html +mkdir build +cp remix-debugger/build/app.js build/app.js +mkdir assets +cp -R remix-debugger/assets/. assets/ + +git add -f README.md index.html build/app.js assets git commit -m "Built website from {$SHA}." ENCRYPTION_LABEL=fade88419824 diff --git a/ci/deploy_key.enc b/remix-debugger/ci/deploy_key.enc similarity index 100% rename from ci/deploy_key.enc rename to remix-debugger/ci/deploy_key.enc diff --git a/ci/sauceDisconnect.js b/remix-debugger/ci/sauceDisconnect.js similarity index 100% rename from ci/sauceDisconnect.js rename to remix-debugger/ci/sauceDisconnect.js diff --git a/findClient.js b/remix-debugger/findClient.js similarity index 100% rename from findClient.js rename to remix-debugger/findClient.js diff --git a/index.html b/remix-debugger/index.html similarity index 51% rename from index.html rename to remix-debugger/index.html index 26cf0b6c91..4c6c33cf4c 100644 --- a/index.html +++ b/remix-debugger/index.html @@ -1,15 +1,16 @@ +
diff --git a/remix-debugger/index.js b/remix-debugger/index.js new file mode 100644 index 0000000000..a113f9390d --- /dev/null +++ b/remix-debugger/index.js @@ -0,0 +1,24 @@ +'use strict' +var VMDebugger = require('./src/ui/VmDebugger') +var Debugger = require('./src/ui/Ethdebugger') +var BasicPanel = require('./src/ui/BasicPanel') +var TreeView = require('./src/ui/TreeView') + +if (typeof (module) !== 'undefined' && typeof (module.exports) !== 'undefined') { + module.exports = modules() +} + +if (typeof (window) !== 'undefined') { + window.remix = modules() +} + +function modules () { + return { + ui: { + Debugger: Debugger, + VMdebugger: VMDebugger, + BasicPanel: BasicPanel, + TreeView: TreeView + } + } +} diff --git a/nightwatch.js b/remix-debugger/nightwatch.js similarity index 74% rename from nightwatch.js rename to remix-debugger/nightwatch.js index c21e2a7282..b81c8176b5 100644 --- a/nightwatch.js +++ b/remix-debugger/nightwatch.js @@ -1,9 +1,9 @@ 'use strict' -var TRAVIS_JOB_NUMBER = process.env.TRAVIS_JOB_NUMBER +var buildId = process.env.CIRCLE_BUILD_NUM || process.env.TRAVIS_JOB_NUMBER module.exports = { - 'src_folders': ['./test-browser'], - 'output_folder': './test-browser/reports', + 'src_folders': ['./test-browser/test'], + 'output_folder': './test-browser/test/reports', 'custom_commands_path': '', 'custom_assertions_path': '', 'globals_path': '', @@ -43,8 +43,8 @@ module.exports = { 'browserName': 'firefox', 'javascriptEnabled': true, 'acceptSslCerts': true, - 'build': 'build-' + TRAVIS_JOB_NUMBER, - 'tunnel-identifier': 'remix_tests_' + TRAVIS_JOB_NUMBER + 'build': 'build-' + buildId, + 'tunnel-identifier': 'remix_tests_' + buildId } }, @@ -53,8 +53,8 @@ module.exports = { 'browserName': 'chrome', 'javascriptEnabled': true, 'acceptSslCerts': true, - 'build': 'build-' + TRAVIS_JOB_NUMBER, - 'tunnel-identifier': 'remix_tests_' + TRAVIS_JOB_NUMBER + 'build': 'build-' + buildId, + 'tunnel-identifier': 'remix_tests_' + buildId } }, @@ -62,10 +62,11 @@ module.exports = { 'desiredCapabilities': { 'browserName': 'safari', 'javascriptEnabled': true, - 'platform': 'MAC', + 'platform': 'OS X 10.11', + 'version': '10.0', 'acceptSslCerts': true, - 'build': 'build-' + TRAVIS_JOB_NUMBER, - 'tunnel-identifier': 'remix_tests_' + TRAVIS_JOB_NUMBER + 'build': 'build-' + buildId, + 'tunnel-identifier': 'remix_tests_' + buildId } }, @@ -74,8 +75,10 @@ module.exports = { 'browserName': 'internet explorer', 'javascriptEnabled': true, 'acceptSslCerts': true, - 'build': 'build-' + TRAVIS_JOB_NUMBER, - 'tunnel-identifier': 'remix_tests_' + TRAVIS_JOB_NUMBER + 'platform': 'WIN8.1', + 'version': '11', + 'build': 'build-' + buildId, + 'tunnel-identifier': 'remix_tests_' + buildId } }, diff --git a/remix-debugger/package.json b/remix-debugger/package.json new file mode 100644 index 0000000000..a5cdaf390a --- /dev/null +++ b/remix-debugger/package.json @@ -0,0 +1,150 @@ +{ + "name": "remix-debugger", + "version": "0.1.9", + "description": "Ethereum IDE and tools for the web", + "contributors": [ + { + "name": "Yann Levreau", + "email": "yann@ethdev.com" + }, + { + "name": "Liana Husikyan", + "email": "liana@ethdev.com" + } + ], + "main": "./index.js", + "devDependencies": { + "babel-eslint": "^7.1.1", + "babel-plugin-transform-object-assign": "^6.22.0", + "babel-plugin-yo-yoify": "^0.3.3", + "babel-polyfill": "^6.22.0", + "babel-preset-env": "^1.6.1", + "babel-preset-es2015": "^6.24.0", + "babel-preset-stage-0": "^6.24.1", + "babelify": "^7.3.0", + "browserify": "^13.0.1", + "browserify-livereload": "^1.0.10", + "clipboard-copy": "^1.2.0", + "csjs-inject": "^1.0.1", + "ethereum-common": "0.0.18", + "ethereumjs-block": "^1.2.2", + "ethereumjs-tx": "^1.1.1", + "ethereumjs-util": "^4.5.0", + "ethereumjs-vm": "^2.3.3", + "fast-async": "^6.1.2", + "http-server": "^0.9.0", + "nightwatch": "^0.9.5", + "notify-error": "^1.2.0", + "npm-run-all": "^4.1.2", + "onchange": "^3.3.0", + "remix-core": "^0.0.15", + "remix-lib": "^0.2.9", + "remix-solidity": "^0.1.11", + "selenium-standalone": "^6.0.1", + "solc": "^0.4.13", + "standard": "^7.0.1", + "standard-reporter": "^1.0.5", + "tape": "^4.6.0", + "watchify": "^3.9.0", + "web3": "^0.15.3", + "yo-yo": "^1.2.1", + "yo-yoify": "^3.1.0" + }, + "scripts": { + "build": "mkdirp build; browserify index.js > build/app.js", + "lint": "standard | notify-error", + "nightwatch_local": "nightwatch --config nightwatch.js --env local", + "nightwatch_remote_chrome": "nightwatch --config nightwatch.js --env chrome", + "nightwatch_remote_firefox": "nightwatch --config nightwatch.js --env default", + "nightwatch_remote_ie": "nightwatch --config nightwatch.js --env ie", + "nightwatch_remote_parallel": "nightwatch --config nightwatch.js --env safari,chrome,default", + "nightwatch_remote_safari": "nightwatch --config nightwatch.js --env safari", + "onchange": "onchange build/app.js -- npm run lint", + "selenium": "selenium-standalone start", + "selenium-install": "selenium-standalone install", + "serve": "http-server .", + "start": "./runNode.sh", + "start_dev": "npm-run-all -lpr serve watch onchange", + "start_eth": "npm run warning_message; eth -j --rpccorsdomain '*'", + "start_geth": "npm run warning_message; geth --rpc --rpcapi 'web3,eth,debug' --rpcport 8545 --rpccorsdomain '*'", + "test": "standard && tape ./test/tests.js && ./ci/browser_tests.sh", + "test-browser": "npm-run-all -lpr selenium serve waittest", + "waittest": "sleep 5 && npm run nightwatch_local", + "warning_message": "echo 'DO NOT DO THIS IF eth/geth STORES PRIVATE KEYS!! External system might be able to access your node through the RPC server.\n\n';", + "watch": "mkdirp build; watchify index.js -p [ browserify-livereload --host 127.0.0.1 --port 1337 ] -dv -o build/app.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ethereum/remix.git" + }, + "author": "cpp-ethereum team", + "license": "MIT", + "bugs": { + "url": "https://github.com/ethereum/remix/issues" + }, + "homepage": "https://github.com/ethereum/remix#readme", + "standard": { + "ignore": [ + "node_modules/*", + "build/*", + "test/resources/*" + ] + }, + "babel": { + "plugins": [ + "transform-es2015-template-literals", + "transform-es2015-literals", + "transform-es2015-function-name", + "transform-es2015-arrow-functions", + "transform-es2015-block-scoped-functions", + "transform-es2015-classes", + "transform-es2015-object-super", + "transform-es2015-shorthand-properties", + "transform-es2015-duplicate-keys", + "transform-es2015-computed-properties", + "transform-es2015-for-of", + "transform-es2015-sticky-regex", + "transform-es2015-unicode-regex", + "check-es2015-constants", + "transform-es2015-spread", + "transform-es2015-parameters", + "transform-es2015-destructuring", + "transform-es2015-block-scoping", + "transform-object-assign" + ] + }, + "browserify": { + "transform": [ + [ + "babelify", + { + "sourceMapsAbsolute": false, + "sourceMaps": true, + "plugins": [ + [ + "fast-async", + { + "runtimePattern": null, + "compiler": { + "es7": true, + "noRuntime": true, + "promises": true, + "wrapAwait": true + } + } + ], + [ + "yo-yoify" + ], + [ + "transform-object-assign" + ] + ], + "presets": [ + "es2015" + ] + } + ] + ] + } +} diff --git a/runNode.sh b/remix-debugger/runNode.sh similarity index 100% rename from runNode.sh rename to remix-debugger/runNode.sh diff --git a/src/ui/BasicPanel.js b/remix-debugger/src/ui/BasicPanel.js similarity index 77% rename from src/ui/BasicPanel.js rename to remix-debugger/src/ui/BasicPanel.js index aa4eae240d..6efed6231c 100644 --- a/src/ui/BasicPanel.js +++ b/remix-debugger/src/ui/BasicPanel.js @@ -1,7 +1,16 @@ 'use strict' var style = require('./styles/basicStyles') var yo = require('yo-yo') -var ui = require('../helpers/ui') +var remixLib = require('remix-lib') +var ui = remixLib.helpers.ui + +var csjs = require('csjs-inject') + +var css = csjs` + .container { + width: 70%; + } +` function BasicPanel (_name, _width, _height) { this.data @@ -24,7 +33,8 @@ BasicPanel.prototype.show = function () { } BasicPanel.prototype.render = function () { - var view = yo`
+ var view = yo` +
${this.name}
diff --git a/remix-debugger/src/ui/ButtonNavigator.js b/remix-debugger/src/ui/ButtonNavigator.js new file mode 100644 index 0000000000..f438d69236 --- /dev/null +++ b/remix-debugger/src/ui/ButtonNavigator.js @@ -0,0 +1,183 @@ +'use strict' +var remixLib = require('remix-lib') +var EventManager = remixLib.EventManager +var yo = require('yo-yo') + +var csjs = require('csjs-inject') +var styleGuide = remixLib.ui.themeChooser +var styles = styleGuide.chooser() + +var css = csjs` + .buttons { + display: flex; + flex-wrap: wrap; + } + .stepButtons { + width: 100%; + display: flex; + justify-content: center; + } + .stepButton { + ${styles.rightPanel.debuggerTab.button_Debugger} + } + .jumpButtons { + width: 100%; + display: flex; + justify-content: center; + } + .jumpButton { + ${styles.rightPanel.debuggerTab.button_Debugger} + } + .navigator { + color: ${styles.rightPanel.debuggerTab.text_Primary}; + } + .navigator:hover { + color: ${styles.rightPanel.debuggerTab.button_Debugger_icon_HoverColor}; + } +` + +function ButtonNavigator (_parent, _traceManager) { + this.event = new EventManager() + this.intoBackDisabled = true + this.overBackDisabled = true + this.intoForwardDisabled = true + this.overForwardDisabled = true + this.jumpOutDisabled = true + this.jumpNextBreakpointDisabled = true + this.jumpPreviousBreakpointDisabled = true + + this.traceManager = _traceManager + this.currentCall = null + this.revertionPoint = null + + _parent.event.register('indexChanged', this, (index) => { + if (index < 0) return + if (_parent.currentStepIndex !== index) return + + this.traceManager.buildCallPath(index, (error, callsPath) => { + if (error) { + console.log(error) + resetWarning(this) + } else { + this.currentCall = callsPath[callsPath.length - 1] + if (this.currentCall.reverted) { + this.revertionPoint = this.currentCall.return + this.view.querySelector('#reverted').style.display = 'block' + this.view.querySelector('#reverted #outofgas').style.display = this.currentCall.outOfGas ? 'inline' : 'none' + this.view.querySelector('#reverted #parenthasthrown').style.display = 'none' + } else { + var k = callsPath.length - 2 + while (k >= 0) { + var parent = callsPath[k] + if (parent.reverted) { + this.revertionPoint = parent.return + this.view.querySelector('#reverted').style.display = 'block' + this.view.querySelector('#reverted #parenthasthrown').style.display = parent ? 'inline' : 'none' + this.view.querySelector('#reverted #outofgas').style.display = 'none' + return + } + k-- + } + resetWarning(this) + } + } + }) + }) + + this.view +} + +module.exports = ButtonNavigator + +ButtonNavigator.prototype.render = function () { + var self = this + var view = yo`
+
+ + + + +
+ +
+ + + +
+ +
` + if (!this.view) { + this.view = view + } + return view +} + +ButtonNavigator.prototype.reset = function () { + this.intoBackDisabled = true + this.overBackDisabled = true + this.intoForwardDisabled = true + this.overForwardDisabled = true + this.jumpOutDisabled = true + this.jumpNextBreakpointDisabled = true + this.jumpPreviousBreakpointDisabled = true + resetWarning(this) +} + +ButtonNavigator.prototype.stepChanged = function (step) { + this.intoBackDisabled = step <= 0 + this.overBackDisabled = step <= 0 + if (!this.traceManager) { + this.intoForwardDisabled = true + this.overForwardDisabled = true + } else { + var self = this + this.traceManager.getLength(function (error, length) { + if (error) { + self.reset() + console.log(error) + } else { + self.jumpNextBreakpointDisabled = step >= length - 1 + self.jumpPreviousBreakpointDisabled = step <= 0 + self.intoForwardDisabled = step >= length - 1 + self.overForwardDisabled = step >= length - 1 + var stepOut = self.traceManager.findStepOut(step) + self.jumpOutDisabled = stepOut === step + } + self.updateAll() + }) + } + this.updateAll() +} + +ButtonNavigator.prototype.updateAll = function () { + this.updateDisabled('intoback', this.intoBackDisabled) + this.updateDisabled('overback', this.overBackDisabled) + this.updateDisabled('overforward', this.overForwardDisabled) + this.updateDisabled('intoforward', this.intoForwardDisabled) + this.updateDisabled('jumpout', this.jumpOutDisabled) + this.updateDisabled('jumptoexception', this.jumpOutDisabled) + this.updateDisabled('jumpnextbreakpoint', this.jumpNextBreakpointDisabled) + this.updateDisabled('jumppreviousbreakpoint', this.jumpPreviousBreakpointDisabled) +} + +ButtonNavigator.prototype.updateDisabled = function (id, disabled) { + if (disabled) { + document.getElementById(id).setAttribute('disabled', true) + } else { + document.getElementById(id).removeAttribute('disabled') + } +} + +function resetWarning (self) { + self.view.querySelector('#reverted #outofgas').style.display = 'none' + self.view.querySelector('#reverted #parenthasthrown').style.display = 'none' + self.view.querySelector('#reverted').style.display = 'none' +} + +module.exports = ButtonNavigator diff --git a/src/ui/CalldataPanel.js b/remix-debugger/src/ui/CalldataPanel.js similarity index 61% rename from src/ui/CalldataPanel.js rename to remix-debugger/src/ui/CalldataPanel.js index e798ebb0f3..e2fef57b74 100644 --- a/src/ui/CalldataPanel.js +++ b/remix-debugger/src/ui/CalldataPanel.js @@ -1,11 +1,11 @@ 'use strict' -var BasicPanel = require('./BasicPanel') +var DropdownPanel = require('./DropdownPanel') var yo = require('yo-yo') function CalldataPanel (_parent, _traceManager) { this.parent = _parent this.traceManager = _traceManager - this.basicPanel = new BasicPanel('Call Data') + this.basicPanel = new DropdownPanel('Call Data', {json: true}) this.init() } @@ -15,28 +15,19 @@ CalldataPanel.prototype.render = function () { CalldataPanel.prototype.init = function () { var self = this - this.parent.register('indexChanged', this, function (index) { + this.parent.event.register('indexChanged', this, function (index) { if (index < 0) return if (self.parent.currentStepIndex !== index) return self.traceManager.getCallDataAt(index, function (error, calldata) { if (error) { - self.basicPanel.data = '' + self.basicPanel.update({}) console.log(error) } else if (self.parent.currentStepIndex === index) { - self.basicPanel.data = self.format(calldata) + self.basicPanel.update(calldata) } - self.basicPanel.update() }) }) } -CalldataPanel.prototype.format = function (calldata) { - var ret = '' - for (var key in calldata) { - ret += calldata[key] + '\n' - } - return ret -} - module.exports = CalldataPanel diff --git a/src/ui/CallstackPanel.js b/remix-debugger/src/ui/CallstackPanel.js similarity index 61% rename from src/ui/CallstackPanel.js rename to remix-debugger/src/ui/CallstackPanel.js index 31e38361dc..a9ecdbac5e 100644 --- a/src/ui/CallstackPanel.js +++ b/remix-debugger/src/ui/CallstackPanel.js @@ -1,11 +1,11 @@ 'use strict' -var BasicPanel = require('./BasicPanel') +var DropdownPanel = require('./DropdownPanel') var yo = require('yo-yo') function CallstackPanel (_parent, _traceManager) { this.parent = _parent this.traceManager = _traceManager - this.basicPanel = new BasicPanel('Call Stack') + this.basicPanel = new DropdownPanel('Call Stack', {json: true}) this.init() } @@ -15,28 +15,19 @@ CallstackPanel.prototype.render = function () { CallstackPanel.prototype.init = function () { var self = this - this.parent.register('indexChanged', this, function (index) { + this.parent.event.register('indexChanged', this, function (index) { if (index < 0) return if (self.parent.currentStepIndex !== index) return self.traceManager.getCallStackAt(index, function (error, callstack) { if (error) { console.log(error) - self.basicPanel.data = '' + self.basicPanel.update({}) } else if (self.parent.currentStepIndex === index) { - self.basicPanel.data = self.format(callstack) + self.basicPanel.update(callstack) } - self.basicPanel.update() }) }) } -CallstackPanel.prototype.format = function (callstack) { - var ret = '' - for (var key in callstack) { - ret += callstack[key] + '\n' - } - return ret -} - module.exports = CallstackPanel diff --git a/remix-debugger/src/ui/CodeListView.js b/remix-debugger/src/ui/CodeListView.js new file mode 100644 index 0000000000..784d6cbfe4 --- /dev/null +++ b/remix-debugger/src/ui/CodeListView.js @@ -0,0 +1,89 @@ +'use strict' +var style = require('./styles/basicStyles') +var yo = require('yo-yo') +var remixLib = require('remix-lib') +var DropdownPanel = require('./DropdownPanel') +var EventManager = remixLib.EventManager +var csjs = require('csjs-inject') +var styleGuide = remixLib.ui.themeChooser +var styles = styleGuide.chooser() + +var css = csjs` + .instructions { + ${styles.rightPanel.debuggerTab.box_Debugger} + width: 75%; + overflow-y: scroll; + max-height: 250px; + } +` +function CodeListView (_parent, _codeManager) { + this.event = new EventManager() + this.parent = _parent + this.codeManager = _codeManager + this.code + this.address + this.codeView + this.itemSelected + this.basicPanel = new DropdownPanel('Instructions', {json: false}) + this.basicPanel.event.register('hide', () => { + this.event.trigger('hide', []) + }) + this.basicPanel.event.register('show', () => { + this.event.trigger('show', []) + }) + this.init() +} + +CodeListView.prototype.render = function () { + return yo`
${this.basicPanel.render({height: style.instructionsList.height})}
` +} + +CodeListView.prototype.init = function () { + var self = this + this.codeManager.event.register('changed', this, this.changed) + this.parent.event.register('traceUnloaded', this, function () { + self.changed([], '', -1) + }) +} + +CodeListView.prototype.indexChanged = function (index) { + if (index >= 0) { + if (this.itemSelected) { + this.itemSelected.removeAttribute('selected') + this.itemSelected.removeAttribute('style') + if (this.itemSelected.firstChild) { + this.itemSelected.firstChild.removeAttribute('style') + } + } + this.itemSelected = this.codeView.children[index] + this.itemSelected.setAttribute('style', 'background-color: ' + styles.rightPanel.debuggerTab.text_BgHighlight) + this.itemSelected.setAttribute('selected', 'selected') + if (this.itemSelected.firstChild) { + this.itemSelected.firstChild.setAttribute('style', 'margin-left: 2px') + } + this.codeView.scrollTop = this.itemSelected.offsetTop - parseInt(this.codeView.offsetTop) + } +} + +CodeListView.prototype.changed = function (code, address, index) { + if (this.address !== address) { + this.code = code + this.address = address + this.codeView = this.renderAssemblyItems() + this.basicPanel.setContent(this.codeView) + } + this.indexChanged(index) +} + +CodeListView.prototype.renderAssemblyItems = function () { + if (this.code) { + var codeView = this.code.map(function (item, i) { + return yo`
${item}
` + }) + return yo`
+ ${codeView} +
` + } +} + +module.exports = CodeListView diff --git a/remix-debugger/src/ui/DropdownPanel.js b/remix-debugger/src/ui/DropdownPanel.js new file mode 100644 index 0000000000..17337d49e9 --- /dev/null +++ b/remix-debugger/src/ui/DropdownPanel.js @@ -0,0 +1,201 @@ +'use strict' +var yo = require('yo-yo') +const copy = require('clipboard-copy') +var remixLib = require('remix-lib') +var TreeView = require('./TreeView') +var EventManager = remixLib.EventManager + +var csjs = require('csjs-inject') +var styleGuide = remixLib.ui.themeChooser +var styles = styleGuide.chooser() + +var css = csjs` + .title { + margin-top: 10px; + ${styles.rightPanel.debuggerTab.dropdown_Debugger}; + display: flex; + align-items: center; + } + .name { + font-weight: bold; + } + .nameDetail { + font-weight: bold; + margin-left: 3px; + } + .icon { + color: ${styles.rightPanel.debuggerTab.button_Debugger_icon_Color}; + margin-right: 5%; + } + .eyeButton { + margin: 3px; + } + .eyeButton:hover { + color: ${styles.rightPanel.debuggerTab.button_Debugger_icon_HoverColor}; + } + .dropdownpanel { + ${styles.rightPanel.debuggerTab.dropdown_Debugger}; + width: 100%; + } + .dropdownrawcontent { + padding: 2px; + word-break: break-all; + } + .message { + padding: 2px; + word-break: break-all; + } + .refresh { + display: none; + margin-left: 4px; + margin-top: 4px; + animation: spin 2s linear infinite; + } +` + +function DropdownPanel (_name, _opts) { + this.event = new EventManager() + if (!_opts) { + _opts = {} + } + this.name = _name + this.header = '' + this.json = _opts.json + if (this.json) { + this.treeView = new TreeView(_opts) + } + this.view +} + +DropdownPanel.prototype.setMessage = function (message) { + if (this.view) { + this.view.querySelector('.dropdownpanel .dropdownrawcontent').style.display = 'none' + this.view.querySelector('.dropdownpanel .dropdowncontent').style.display = 'none' + this.view.querySelector('.dropdownpanel .fa-refresh').style.display = 'none' + this.message(message) + } +} + +DropdownPanel.prototype.setLoading = function () { + if (this.view) { + this.view.querySelector('.dropdownpanel .dropdownrawcontent').style.display = 'none' + this.view.querySelector('.dropdownpanel .dropdowncontent').style.display = 'none' + this.view.querySelector('.dropdownpanel .fa-refresh').style.display = 'inline-block' + this.message('') + } +} + +DropdownPanel.prototype.setUpdating = function () { + if (this.view) { + this.view.querySelector('.dropdownpanel .dropdowncontent').style.color = styles.appProperties.greyedText_color + } +} + +DropdownPanel.prototype.update = function (_data, _header) { + if (this.view) { + this.view.querySelector('.dropdownpanel .fa-refresh').style.display = 'none' + this.view.querySelector('.dropdownpanel .dropdowncontent').style.display = 'block' + this.view.querySelector('.dropdownpanel .dropdowncontent').style.color = styles.appProperties.mainText_Color + this.view.querySelector('.dropdownpanel .dropdownrawcontent').innerText = JSON.stringify(_data, null, '\t') + this.view.querySelector('.title div.btn').style.display = 'block' + this.view.querySelector('.title span').innerText = _header || ' ' + this.message('') + if (this.json) { + this.treeView.update(_data) + } + } +} + +DropdownPanel.prototype.setContent = function (node) { + if (this.view) { + var parent = this.view.querySelector('.dropdownpanel div.dropdowncontent') + parent.replaceChild(node, parent.firstElementChild) + } +} + +DropdownPanel.prototype.render = function (overridestyle) { + var content = yo`
Empty
` + if (this.json) { + content = this.treeView.render({}) + } + overridestyle === undefined ? {} : overridestyle + var self = this + var view = yo` +
+ +
+
+
${this.name}
+
+
+ +
` + if (!this.view) { + this.view = view + } + return view +} + +DropdownPanel.prototype.copyClipboard = function () { + var content = this.view.querySelector('.dropdownpanel .dropdownrawcontent') + if (content) copy(content.innerText ? content.innerText : content.textContent) +} + +DropdownPanel.prototype.toggle = function () { + var el = this.view.querySelector('.dropdownpanel') + var caret = this.view.querySelector('.title').firstElementChild + if (el.style.display === '') { + el.style.display = 'none' + caret.className = `${css.icon} fa fa-caret-right` + this.event.trigger('hide', []) + } else { + el.style.display = '' + caret.className = `${css.icon} fa fa-caret-down` + this.event.trigger('show', []) + } +} + +DropdownPanel.prototype.hide = function () { + if (this.view) { + var caret = this.view.querySelector('.title').firstElementChild + var el = this.view.querySelector('.dropdownpanel') + el.style.display = 'none' + caret.className = `${css.icon} fa fa-caret-right` + this.event.trigger('hide', []) + } +} + +DropdownPanel.prototype.show = function () { + if (this.view) { + var caret = this.view.querySelector('.title').firstElementChild + var el = this.view.querySelector('.dropdownpanel') + el.style.display = '' + caret.className = `${css.icon} fa fa-caret-down` + this.event.trigger('show', []) + } +} + +DropdownPanel.prototype.message = function (message) { + if (this.view) { + var mes = this.view.querySelector('.dropdownpanel .message') + mes.innerText = message + mes.style.display = (message === '') ? 'none' : 'block' + } +} + +module.exports = DropdownPanel diff --git a/remix-debugger/src/ui/Ethdebugger.js b/remix-debugger/src/ui/Ethdebugger.js new file mode 100644 index 0000000000..b799c0cc59 --- /dev/null +++ b/remix-debugger/src/ui/Ethdebugger.js @@ -0,0 +1,185 @@ +'use strict' +var TxBrowser = require('./TxBrowser') +var StepManager = require('./StepManager') +var remixCore = require('remix-core') +var TraceManager = remixCore.trace.TraceManager +var VmDebugger = require('./VmDebugger') +var remixLib = require('remix-lib') +var global = remixLib.global +var init = remixLib.init +var executionContext = remixLib.execution.executionContext +var EventManager = remixLib.EventManager +var yo = require('yo-yo') +var csjs = require('csjs-inject') +var Web3Providers = remixLib.vm.Web3Providers +var DummyProvider = remixLib.vm.DummyProvider +var CodeManager = remixCore.code.CodeManager +var remixSolidity = require('remix-solidity') +var SolidityProxy = remixSolidity.SolidityProxy +var InternalCallTree = remixSolidity.InternalCallTree + +var css = csjs` + .statusMessage { + margin-left: 15px; + } + .innerShift { + padding: 2px; + margin-left: 10px; + } +` + +function Ethdebugger (opts) { + this.opts = opts || {} + if (!this.opts.compilationResult) this.opts.compilationResult = () => { return null } + + var self = this + this.event = new EventManager() + + this.currentStepIndex = -1 + this.tx + this.statusMessage = '' + + this.view + this.web3Providers = new Web3Providers() + this.addProvider('DUMMYWEB3', new DummyProvider()) + this.switchProvider('DUMMYWEB3') + this.traceManager = new TraceManager() + this.codeManager = new CodeManager(this.traceManager) + this.solidityProxy = new SolidityProxy(this.traceManager, this.codeManager) + + var callTree = new InternalCallTree(this.event, this.traceManager, this.solidityProxy, this.codeManager, { includeLocalVariables: true }) + this.callTree = callTree // TODO: currently used by browser solidity, we should improve the API + + this.event.register('indexChanged', this, function (index) { + self.codeManager.resolveStep(index, self.tx) + }) + + this.txBrowser = new TxBrowser(this) + this.txBrowser.event.register('newTxLoading', this, function () { + self.unLoad() + }) + this.txBrowser.event.register('newTraceRequested', this, function (blockNumber, txIndex, tx) { + self.startDebugging(blockNumber, txIndex, tx) + }) + this.txBrowser.event.register('unloadRequested', this, function (blockNumber, txIndex, tx) { + self.unLoad() + }) + this.stepManager = new StepManager(this, this.traceManager) + this.stepManager.event.register('stepChanged', this, function (stepIndex) { + self.stepChanged(stepIndex) + }) + this.vmDebugger = new VmDebugger(this, this.traceManager, this.codeManager, this.solidityProxy, callTree) + + this.codeManager.event.register('changed', this, (code, address, instIndex) => { + this.callTree.sourceLocationTracker.getSourceLocationFromVMTraceIndex(address, this.currentStepIndex, this.solidityProxy.contracts, (error, sourceLocation) => { + if (!error) { + this.event.trigger('sourceLocationChanged', [sourceLocation]) + } + }) + }) +} + +Ethdebugger.prototype.setBreakpointManager = function (breakpointManager) { + this.breakpointManager = breakpointManager +} + +Ethdebugger.prototype.web3 = function () { + return global.web3 +} + +Ethdebugger.prototype.addProvider = function (type, obj) { + this.web3Providers.addProvider(type, obj) + this.event.trigger('providerAdded', [type]) +} + +Ethdebugger.prototype.switchProvider = function (type) { + var self = this + this.web3Providers.get(type, function (error, obj) { + if (error) { + console.log('provider ' + type + ' not defined') + } else { + global.web3 = obj + executionContext.detectNetwork((error, network) => { + if (error || !network) { + global.web3Debug = obj + } else { + var webDebugNode = init.web3DebugNode(network.name) + global.web3Debug = !webDebugNode ? obj : webDebugNode + } + }) + self.event.trigger('providerChanged', [type]) + } + }) +} + +Ethdebugger.prototype.setCompilationResult = function (compilationResult) { + if (compilationResult && compilationResult.sources && compilationResult.contracts) { + this.solidityProxy.reset(compilationResult) + } else { + this.solidityProxy.reset({}) + } +} + +Ethdebugger.prototype.debug = function (tx) { + this.setCompilationResult(this.opts.compilationResult()) + if (tx instanceof Object) { + this.txBrowser.load(tx.hash) + } else if (tx instanceof String) { + this.txBrowser.load(tx) + } +} + +Ethdebugger.prototype.render = function () { + var view = yo`
+
+ ${this.txBrowser.render()} + ${this.stepManager.render()} +
+
${this.statusMessage}
+ ${this.vmDebugger.render()} +
` + if (!this.view) { + this.view = view + } + return view +} + +Ethdebugger.prototype.unLoad = function () { + this.traceManager.init() + this.codeManager.clear() + this.stepManager.reset() + this.event.trigger('traceUnloaded') +} + +Ethdebugger.prototype.stepChanged = function (stepIndex) { + this.currentStepIndex = stepIndex + this.event.trigger('indexChanged', [stepIndex]) +} + +Ethdebugger.prototype.startDebugging = function (blockNumber, txIndex, tx) { + if (this.traceManager.isLoading) { + return + } + this.setCompilationResult(this.opts.compilationResult()) + this.statusMessage = 'Loading trace...' + yo.update(this.view, this.render()) + console.log('loading trace...') + this.tx = tx + var self = this + this.traceManager.resolveTrace(tx, function (error, result) { + console.log('trace loaded ' + result) + if (result) { + self.statusMessage = '' + yo.update(self.view, self.render()) + self.event.trigger('newTraceLoaded', [self.traceManager.trace]) + if (self.breakpointManager && self.breakpointManager.hasBreakpoint()) { + self.breakpointManager.jumpNextBreakpoint(false) + } + } else { + self.statusMessage = error ? error.message : 'Trace not loaded' + yo.update(self.view, self.render()) + } + }) +} + +module.exports = Ethdebugger diff --git a/src/ui/FullStoragesChanges.js b/remix-debugger/src/ui/FullStoragesChanges.js similarity index 54% rename from src/ui/FullStoragesChanges.js rename to remix-debugger/src/ui/FullStoragesChanges.js index 3ddf9f0dbb..ef454851c2 100644 --- a/src/ui/FullStoragesChanges.js +++ b/remix-debugger/src/ui/FullStoragesChanges.js @@ -1,14 +1,17 @@ 'use strict' -var BasicPanel = require('./BasicPanel') +var DropdownPanel = require('./DropdownPanel') +var remixCore = require('remix-core') +var StorageViewer = remixCore.storage.StorageViewer var yo = require('yo-yo') function FullStoragesChanges (_parent, _traceManager) { + this.storageResolver = null this.parent = _parent this.traceManager = _traceManager this.addresses = [] this.view this.traceLength - this.basicPanel = new BasicPanel('Full Storages Changes', '1205px', '100px') + this.basicPanel = new DropdownPanel('Full Storages Changes', {json: true}) this.init() } @@ -20,24 +23,14 @@ FullStoragesChanges.prototype.render = function () { return view } -FullStoragesChanges.prototype.hide = function () { - this.view.style.display = 'none' -} - -FullStoragesChanges.prototype.show = function () { - this.view.style.display = 'block' -} - FullStoragesChanges.prototype.init = function () { var self = this - this.parent.register('newTraceLoaded', this, function (length) { + this.parent.event.register('newTraceLoaded', this, function (length) { self.panels = [] self.traceManager.getAddresses(function (error, addresses) { if (!error) { self.addresses = addresses - self.basicPanel.data = '' - yo.update(self.view, self.render()) - self.hide() + self.basicPanel.update({}) } }) @@ -48,24 +41,29 @@ FullStoragesChanges.prototype.init = function () { }) }) - this.parent.register('indexChanged', this, function (index) { + this.parent.event.register('indexChanged', this, function (index) { if (index < 0) return if (self.parent.currentStepIndex !== index) return + if (!self.storageResolver) return if (index === self.traceLength - 1) { var storageJSON = {} for (var k in self.addresses) { - self.traceManager.getStorageAt(index, null, function (error, result) { + var address = self.addresses[k] + var storageViewer = new StorageViewer({ + stepIndex: self.parent.currentStepIndex, + tx: self.parent.tx, + address: address + }, self.storageResolver, self.traceManager) + storageViewer.storageRange(function (error, result) { if (!error) { - storageJSON[self.addresses[k]] = result - self.basicPanel.data = JSON.stringify(storageJSON, null, '\t') - yo.update(self.view, self.render()) - self.show() + storageJSON[address] = result + self.basicPanel.update(storageJSON) } - }, self.addresses[k]) + }) } } else { - self.hide() + self.basicPanel.update({}) } }) } diff --git a/src/ui/MemoryPanel.js b/remix-debugger/src/ui/MemoryPanel.js similarity index 62% rename from src/ui/MemoryPanel.js rename to remix-debugger/src/ui/MemoryPanel.js index 0a7e6a8ef7..c37cf48b2c 100644 --- a/src/ui/MemoryPanel.js +++ b/remix-debugger/src/ui/MemoryPanel.js @@ -1,12 +1,17 @@ 'use strict' -var BasicPanel = require('./BasicPanel') -var util = require('../helpers/ui') +var DropdownPanel = require('./DropdownPanel') +var remixLib = require('remix-lib') +var ui = remixLib.helpers.ui var yo = require('yo-yo') function MemoryPanel (_parent, _traceManager) { this.parent = _parent this.traceManager = _traceManager - this.basicPanel = new BasicPanel('Memory') + this.basicPanel = new DropdownPanel('Memory', { + json: true, + css: { + 'font-family': 'monospace' + }}) this.init() } @@ -16,18 +21,17 @@ MemoryPanel.prototype.render = function () { MemoryPanel.prototype.init = function () { var self = this - this.parent.register('indexChanged', this, function (index) { + this.parent.event.register('indexChanged', this, function (index) { if (index < 0) return if (self.parent.currentStepIndex !== index) return self.traceManager.getMemoryAt(index, function (error, memory) { if (error) { console.log(error) - self.basicPanel.data = '' + self.basicPanel.update({}) } else if (self.parent.currentStepIndex === index) { - self.basicPanel.data = util.formatMemory(memory, 16) + self.basicPanel.update(ui.formatMemory(memory, 16)) } - self.basicPanel.update() }) }) } diff --git a/remix-debugger/src/ui/Slider.js b/remix-debugger/src/ui/Slider.js new file mode 100644 index 0000000000..10ca255d81 --- /dev/null +++ b/remix-debugger/src/ui/Slider.js @@ -0,0 +1,75 @@ +'use strict' +var remixLib = require('remix-lib') +var EventManager = remixLib.EventManager +var yo = require('yo-yo') + +class Slider { + constructor (_traceManager, _stepOverride) { + this.event = new EventManager() + this.traceManager = _traceManager + this.max + this.disabled = true + this.view + this.solidityMode = false + this.stepOverride = _stepOverride + + this.previousValue = null + } + + render () { + var self = this + var view = yo`
+ +
` + if (!this.view) { + this.view = view + } + return view + } + + init (length) { + var slider = this.view.querySelector('#slider') + slider.setAttribute('max', length - 1) + this.max = length - 1 + this.updateDisabled(length === 0) + this.disabled = length === 0 + this.setValue(0) + } + + onChange (event) { + var value = parseInt(this.view.querySelector('#slider').value) + if (this.stepOverride) { + var correctedValue = this.stepOverride(value) + if (correctedValue !== value) { + this.setValue(correctedValue) + value = correctedValue + } + } + if (value === this.previousValue) return + this.previousValue = value + this.event.trigger('moved', [value]) + } + + setValue (value) { + this.view.querySelector('#slider').value = value + } + + updateDisabled (disabled) { + if (disabled) { + this.view.querySelector('#slider').setAttribute('disabled', true) + } else { + this.view.querySelector('#slider').removeAttribute('disabled') + } + } +} + +module.exports = Slider diff --git a/remix-debugger/src/ui/SolidityLocals.js b/remix-debugger/src/ui/SolidityLocals.js new file mode 100644 index 0000000000..3b8f6d91e0 --- /dev/null +++ b/remix-debugger/src/ui/SolidityLocals.js @@ -0,0 +1,80 @@ +'use strict' +var DropdownPanel = require('./DropdownPanel') +var remixSolidity = require('remix-solidity') +var localDecoder = remixSolidity.localDecoder +var solidityTypeFormatter = require('./SolidityTypeFormatter') +var remixCore = require('remix-core') +var StorageViewer = remixCore.storage.StorageViewer +var yo = require('yo-yo') + +class SolidityLocals { + + constructor (_parent, _traceManager, _internalTreeCall) { + this.parent = _parent + this.internalTreeCall = _internalTreeCall + this.storageResolver = null + this.traceManager = _traceManager + this.basicPanel = new DropdownPanel('Solidity Locals', { + json: true, + formatSelf: solidityTypeFormatter.formatSelf, + extractData: solidityTypeFormatter.extractData + }) + this.init() + this.view + } + + render () { + this.view = yo`
+ ${this.basicPanel.render()} +
` + return this.view + } + + init () { + var decodeTimeout = null + this.parent.event.register('sourceLocationChanged', this, (sourceLocation) => { + if (!this.storageResolver) { + this.basicPanel.setMessage('storage not ready') + return + } + if (decodeTimeout) { + window.clearTimeout(decodeTimeout) + } + this.basicPanel.setUpdating() + decodeTimeout = setTimeout(() => { + decode(this, sourceLocation) + }, 500) + }) + } +} + +function decode (self, sourceLocation) { + self.traceManager.waterfall([ + self.traceManager.getStackAt, + self.traceManager.getMemoryAt, + self.traceManager.getCurrentCalledAddressAt], + self.parent.currentStepIndex, + (error, result) => { + if (!error) { + var stack = result[0].value + var memory = result[1].value + try { + var storageViewer = new StorageViewer({ + stepIndex: self.parent.currentStepIndex, + tx: self.parent.tx, + address: result[2].value + }, self.storageResolver, self.traceManager) + localDecoder.solidityLocals(self.parent.currentStepIndex, self.internalTreeCall, stack, memory, storageViewer, sourceLocation).then((locals) => { + if (!locals.error) { + self.basicPanel.update(locals) + } + }) + } catch (e) { + self.basicPanel.setMessage(e.message) + } + } else { + console.log(error) + } + }) +} +module.exports = SolidityLocals diff --git a/remix-debugger/src/ui/SolidityState.js b/remix-debugger/src/ui/SolidityState.js new file mode 100644 index 0000000000..f69fc045f4 --- /dev/null +++ b/remix-debugger/src/ui/SolidityState.js @@ -0,0 +1,104 @@ +'use strict' +var DropdownPanel = require('./DropdownPanel') +var remixSolidity = require('remix-solidity') +var stateDecoder = remixSolidity.stateDecoder +var solidityTypeFormatter = require('./SolidityTypeFormatter') +var remixCore = require('remix-core') +var StorageViewer = remixCore.storage.StorageViewer +var yo = require('yo-yo') + +function SolidityState (_parent, _traceManager, _codeManager, _solidityProxy) { + this.storageResolver = null + this.parent = _parent + this.traceManager = _traceManager + this.codeManager = _codeManager + this.solidityProxy = _solidityProxy + this.basicPanel = new DropdownPanel('Solidity State', { + json: true, + formatSelf: solidityTypeFormatter.formatSelf, + extractData: solidityTypeFormatter.extractData + }) + this.init() + this.view + this.stateVariablesByAddresses = {} + _parent.event.register('traceUnloaded', () => { this.stateVariablesByAddresses = {} }) + _parent.event.register('newTraceLoaded', () => { this.stateVariablesByAddresses = {} }) +} + +SolidityState.prototype.render = function () { + if (!this.view) { + this.view = yo`
+ ${this.basicPanel.render()} +
` + } + return this.view +} + +SolidityState.prototype.init = function () { + var self = this + var decodeTimeout = null + this.parent.event.register('indexChanged', this, function (index) { + if (index < 0) { + self.basicPanel.setMessage('invalid step index') + return + } + + if (self.parent.currentStepIndex !== index) return + if (!self.solidityProxy.loaded()) { + self.basicPanel.setMessage('no source has been specified') + return + } + + if (!self.storageResolver) { + return + } + if (decodeTimeout) { + window.clearTimeout(decodeTimeout) + } + self.basicPanel.setUpdating() + decodeTimeout = setTimeout(() => { + decode(self, index) + }, 500) + }) +} + +function decode (self, index) { + self.traceManager.getCurrentCalledAddressAt(self.parent.currentStepIndex, (error, address) => { + if (error) { + self.basicPanel.update({}) + console.log(error) + } else { + if (self.stateVariablesByAddresses[address]) { + extractStateVariables(self, self.stateVariablesByAddresses[address], address) + } else { + self.solidityProxy.extractStateVariablesAt(index, function (error, stateVars) { + if (error) { + self.basicPanel.update({}) + console.log(error) + } else { + self.stateVariablesByAddresses[address] = stateVars + extractStateVariables(self, stateVars, address) + } + }) + } + } + }) +} + +function extractStateVariables (self, stateVars, address) { + var storageViewer = new StorageViewer({ + stepIndex: self.parent.currentStepIndex, + tx: self.parent.tx, + address: address + }, self.storageResolver, self.traceManager) + stateDecoder.decodeState(stateVars, storageViewer).then((result) => { + self.basicPanel.setMessage('') + if (!result.error) { + self.basicPanel.update(result) + } else { + self.basicPanel.setMessage(result.error) + } + }) +} + +module.exports = SolidityState diff --git a/remix-debugger/src/ui/SolidityTypeFormatter.js b/remix-debugger/src/ui/SolidityTypeFormatter.js new file mode 100644 index 0000000000..48c422418c --- /dev/null +++ b/remix-debugger/src/ui/SolidityTypeFormatter.js @@ -0,0 +1,71 @@ +'use strict' +var yo = require('yo-yo') +var BN = require('ethereumjs-util').BN + +module.exports = { + formatSelf: formatSelf, + extractData: extractData +} + +function formatSelf (key, data) { + var style = fontColor(data) + var keyStyle = data.isProperty ? 'color:#847979' : '' + if (data.type === 'string') { + data.self = JSON.stringify(data.self) + } + return yo`` +} + +function extractData (item, parent, key) { + var ret = {} + if (item.isProperty) { + return item + } + if (item.type.lastIndexOf(']') === item.type.length - 1) { + ret.children = (item.value || []).map(function (item, index) { + return {key: index, value: item} + }) + ret.children.unshift({ + key: 'length', + value: { + self: (new BN(item.length.replace('0x', ''), 16)).toString(10), + type: 'uint', + isProperty: true + } + }) + ret.isArray = true + ret.self = parent.isArray ? '' : item.type + } else if (item.type.indexOf('struct') === 0) { + ret.children = Object.keys((item.value || {})).map(function (key) { + return {key: key, value: item.value[key]} + }) + ret.self = item.type + ret.isStruct = true + } else if (item.type.indexOf('mapping') === 0) { + ret.children = Object.keys((item.value || {})).map(function (key) { + return {key: key, value: item.value[key]} + }) + ret.isMapping = true + ret.self = item.type + } else { + ret.children = null + ret.self = item.value + ret.type = item.type + } + return ret +} + +function fontColor (data) { + var color = '#124B46' + if (data.isArray || data.isStruct || data.isMapping) { + color = '#847979' + } else if (data.type.indexOf('uint') === 0 || + data.type.indexOf('int') === 0 || + data.type.indexOf('bool') === 0 || + data.type.indexOf('enum') === 0) { + color = '#0F0CE9' + } else if (data.type === 'string') { + color = '#E91E0C' + } + return 'color:' + color +} diff --git a/src/ui/StackPanel.js b/remix-debugger/src/ui/StackPanel.js similarity index 57% rename from src/ui/StackPanel.js rename to remix-debugger/src/ui/StackPanel.js index 956ea1fb65..de768dc616 100644 --- a/src/ui/StackPanel.js +++ b/remix-debugger/src/ui/StackPanel.js @@ -1,12 +1,11 @@ 'use strict' -var BasicPanel = require('./BasicPanel') -var ui = require('../helpers/ui') +var DropdownPanel = require('./DropdownPanel') var yo = require('yo-yo') function StackPanel (_parent, _traceManager) { this.parent = _parent this.traceManager = _traceManager - this.basicPanel = new BasicPanel('Stack') + this.basicPanel = new DropdownPanel('Stack', {json: true}) this.init() } @@ -16,29 +15,19 @@ StackPanel.prototype.render = function () { StackPanel.prototype.init = function () { var self = this - this.parent.register('indexChanged', this, function (index) { + this.parent.event.register('indexChanged', this, function (index) { if (index < 0) return if (self.parent.currentStepIndex !== index) return self.traceManager.getStackAt(index, function (error, stack) { if (error) { - self.basicPanel.data = '' + self.basicPanel.update({}) console.log(error) } else if (self.parent.currentStepIndex === index) { - self.basicPanel.data = self.format(stack) + self.basicPanel.update(stack) } - self.basicPanel.update() }) }) } -StackPanel.prototype.format = function (stack) { - var ret = '' - for (var key in stack) { - var hex = ui.normalizeHex(stack[key]) - ret += hex + '\n' - } - return ret -} - module.exports = StackPanel diff --git a/remix-debugger/src/ui/StepDetail.js b/remix-debugger/src/ui/StepDetail.js new file mode 100644 index 0000000000..88950e4e93 --- /dev/null +++ b/remix-debugger/src/ui/StepDetail.js @@ -0,0 +1,100 @@ +'use strict' +var yo = require('yo-yo') +var DropdownPanel = require('./DropdownPanel') + +function StepDetail (_parent, _traceManager) { + this.parent = _parent + this.traceManager = _traceManager + + this.basicPanel = new DropdownPanel('Step detail', {json: true}) + + this.detail = initDetail() + this.view + this.init() +} + +StepDetail.prototype.render = function () { + return yo`
${this.basicPanel.render()}
` +} + +StepDetail.prototype.init = function () { + var self = this + this.parent.event.register('traceUnloaded', this, function () { + self.detail = initDetail() + self.basicPanel.update(self.detail) + }) + + this.parent.event.register('newTraceLoaded', this, function () { + self.detail = initDetail() + self.basicPanel.update(self.detail) + }) + + this.parent.event.register('indexChanged', this, function (index) { + if (index < 0) return + + self.detail['vm trace step'] = index + + self.traceManager.getCurrentStep(index, function (error, step) { + if (error) { + console.log(error) + self.detail['execution step'] = '-' + } else { + self.detail['execution step'] = step + } + self.basicPanel.update(self.detail) + }) + + self.traceManager.getMemExpand(index, function (error, addmem) { + if (error) { + console.log(error) + self.detail['add memory'] = '-' + } else { + self.detail['add memory'] = addmem + } + self.basicPanel.update(self.detail) + }) + + self.traceManager.getStepCost(index, function (error, gas) { + if (error) { + console.log(error) + self.detail.gas = '-' + } else { + self.detail.gas = gas + } + self.basicPanel.update(self.detail) + }) + + self.traceManager.getCurrentCalledAddressAt(index, function (error, address) { + if (error) { + console.log(error) + self.detail['loaded address'] = '-' + } else { + self.detail['loaded address'] = address + } + self.basicPanel.update(self.detail) + }) + + self.traceManager.getRemainingGas(index, function (error, remaingas) { + if (error) { + console.log(error) + self.detail['remaining gas'] = '-' + } else { + self.detail['remaining gas'] = remaingas + } + self.basicPanel.update(self.detail) + }) + }) +} + +module.exports = StepDetail + +function initDetail () { + return { + 'vm trace step': '-', + 'execution step': '-', + 'add memory': '', + 'gas': '', + 'remaining gas': '-', + 'loaded address': '-' + } +} diff --git a/remix-debugger/src/ui/StepManager.js b/remix-debugger/src/ui/StepManager.js new file mode 100644 index 0000000000..eaa2ec0d85 --- /dev/null +++ b/remix-debugger/src/ui/StepManager.js @@ -0,0 +1,204 @@ +'use strict' +var ButtonNavigator = require('./ButtonNavigator') +var Slider = require('./Slider') +var remixLib = require('remix-lib') +var EventManager = remixLib.EventManager +var yo = require('yo-yo') +var util = remixLib.util + +function StepManager (_parent, _traceManager) { + this.event = new EventManager() + this.parent = _parent + this.traceManager = _traceManager + this.sourceMapByAddress = {} + this.solidityMode = false + + var self = this + this.parent.event.register('newTraceLoaded', this, function () { + self.traceManager.getLength(function (error, length) { + if (error) { + console.log(error) + } else { + self.slider.init(length) + self.init() + } + }) + }) + + this.slider = new Slider(this.traceManager, (step) => { + return this.solidityMode ? this.resolveToReducedTrace(step, 0) : step + }) + this.slider.event.register('moved', this, function (step) { + self.sliderMoved(step) + }) + + this.parent.callTree.event.register('callTreeReady', () => { + this.solidityMode = true + this.parent.vmDebugger.asmCode.event.register('hide', () => { + this.solidityMode = this.parent.callTree.reducedTrace.length !== 0 + }) + this.parent.vmDebugger.asmCode.event.register('show', () => { + this.solidityMode = false + }) + if (this.parent.callTree.functionCallStack.length) { + this.jumpTo(this.parent.callTree.functionCallStack[0]) + } + }) + + this.buttonNavigator = new ButtonNavigator(_parent, this.traceManager) + this.buttonNavigator.event.register('stepIntoBack', this, function () { + self.stepIntoBack() + }) + this.buttonNavigator.event.register('stepIntoForward', this, function () { + self.stepIntoForward() + }) + this.buttonNavigator.event.register('stepOverBack', this, function () { + self.stepOverBack() + }) + this.buttonNavigator.event.register('stepOverForward', this, function () { + self.stepOverForward() + }) + this.buttonNavigator.event.register('jumpOut', this, function () { + self.jumpOut() + }) + this.buttonNavigator.event.register('jumpToException', this, function (exceptionIndex) { + self.jumpTo(exceptionIndex) + }) + this.buttonNavigator.event.register('jumpNextBreakpoint', (exceptionIndex) => { + self.parent.breakpointManager.jumpNextBreakpoint(true) + }) + this.buttonNavigator.event.register('jumpPreviousBreakpoint', (exceptionIndex) => { + self.parent.breakpointManager.jumpPreviousBreakpoint(true) + }) +} + +StepManager.prototype.resolveToReducedTrace = function (value, incr) { + if (this.parent.callTree.reducedTrace.length) { + var nextSource = util.findClosestIndex(value, this.parent.callTree.reducedTrace) + nextSource = nextSource + incr + if (nextSource <= 0) { + nextSource = 0 + } else if (nextSource > this.parent.callTree.reducedTrace.length) { + nextSource = this.parent.callTree.reducedTrace.length - 1 + } + return this.parent.callTree.reducedTrace[nextSource] + } + return value +} + +StepManager.prototype.render = function () { + return ( + yo`
+ ${this.slider.render()} + ${this.buttonNavigator.render()} +
` + ) +} + +StepManager.prototype.reset = function () { + this.slider.setValue(0) + this.currentStepIndex = 0 + this.buttonNavigator.reset() +} + +StepManager.prototype.init = function () { + this.slider.setValue(0) + this.changeState(0) +} + +StepManager.prototype.newTraceAvailable = function () { + this.init() +} + +StepManager.prototype.jumpTo = function (step) { + if (!this.traceManager.inRange(step)) { + return + } + this.slider.setValue(step) + this.changeState(step) +} + +StepManager.prototype.sliderMoved = function (step) { + if (!this.traceManager.inRange(step)) { + return + } + this.changeState(step) +} + +StepManager.prototype.stepIntoForward = function () { + if (!this.traceManager.isLoaded()) { + return + } + var step = this.currentStepIndex + if (this.solidityMode) { + step = this.resolveToReducedTrace(step, 1) + } else { + step += 1 + } + if (!this.traceManager.inRange(step)) { + return + } + this.slider.setValue(step) + this.changeState(step) +} + +StepManager.prototype.stepIntoBack = function () { + if (!this.traceManager.isLoaded()) { + return + } + var step = this.currentStepIndex + if (this.solidityMode) { + step = this.resolveToReducedTrace(step, -1) + } else { + step -= 1 + } + if (!this.traceManager.inRange(step)) { + return + } + this.slider.setValue(step) + this.changeState(step) +} + +StepManager.prototype.stepOverForward = function () { + if (!this.traceManager.isLoaded()) { + return + } + var step = this.traceManager.findStepOverForward(this.currentStepIndex) + if (this.solidityMode) { + step = this.resolveToReducedTrace(step, 1) + } + this.slider.setValue(step) + this.changeState(step) +} + +StepManager.prototype.stepOverBack = function () { + if (!this.traceManager.isLoaded()) { + return + } + var step = this.traceManager.findStepOverBack(this.currentStepIndex) + if (this.solidityMode) { + step = this.resolveToReducedTrace(step, -1) + } + this.slider.setValue(step) + this.changeState(step) +} + +StepManager.prototype.jumpOut = function () { + if (!this.traceManager.isLoaded()) { + return + } + var step = this.traceManager.findStepOut(this.currentStepIndex) + if (this.solidityMode) { + step = this.resolveToReducedTrace(step, 0) + } + this.slider.setValue(step) + this.changeState(step) +} + +StepManager.prototype.changeState = function (step) { + this.currentStepIndex = step + this.buttonNavigator.stepChanged(step) + this.event.trigger('stepChanged', [step]) +} + +module.exports = StepManager diff --git a/remix-debugger/src/ui/StoragePanel.js b/remix-debugger/src/ui/StoragePanel.js new file mode 100644 index 0000000000..cd4573e4f7 --- /dev/null +++ b/remix-debugger/src/ui/StoragePanel.js @@ -0,0 +1,50 @@ +'use strict' +var DropdownPanel = require('./DropdownPanel') +var remixCore = require('remix-core') +var StorageViewer = remixCore.storage.StorageViewer +var yo = require('yo-yo') + +function StoragePanel (_parent, _traceManager) { + this.parent = _parent + this.storageResolver = null + this.traceManager = _traceManager + this.basicPanel = new DropdownPanel('Storage', {json: true}) + this.init() + this.disabled = false +} + +StoragePanel.prototype.render = function () { + return yo`
${this.basicPanel.render()}
` +} + +StoragePanel.prototype.init = function () { + var self = this + this.parent.event.register('indexChanged', this, function (index) { + if (self.disabled) return + if (index < 0) return + if (self.parent.currentStepIndex !== index) return + if (!self.storageResolver) return + + this.traceManager.getCurrentCalledAddressAt(index, (error, address) => { + if (!error) { + var storageViewer = new StorageViewer({ + stepIndex: self.parent.currentStepIndex, + tx: self.parent.tx, + address: address + }, self.storageResolver, self.traceManager) + + storageViewer.storageRange((error, storage) => { + if (error) { + console.log(error) + self.basicPanel.update({}) + } else if (self.parent.currentStepIndex === index) { + var header = storageViewer.isComplete(address) ? 'completely loaded' : 'partially loaded...' + self.basicPanel.update(storage, header) + } + }) + } + }) + }) +} + +module.exports = StoragePanel diff --git a/remix-debugger/src/ui/TreeView.js b/remix-debugger/src/ui/TreeView.js new file mode 100644 index 0000000000..7601a707f3 --- /dev/null +++ b/remix-debugger/src/ui/TreeView.js @@ -0,0 +1,186 @@ +'use strict' +var yo = require('yo-yo') +var csjs = require('csjs-inject') +var css = csjs` + .li_tv { + list-style-type: none; + -webkit-margin-before: 0px; + -webkit-margin-after: 0px; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; + -webkit-padding-start: 0px; + margin-left: 10px; + } + .ul_tv { + list-style-type: none; + -webkit-margin-before: 0px; + -webkit-margin-after: 0px; + -webkit-margin-start: 0px; + -webkit-margin-end: 0px; + -webkit-padding-start: 0px; + } + .caret_tv { + width: 10px; + } + .label_tv { + display: flex; + align-items: center; + } +` + +var remixLib = require('remix-lib') +var EventManager = remixLib.EventManager + +/** + * TreeView + * - extendable by specifying custom `extractData` and `formatSelf` function + * - trigger `nodeClick` and `leafClick` + */ +class TreeView { + + constructor (opts) { + this.event = new EventManager() + this.extractData = opts.extractData || this.extractDataDefault + this.formatSelf = opts.formatSelf || this.formatSelfDefault + this.view = null + } + + render (json, expand) { + var view = this.renderProperties(json, expand) + if (!this.view) { + this.view = view + } + return view + } + + update (json) { + if (this.view) { + yo.update(this.view, this.render(json)) + } + } + + renderObject (item, parent, key, expand, keyPath) { + var data = this.extractData(item, parent, key) + var children = (data.children || []).map((child, index) => { + return this.renderObject(child.value, data, child.key, expand, keyPath + '/' + child.key) + }) + return this.formatData(key, data, children, expand, keyPath) + } + + renderProperties (json, expand, key) { + key = key || '' + var children = Object.keys(json).map((innerkey) => { + return this.renderObject(json[innerkey], json, innerkey, expand, innerkey) + }) + return yo`
    ${children}
` + } + + formatData (key, data, children, expand, keyPath) { + var self = this + var li = yo`
  • ` + var caret = yo`
    ` + var label = yo` +
    + ${caret} + ${self.formatSelf(key, data, li)} +
    ` + li.appendChild(label) + if (data.children) { + var list = yo`
      ${children}
    ` + list.style.display = 'none' + caret.className = list.style.display === 'none' ? `fa fa-caret-right caret ${css.caret_tv}` : `fa fa-caret-down caret ${css.caret_tv}` + label.onclick = function () { + self.expand(keyPath) + } + label.oncontextmenu = function (event) { + self.event.trigger('nodeRightClick', [keyPath, data, label, event]) + } + li.appendChild(list) + } else { + caret.style.visibility = 'hidden' + label.oncontextmenu = function (event) { + self.event.trigger('leafRightClick', [keyPath, data, label, event]) + } + label.onclick = function (event) { + self.event.trigger('leafClick', [keyPath, data, label, event]) + } + } + return li + } + + isExpanded (path) { + var current = this.nodeAt(path) + if (current) { + return current.style.display !== 'none' + } + return false + } + + expand (path) { + var caret = this.caretAt(path) + var node = this.nodeAt(path) + if (node) { + node.style.display = node.style.display === 'none' ? 'block' : 'none' + caret.className = node.style.display === 'none' ? `fa fa-caret-right caret ${css.caret_tv}` : `fa fa-caret-down caret ${css.caret_tv}` + this.event.trigger('nodeClick', [path, node]) + } + } + + caretAt (path) { + var label = this.labelAt(path) + if (label) { + return label.querySelector('.caret') + } + } + + itemAt (path) { + return this.view.querySelector(`li[key="${path}"]`) + } + + labelAt (path) { + return this.view.querySelector(`div[key="${path}"]`) + } + + nodeAt (path) { + return this.view.querySelector(`ul[key="${path}"]`) + } + + updateNodeFromJSON (path, jsonTree, expand) { + var newTree = this.renderProperties(jsonTree, expand, path) + var current = this.nodeAt(path) + if (current && current.parentElement) { + current.parentElement.replaceChild(newTree, current) + } + } + + formatSelfDefault (key, data) { + return yo`` + } + + extractDataDefault (item, parent, key) { + var ret = {} + if (item instanceof Array) { + ret.children = item.map((item, index) => { + return {key: index, value: item} + }) + ret.self = 'Array' + ret.isNode = true + ret.isLeaf = false + } else if (item instanceof Object) { + ret.children = Object.keys(item).map((key) => { + return {key: key, value: item[key]} + }) + ret.self = 'Object' + ret.isNode = true + ret.isLeaf = false + } else { + ret.self = item + ret.children = null + ret.isNode = false + ret.isLeaf = true + } + return ret + } +} + +module.exports = TreeView diff --git a/remix-debugger/src/ui/TxBrowser.js b/remix-debugger/src/ui/TxBrowser.js new file mode 100644 index 0000000000..2d90132d26 --- /dev/null +++ b/remix-debugger/src/ui/TxBrowser.js @@ -0,0 +1,211 @@ +var remixLib = require('remix-lib') +var global = remixLib.global +var EventManager = remixLib.EventManager +var traceHelper = remixLib.helpers.trace +var yo = require('yo-yo') +var init = remixLib.init +var DropdownPanel = require('./DropdownPanel') +var csjs = require('csjs-inject') +var styleGuide = remixLib.ui.themeChooser +var styles = styleGuide.chooser() + +var css = csjs` + .container { + display: flex; + flex-direction: column; + } + .txContainer { + display: flex; + flex-direction: column; + } + .txinputs { + width: 100%; + display: flex; + justify-content: center; + } + .txinput { + ${styles.rightPanel.debuggerTab.input_Debugger} + min-width: 30px; + margin: 3px; + } + .txbuttons { + width: 100%; + display: flex; + justify-content: center; + } + .txbutton { + ${styles.rightPanel.debuggerTab.button_Debugger} + } + .txbutton:hover { + color: ${styles.rightPanel.debuggerTab.button_Debugger_icon_HoverColor}; + } + .txinfo { + margin-top: 5px; + } + .vmargin { + margin-top: 10px; + margin-bottom: 10px; + } +` +function TxBrowser (_parent) { + this.event = new EventManager() + + this.blockNumber + this.txNumber + this.view + this.displayConnectionSetting = true + this.basicPanel = new DropdownPanel('Transaction', {json: true}) + this.basicPanel.data = {} + var self = this + _parent.event.register('providerChanged', this, function (provider) { + self.displayConnectionSetting = provider === 'INTERNAL' + self.setDefaultValues() + if (self.view) { + yo.update(self.view, self.render()) + } + }) +} + +// creation 0xa9619e1d0a35b2c1d686f5b661b3abd87f998d2844e8e9cc905edb57fc9ce349 +// invokation 0x71a6d583d16d142c5c3e8903060e8a4ee5a5016348a9448df6c3e63b68076ec4 0xcda2b2835add61af54cf83bd076664d98d7908c6cd98d86423b3b48d8b8e51ff +// test: +// creation: 0x72908de76f99fca476f9e3a3b5d352f350a98cd77d09cebfc59ffe32a6ecaa0b +// invokation: 0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51 + +TxBrowser.prototype.setDefaultValues = function () { + this.connectInfo = '' + this.basicPanel.update({}) + this.basicPanel.hide() + if (this.view) { + yo.update(this.view, this.render()) + } +} + +TxBrowser.prototype.submit = function () { + if (!this.txNumber) { + return + } + this.event.trigger('newTxLoading', [this.blockNumber, this.txNumber]) + try { + var self = this + if (this.txNumber.indexOf('0x') !== -1) { + global.web3.eth.getTransaction(this.txNumber, function (error, result) { + self.update(error, result) + }) + } else { + global.web3.eth.getTransactionFromBlock(this.blockNumber, this.txNumber, function (error, result) { + self.update(error, result) + }) + } + } catch (e) { + self.update(e.message) + } +} + +TxBrowser.prototype.update = function (error, tx) { + var info = {} + if (error) { + this.view.querySelector('#error').innerHTML = error + } else { + if (tx) { + this.view.querySelector('#error').innerHTML = '' + if (!tx.to) { + tx.to = traceHelper.contractCreationToken('0') + } + info.from = tx.from + info.to = tx.to + info.hash = tx.hash + this.event.trigger('newTraceRequested', [this.blockNumber, this.txNumber, tx]) + } else { + var mes = '' + info.from = mes + info.to = mes + info.hash = mes + this.view.querySelector('#error').innerHTML = 'Cannot find transaction with reference. Block number: ' + this.blockNumber + '. Transaction index/hash: ' + this.txNumber + } + } + this.basicPanel.update(info) +} + +TxBrowser.prototype.updateWeb3Url = function (newhost) { + init.setProvider(global.web3, newhost) + var self = this + this.checkWeb3(function (error, block) { + if (!error) { + self.connectInfo = 'Connected to ' + global.web3.currentProvider.host + '. Current block number: ' + block + } else { + self.connectInfo = 'Unable to connect to ' + global.web3.currentProvider.host + '. ' + error.message + } + yo.update(self.view, self.render()) + }) +} + +TxBrowser.prototype.checkWeb3 = function (callback) { + try { + global.web3.eth.getBlockNumber(function (error, block) { + callback(error, block) + }) + } catch (e) { + console.log(e) + callback(e.message, null) + } +} + +TxBrowser.prototype.updateBlockN = function (ev) { + this.blockNumber = ev.target.value +} + +TxBrowser.prototype.updateTxN = function (ev) { + this.txNumber = ev.target.value +} + +TxBrowser.prototype.load = function (txHash) { + this.txNumber = txHash + this.submit() +} + +TxBrowser.prototype.unload = function (txHash) { + this.event.trigger('unloadRequested') + this.init() +} + +TxBrowser.prototype.init = function (ev) { + this.setDefaultValues() +} + +TxBrowser.prototype.connectionSetting = function () { + if (this.displayConnectionSetting) { + var self = this + return yo`
    Node URL: + ${this.connectInfo}
    ` + } else { + return '' + } +} + +TxBrowser.prototype.render = function () { + var self = this + var view = yo`
    + ${this.connectionSetting()} +
    +
    + + +
    +
    + + +
    +
    + +
    + ${this.basicPanel.render()} +
    +
    ` + if (!this.view) { + this.view = view + } + return view +} + +module.exports = TxBrowser diff --git a/remix-debugger/src/ui/VmDebugger.js b/remix-debugger/src/ui/VmDebugger.js new file mode 100644 index 0000000000..c372c7b9f5 --- /dev/null +++ b/remix-debugger/src/ui/VmDebugger.js @@ -0,0 +1,90 @@ +'use strict' +var CodeListView = require('./CodeListView') +var CalldataPanel = require('./CalldataPanel') +var MemoryPanel = require('./MemoryPanel') +var CallstackPanel = require('./CallstackPanel') +var StackPanel = require('./StackPanel') +var StoragePanel = require('./StoragePanel') +var FullStoragesChangesPanel = require('./FullStoragesChanges') +var StepDetail = require('./StepDetail') +var DropdownPanel = require('./DropdownPanel') +var SolidityState = require('./SolidityState') +var SolidityLocals = require('./SolidityLocals') +var remixCore = require('remix-core') +var StorageResolver = remixCore.storage.StorageResolver +var yo = require('yo-yo') + +function VmDebugger (_parent, _traceManager, _codeManager, _solidityProxy, _callTree) { + this.asmCode = new CodeListView(_parent, _codeManager) + this.stackPanel = new StackPanel(_parent, _traceManager) + this.storagePanel = new StoragePanel(_parent, _traceManager) + this.memoryPanel = new MemoryPanel(_parent, _traceManager) + this.calldataPanel = new CalldataPanel(_parent, _traceManager) + this.callstackPanel = new CallstackPanel(_parent, _traceManager) + this.stepDetail = new StepDetail(_parent, _traceManager) + this.solidityState = new SolidityState(_parent, _traceManager, _codeManager, _solidityProxy) + this.solidityLocals = new SolidityLocals(_parent, _traceManager, _callTree) + + /* Return values - */ + this.returnValuesPanel = new DropdownPanel('Return Value', {json: true}) + this.returnValuesPanel.data = {} + _parent.event.register('indexChanged', this.returnValuesPanel, function (index) { + var self = this + _traceManager.getReturnValue(index, function (error, returnValue) { + if (error) { + self.update([error]) + } else if (_parent.currentStepIndex === index) { + self.update([returnValue]) + } + }) + }) + /* Return values - */ + + this.fullStoragesChangesPanel = new FullStoragesChangesPanel(_parent, _traceManager) + + this.view + var self = this + _parent.event.register('newTraceLoaded', this, function () { + var storageResolver = new StorageResolver() + self.storagePanel.storageResolver = storageResolver + self.solidityState.storageResolver = storageResolver + self.solidityLocals.storageResolver = storageResolver + self.fullStoragesChangesPanel.storageResolver = storageResolver + self.view.style.display = 'block' + }) + _parent.event.register('traceUnloaded', this, function () { + self.view.style.display = 'none' + }) + _parent.callTree.event.register('callTreeReady', () => { + if (_parent.callTree.reducedTrace.length) { + self.solidityLocals.basicPanel.show() + self.solidityState.basicPanel.show() + } else { + self.asmCode.basicPanel.show() + } + }) +} + +VmDebugger.prototype.render = function () { + var view = yo`` + if (!this.view) { + this.view = view + } + return view +} + +module.exports = VmDebugger diff --git a/remix-debugger/src/ui/styles/basicStyles.js b/remix-debugger/src/ui/styles/basicStyles.js new file mode 100644 index 0000000000..c824703d3f --- /dev/null +++ b/remix-debugger/src/ui/styles/basicStyles.js @@ -0,0 +1,84 @@ +'use strict' +module.exports = { + truncate: { + 'white-space': 'nowrap', + 'overflow': 'hidden', + 'text-overflow': 'ellipsis', + 'margin-right': '5px' + }, + font: { + 'font-family': 'arial,sans-serif' + }, + innerShift: { + 'padding': '2px', + 'margin-left': '10px' + }, + container: { + 'margin': '10px', + 'padding': '5px' + }, + statusMessage: { + 'margin-left': '15px' + }, + address: { + 'font-style': 'italic' + }, + instructionsList: { + 'width': '52%', + 'overflow-y': 'scroll', + 'max-height': '250px', + 'margin': '0', + 'margin-left': '10px', + 'padding': '2px' + }, + transactionInfo: { + 'margin-top': '5px' + }, + panel: { + container: { + 'border': '1px solid', + 'width': '70%' + }, + tableContainer: { + 'height': '50%', + 'overflow-y': 'auto' + }, + table: { + 'padding': '5px' + }, + title: { + 'padding': '5px', + 'font-style': 'italic' + } + }, + hidden: { + 'display': 'none' + }, + display: { + 'display': 'block' + }, + inline: { + 'display': 'inline-block' + }, + vmargin: { + 'margin-top': '10px', + 'margin-bottom': '10px' + }, + button: { + 'border-color': 'transparent', + 'border-radius': '3px', + 'border': '.3px solid ${colors.veryLightGrey}', + 'cursor': 'pointer', + 'min-height': '25px', + 'max-height': '25px', + 'padding': '3px', + 'min-width': '100px', + 'font-size': '12px', + 'overflow': 'hidden', + 'word-break': 'normal', + 'background-color': 'hsla(0, 0%, 40%, .2)', + 'color': 'hsla(0, 0%, 40%, 1)', + 'margin': '3px', + 'text-decoration': 'none' + } +} diff --git a/remix-debugger/src/ui/styles/dropdownPanel.js b/remix-debugger/src/ui/styles/dropdownPanel.js new file mode 100644 index 0000000000..bb9a1e7f2b --- /dev/null +++ b/remix-debugger/src/ui/styles/dropdownPanel.js @@ -0,0 +1,31 @@ +'use strict' +module.exports = { + title: { + 'border': '1px solid #dadada', + 'background-color': 'white', + 'width': '100%', + 'color': '#363f47', + 'margin-top': '5px', + 'cursor': 'pointer' + }, + titleInner: { + 'display': 'inline-block' + }, + content: { + 'color': '#111111', + 'width': '100%', + 'min-height': '20px' + }, + inner: { + 'padding': '2px', + 'word-break': 'break-all' + }, + copyBtn: { + 'float': 'right', + 'margin-top': '3px' + }, + caret: { + 'margin-left': '10px', + 'margin-right': '10px' + } +} diff --git a/remix-debugger/src/ui/styles/sliderStyles.js b/remix-debugger/src/ui/styles/sliderStyles.js new file mode 100644 index 0000000000..45912b6380 --- /dev/null +++ b/remix-debugger/src/ui/styles/sliderStyles.js @@ -0,0 +1,6 @@ +'use strict' +module.exports = { + rule: { + 'width': '100%' + } +} diff --git a/remix-debugger/src/ui/styles/treeView.js b/remix-debugger/src/ui/styles/treeView.js new file mode 100644 index 0000000000..b50bacd24f --- /dev/null +++ b/remix-debugger/src/ui/styles/treeView.js @@ -0,0 +1,30 @@ +'use strict' +module.exports = { + cssUl: { + 'list-style-type': 'none', + '-webkit-margin-before': '0px', + '-webkit-margin-after': '0px', + '-webkit-margin-start': '0px', + '-webkit-margin-end': '0px', + '-webkit-padding-start': '0px' + }, + cssLi: { + 'list-style-type': 'none', + '-webkit-margin-before': '0px', + '-webkit-margin-after': '0px', + '-webkit-margin-start': '0px', + '-webkit-margin-end': '0px', + '-webkit-padding-start': '0px', + 'margin-left': '10px' + }, + label: { + 'vertical-align': 'top', + 'font-family': 'arial,sans-serif' + }, + caret: { + 'margin-top': '3px', + 'width': '10px' + }, + data: { + } +} diff --git a/remix-debugger/test-browser/resources/insertTestWeb3.js b/remix-debugger/test-browser/resources/insertTestWeb3.js new file mode 100644 index 0000000000..833c7198e1 --- /dev/null +++ b/remix-debugger/test-browser/resources/insertTestWeb3.js @@ -0,0 +1,77 @@ +/* global XMLHttpRequest */ +function loadJSON (url, callback) { + var xobj = new XMLHttpRequest() + xobj.overrideMimeType('application/json') + xobj.open('GET', url, true) + xobj.onreadystatechange = function () { + if (xobj.readyState === 4 && xobj.status === 200) { + callback(xobj.responseText) + } + } + xobj.send(null) +} + +function loadTestWeb3 (data) { + var container = document.getElementById('app') + var vmdebugger = container.debugger + var uiTestweb3 = {} + uiTestweb3.eth = {} + uiTestweb3.debug = {} + uiTestweb3.eth.getCode = function (address, callback) { + if (callback) { + callback(null, data.testCodes[address]) + } else { + return data.testCodes[address] + } + } + + uiTestweb3.debug.traceTransaction = function (txHash, options, callback) { + callback(null, data.testTraces[txHash]) + } + + uiTestweb3.debug.storageRangeAt = function (blockNumber, txIndex, address, start, size, callback) { + callback(null, { storage: {}, complete: true }) + } + + uiTestweb3.eth.getTransaction = function (txHash, callback) { + if (callback) { + callback(null, data.testTxs[txHash]) + } else { + return data.testTxs[txHash] + } + } + + uiTestweb3.eth.getTransactionFromBlock = function (blockNumber, txIndex, callback) { + if (callback) { + callback(null, data.testTxsByBlock[blockNumber + '-' + txIndex]) + } else { + return data.testTxsByBlock[blockNumber + '-' + txIndex] + } + } + + uiTestweb3.eth.getBlockNumber = function (callback) { callback(null, 'web3 modified for testing purposes :)') } + + uiTestweb3.providers = { 'HttpProvider': function (url) {} } + + uiTestweb3.setProvider = function (provider) {} + + uiTestweb3.currentProvider = {host: 'web3 modified for testing purposes :)'} + vmdebugger.addProvider('TEST', uiTestweb3) + vmdebugger.switchProvider('TEST') +} + +function waitForRemix (data) { + setTimeout(function () { + if (!document.getElementById('app').debugger) { + waitForRemix(data) + } else { + loadTestWeb3(data) + } + }, 500) +} + +loadJSON('/test-browser/resources/testWeb3.json', function (result) { + var data = JSON.parse(result) + waitForRemix(data) +}) + diff --git a/remix-debugger/test-browser/resources/testWeb3.json b/remix-debugger/test-browser/resources/testWeb3.json new file mode 100644 index 0000000000..ba5f88cb0f --- /dev/null +++ b/remix-debugger/test-browser/resources/testWeb3.json @@ -0,0 +1,17 @@ +{ + "testTxs": { + "0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51": {"blockHash":"0xd1d34932f8733e0485b7d9bf8500c4046d650f20ed7792508c304304fa7bbfac","blockNumber":89,"from":"0x00101c5bfa3fc8bad02c9f5fd65b069306251915","gas":105967,"gasPrice":"20000000000","hash":"0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51","input":"0x60fe47b10000000000000000000000000000000000000000000000000000000000000038","nonce":3,"to":"0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5","transactionIndex":0,"value":"0"} + }, + + "testTxsByBlock": { + "105967-0": {"blockHash":"0xd1d34932f8733e0485b7d9bf8500c4046d650f20ed7792508c304304fa7bbfac","blockNumber":89,"from":"0x00101c5bfa3fc8bad02c9f5fd65b069306251915","gas":105967,"gasPrice":"20000000000","hash":"0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51","input":"0x60fe47b10000000000000000000000000000000000000000000000000000000000000038","nonce":3,"to":"0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5","transactionIndex":0,"value":"0"} + }, + + "testCodes": { + "0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5": "0x60606040526000357c01000000000000000000000000000000000000000000000000000000009004806360fe47b11460415780636d4ce63c14605757603f565b005b605560048080359060200190919050506089565b005b606260048050506078565b6040518082815260200191505060405180910390f35b600060006000505490506086565b90565b80600060005081905550602d6040516045806100f083390180828152602001915050604051809103906000f0600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff02191690830217905550602281016000600050819055505b505660606040526040516020806045833981016040528080519060200190919050505b806001016000600050819055505b50600a80603b6000396000f360606040526008565b00" + }, + + "testTraces": { + "0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51": {"gas":"0x0000000000000000000000000000000000000000000000000000000000019def","return":"0x","structLogs":[{"gas":"84503","gasCost":"3","memory":[],"op":"PUSH1","pc":"0","stack":[]},{"gas":"84500","gasCost":"3","op":"PUSH1","pc":"2","stack":["0x60"]},{"gas":"84497","gasCost":"12","memexpand":"3","op":"MSTORE","pc":"4","stack":["0x60","0x40"]},{"gas":"84485","gasCost":"3","memory":["0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000060"],"op":"PUSH1","pc":"5","stack":[]},{"gas":"84482","gasCost":"3","op":"CALLDATALOAD","pc":"7","stack":["0x00"]},{"gas":"84479","gasCost":"3","op":"PUSH29","pc":"8","stack":["0x60fe47b100000000000000000000000000000000000000000000000000000000"]},{"gas":"84476","gasCost":"3","op":"SWAP1","pc":"38","stack":["0x60fe47b100000000000000000000000000000000000000000000000000000000","0x0100000000000000000000000000000000000000000000000000000000"]},{"gas":"84473","gasCost":"5","op":"DIV","pc":"39","stack":["0x0100000000000000000000000000000000000000000000000000000000","0x60fe47b100000000000000000000000000000000000000000000000000000000"]},{"gas":"84468","gasCost":"3","op":"DUP1","pc":"40","stack":["0x60fe47b1"]},{"gas":"84465","gasCost":"3","op":"PUSH4","pc":"41","stack":["0x60fe47b1","0x60fe47b1"]},{"gas":"84462","gasCost":"3","op":"EQ","pc":"46","stack":["0x60fe47b1","0x60fe47b1","0x60fe47b1"]},{"gas":"84459","gasCost":"3","op":"PUSH1","pc":"47","stack":["0x60fe47b1","0x01"]},{"gas":"84456","gasCost":"10","op":"JUMPI","pc":"49","stack":["0x60fe47b1","0x01","0x41"]},{"gas":"84446","gasCost":"1","op":"JUMPDEST","pc":"65","stack":["0x60fe47b1"]},{"gas":"84445","gasCost":"3","op":"PUSH1","pc":"66","stack":["0x60fe47b1"]},{"gas":"84442","gasCost":"3","op":"PUSH1","pc":"68","stack":["0x60fe47b1","0x55"]},{"gas":"84439","gasCost":"3","op":"DUP1","pc":"70","stack":["0x60fe47b1","0x55","0x04"]},{"gas":"84436","gasCost":"3","op":"DUP1","pc":"71","stack":["0x60fe47b1","0x55","0x04","0x04"]},{"gas":"84433","gasCost":"3","op":"CALLDATALOAD","pc":"72","stack":["0x60fe47b1","0x55","0x04","0x04","0x04"]},{"gas":"84430","gasCost":"3","op":"SWAP1","pc":"73","stack":["0x60fe47b1","0x55","0x04","0x04","0x38"]},{"gas":"84427","gasCost":"3","op":"PUSH1","pc":"74","stack":["0x60fe47b1","0x55","0x04","0x38","0x04"]},{"gas":"84424","gasCost":"3","op":"ADD","pc":"76","stack":["0x60fe47b1","0x55","0x04","0x38","0x04","0x20"]},{"gas":"84421","gasCost":"3","op":"SWAP1","pc":"77","stack":["0x60fe47b1","0x55","0x04","0x38","0x24"]},{"gas":"84418","gasCost":"3","op":"SWAP2","pc":"78","stack":["0x60fe47b1","0x55","0x04","0x24","0x38"]},{"gas":"84415","gasCost":"3","op":"SWAP1","pc":"79","stack":["0x60fe47b1","0x55","0x38","0x24","0x04"]},{"gas":"84412","gasCost":"2","op":"POP","pc":"80","stack":["0x60fe47b1","0x55","0x38","0x04","0x24"]},{"gas":"84410","gasCost":"2","op":"POP","pc":"81","stack":["0x60fe47b1","0x55","0x38","0x04"]},{"gas":"84408","gasCost":"3","op":"PUSH1","pc":"82","stack":["0x60fe47b1","0x55","0x38"]},{"gas":"84405","gasCost":"8","op":"JUMP","pc":"84","stack":["0x60fe47b1","0x55","0x38","0x89"]},{"gas":"84397","gasCost":"1","op":"JUMPDEST","pc":"137","stack":["0x60fe47b1","0x55","0x38"]},{"gas":"84396","gasCost":"3","op":"DUP1","pc":"138","stack":["0x60fe47b1","0x55","0x38"]},{"gas":"84393","gasCost":"3","op":"PUSH1","pc":"139","stack":["0x60fe47b1","0x55","0x38","0x38"]},{"gas":"84390","gasCost":"3","op":"PUSH1","pc":"141","stack":["0x60fe47b1","0x55","0x38","0x38","0x00"]},{"gas":"84387","gasCost":"2","op":"POP","pc":"143","stack":["0x60fe47b1","0x55","0x38","0x38","0x00","0x00"]},{"gas":"84385","gasCost":"3","op":"DUP2","pc":"144","stack":["0x60fe47b1","0x55","0x38","0x38","0x00"]},{"gas":"84382","gasCost":"3","op":"SWAP1","pc":"145","stack":["0x60fe47b1","0x55","0x38","0x38","0x00","0x38"]},{"gas":"84379","gasCost":"5000","op":"SSTORE","pc":"146","stack":["0x60fe47b1","0x55","0x38","0x38","0x38","0x00"]},{"gas":"79379","gasCost":"2","op":"POP","pc":"147","stack":["0x60fe47b1","0x55","0x38","0x38"]},{"gas":"79377","gasCost":"3","op":"PUSH1","pc":"148","stack":["0x60fe47b1","0x55","0x38"]},{"gas":"79374","gasCost":"3","op":"PUSH1","pc":"150","stack":["0x60fe47b1","0x55","0x38","0x2d"]},{"gas":"79371","gasCost":"3","op":"MLOAD","pc":"152","stack":["0x60fe47b1","0x55","0x38","0x2d","0x40"]},{"gas":"79368","gasCost":"3","memory":["0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000060"],"op":"PUSH1","pc":"153","stack":["0x60fe47b1","0x55","0x38","0x2d","0x60"]},{"gas":"79365","gasCost":"3","op":"DUP1","pc":"155","stack":["0x60fe47b1","0x55","0x38","0x2d","0x60","0x45"]},{"gas":"79362","gasCost":"3","op":"PUSH2","pc":"156","stack":["0x60fe47b1","0x55","0x38","0x2d","0x60","0x45","0x45"]},{"gas":"79359","gasCost":"3","op":"DUP4","pc":"159","stack":["0x60fe47b1","0x55","0x38","0x2d","0x60","0x45","0x45","0xf0"]},{"gas":"79356","gasCost":"21","memexpand":"3","op":"CODECOPY","pc":"160","stack":["0x60fe47b1","0x55","0x38","0x2d","0x60","0x45","0x45","0xf0","0x60"]},{"gas":"79335","gasCost":"3","memory":["0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000060","6060604052604051602080604583398101604052808051906020019091905050","5b806001016000600050819055505b50600a80603b6000396000f36060604052","6008565b00000000000000000000000000000000000000000000000000000000"],"op":"ADD","pc":"161","stack":["0x60fe47b1","0x55","0x38","0x2d","0x60","0x45"]},{"gas":"79332","gasCost":"3","op":"DUP1","pc":"162","stack":["0x60fe47b1","0x55","0x38","0x2d","0xa5"]},{"gas":"79329","gasCost":"3","op":"DUP3","pc":"163","stack":["0x60fe47b1","0x55","0x38","0x2d","0xa5","0xa5"]},{"gas":"79326","gasCost":"3","op":"DUP2","pc":"164","stack":["0x60fe47b1","0x55","0x38","0x2d","0xa5","0xa5","0x2d"]},{"gas":"79323","gasCost":"6","memexpand":"1","op":"MSTORE","pc":"165","stack":["0x60fe47b1","0x55","0x38","0x2d","0xa5","0xa5","0x2d","0xa5"]},{"gas":"79317","gasCost":"3","memory":["0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000060","6060604052604051602080604583398101604052808051906020019091905050","5b806001016000600050819055505b50600a80603b6000396000f36060604052","6008565b00000000000000000000000000000000000000000000000000000000","000000002d000000000000000000000000000000000000000000000000000000"],"op":"PUSH1","pc":"166","stack":["0x60fe47b1","0x55","0x38","0x2d","0xa5","0xa5"]},{"gas":"79314","gasCost":"3","op":"ADD","pc":"168","stack":["0x60fe47b1","0x55","0x38","0x2d","0xa5","0xa5","0x20"]},{"gas":"79311","gasCost":"3","op":"SWAP2","pc":"169","stack":["0x60fe47b1","0x55","0x38","0x2d","0xa5","0xc5"]},{"gas":"79308","gasCost":"2","op":"POP","pc":"170","stack":["0x60fe47b1","0x55","0x38","0xc5","0xa5","0x2d"]},{"gas":"79306","gasCost":"2","op":"POP","pc":"171","stack":["0x60fe47b1","0x55","0x38","0xc5","0xa5"]},{"gas":"79304","gasCost":"3","op":"PUSH1","pc":"172","stack":["0x60fe47b1","0x55","0x38","0xc5"]},{"gas":"79301","gasCost":"3","op":"MLOAD","pc":"174","stack":["0x60fe47b1","0x55","0x38","0xc5","0x40"]},{"gas":"79298","gasCost":"3","memory":["0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000060","6060604052604051602080604583398101604052808051906020019091905050","5b806001016000600050819055505b50600a80603b6000396000f36060604052","6008565b00000000000000000000000000000000000000000000000000000000","000000002d000000000000000000000000000000000000000000000000000000"],"op":"DUP1","pc":"175","stack":["0x60fe47b1","0x55","0x38","0xc5","0x60"]},{"gas":"79295","gasCost":"3","op":"SWAP2","pc":"176","stack":["0x60fe47b1","0x55","0x38","0xc5","0x60","0x60"]},{"gas":"79292","gasCost":"3","op":"SUB","pc":"177","stack":["0x60fe47b1","0x55","0x38","0x60","0x60","0xc5"]},{"gas":"79289","gasCost":"3","op":"SWAP1","pc":"178","stack":["0x60fe47b1","0x55","0x38","0x60","0x65"]},{"gas":"79286","gasCost":"3","op":"PUSH1","pc":"179","stack":["0x60fe47b1","0x55","0x38","0x65","0x60"]},{"gas":"79283","gasCost":"32000","op":"CREATE","pc":"181","stack":["0x60fe47b1","0x55","0x38","0x65","0x60","0x00"]},{"gas":"47283","gasCost":"3","memory":[],"op":"PUSH1","pc":"0","stack":[]},{"gas":"47280","gasCost":"3","op":"PUSH1","pc":"2","stack":["0x60"]},{"gas":"47277","gasCost":"12","memexpand":"3","op":"MSTORE","pc":"4","stack":["0x60","0x40"]},{"gas":"47265","gasCost":"3","memory":["0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000060"],"op":"PUSH1","pc":"5","stack":[]},{"gas":"47262","gasCost":"3","op":"MLOAD","pc":"7","stack":["0x40"]},{"gas":"47259","gasCost":"3","memory":["0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000060"],"op":"PUSH1","pc":"8","stack":["0x60"]},{"gas":"47256","gasCost":"3","op":"DUP1","pc":"10","stack":["0x60","0x20"]},{"gas":"47253","gasCost":"3","op":"PUSH1","pc":"11","stack":["0x60","0x20","0x20"]},{"gas":"47250","gasCost":"3","op":"DUP4","pc":"13","stack":["0x60","0x20","0x20","0x45"]},{"gas":"47247","gasCost":"9","memexpand":"1","op":"CODECOPY","pc":"14","stack":["0x60","0x20","0x20","0x45","0x60"]},{"gas":"47238","gasCost":"3","memory":["0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000060","000000000000000000000000000000000000000000000000000000000000002d"],"op":"DUP2","pc":"15","stack":["0x60","0x20"]},{"gas":"47235","gasCost":"3","op":"ADD","pc":"16","stack":["0x60","0x20","0x60"]},{"gas":"47232","gasCost":"3","op":"PUSH1","pc":"17","stack":["0x60","0x80"]},{"gas":"47229","gasCost":"3","op":"MSTORE","pc":"19","stack":["0x60","0x80","0x40"]},{"gas":"47226","gasCost":"3","memory":["0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000080","000000000000000000000000000000000000000000000000000000000000002d"],"op":"DUP1","pc":"20","stack":["0x60"]},{"gas":"47223","gasCost":"3","op":"DUP1","pc":"21","stack":["0x60","0x60"]},{"gas":"47220","gasCost":"3","op":"MLOAD","pc":"22","stack":["0x60","0x60","0x60"]},{"gas":"47217","gasCost":"3","memory":["0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000080","000000000000000000000000000000000000000000000000000000000000002d"],"op":"SWAP1","pc":"23","stack":["0x60","0x60","0x2d"]},{"gas":"47214","gasCost":"3","op":"PUSH1","pc":"24","stack":["0x60","0x2d","0x60"]},{"gas":"47211","gasCost":"3","op":"ADD","pc":"26","stack":["0x60","0x2d","0x60","0x20"]},{"gas":"47208","gasCost":"3","op":"SWAP1","pc":"27","stack":["0x60","0x2d","0x80"]},{"gas":"47205","gasCost":"3","op":"SWAP2","pc":"28","stack":["0x60","0x80","0x2d"]},{"gas":"47202","gasCost":"3","op":"SWAP1","pc":"29","stack":["0x2d","0x80","0x60"]},{"gas":"47199","gasCost":"2","op":"POP","pc":"30","stack":["0x2d","0x60","0x80"]},{"gas":"47197","gasCost":"2","op":"POP","pc":"31","stack":["0x2d","0x60"]},{"gas":"47195","gasCost":"1","op":"JUMPDEST","pc":"32","stack":["0x2d"]},{"gas":"47194","gasCost":"3","op":"DUP1","pc":"33","stack":["0x2d"]},{"gas":"47191","gasCost":"3","op":"PUSH1","pc":"34","stack":["0x2d","0x2d"]},{"gas":"47188","gasCost":"3","op":"ADD","pc":"36","stack":["0x2d","0x2d","0x01"]},{"gas":"47185","gasCost":"3","op":"PUSH1","pc":"37","stack":["0x2d","0x2e"]},{"gas":"47182","gasCost":"3","op":"PUSH1","pc":"39","stack":["0x2d","0x2e","0x00"]},{"gas":"47179","gasCost":"2","op":"POP","pc":"41","stack":["0x2d","0x2e","0x00","0x00"]},{"gas":"47177","gasCost":"3","op":"DUP2","pc":"42","stack":["0x2d","0x2e","0x00"]},{"gas":"47174","gasCost":"3","op":"SWAP1","pc":"43","stack":["0x2d","0x2e","0x00","0x2e"]},{"gas":"47171","gasCost":"20000","op":"SSTORE","pc":"44","stack":["0x2d","0x2e","0x2e","0x00"]},{"gas":"27171","gasCost":"2","op":"POP","pc":"45","stack":["0x2d","0x2e"]},{"gas":"27169","gasCost":"1","op":"JUMPDEST","pc":"46","stack":["0x2d"]},{"gas":"27168","gasCost":"2","op":"POP","pc":"47","stack":["0x2d"]},{"gas":"27166","gasCost":"3","op":"PUSH1","pc":"48","stack":[]},{"gas":"27163","gasCost":"3","op":"DUP1","pc":"50","stack":["0x0a"]},{"gas":"27160","gasCost":"3","op":"PUSH1","pc":"51","stack":["0x0a","0x0a"]},{"gas":"27157","gasCost":"3","op":"PUSH1","pc":"53","stack":["0x0a","0x0a","0x3b"]},{"gas":"27154","gasCost":"6","op":"CODECOPY","pc":"55","stack":["0x0a","0x0a","0x3b","0x00"]},{"gas":"27148","gasCost":"3","memory":["60606040526008565b0000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000080","000000000000000000000000000000000000000000000000000000000000002d"],"op":"PUSH1","pc":"56","stack":["0x0a"]},{"gas":"27145","gasCost":"0","op":"RETURN","pc":"58","stack":["0x0a","0x00"]},{"gas":"25145","gasCost":"3","memory":["0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000060","6060604052604051602080604583398101604052808051906020019091905050","5b806001016000600050819055505b50600a80603b6000396000f36060604052","6008565b00000000000000000000000000000000000000000000000000000000","000000002d000000000000000000000000000000000000000000000000000000"],"op":"PUSH1","pc":"182","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95"]},{"gas":"25142","gasCost":"3","memory":["0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000000","0000000000000000000000000000000000000000000000000000000000000060","6060604052604051602080604583398101604052808051906020019091905050","5b806001016000600050819055505b50600a80603b6000396000f36060604052","6008565b00000000000000000000000000000000000000000000000000000000","000000002d000000000000000000000000000000000000000000000000000000"],"op":"PUSH1","pc":"184","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01"]},{"gas":"25139","gasCost":"3","op":"PUSH2","pc":"186","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01","0x00"]},{"gas":"25136","gasCost":"10","op":"EXP","pc":"189","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01","0x00","0x0100"]},{"gas":"25126","gasCost":"3","op":"DUP2","pc":"190","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01","0x01"]},{"gas":"25123","gasCost":"50","op":"SLOAD","pc":"191","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01","0x01","0x01"]},{"gas":"25073","gasCost":"3","op":"DUP2","pc":"192","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01","0x01","0x00"]},{"gas":"25070","gasCost":"3","op":"PUSH20","pc":"193","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01","0x01","0x00","0x01"]},{"gas":"25067","gasCost":"5","op":"MUL","pc":"214","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01","0x01","0x00","0x01","0xffffffffffffffffffffffffffffffffffffffff"]},{"gas":"25062","gasCost":"3","op":"NOT","pc":"215","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01","0x01","0x00","0xffffffffffffffffffffffffffffffffffffffff"]},{"gas":"25059","gasCost":"3","op":"AND","pc":"216","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01","0x01","0x00","0xffffffffffffffffffffffff0000000000000000000000000000000000000000"]},{"gas":"25056","gasCost":"3","op":"SWAP1","pc":"217","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01","0x01","0x00"]},{"gas":"25053","gasCost":"3","op":"DUP4","pc":"218","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01","0x00","0x01"]},{"gas":"25050","gasCost":"5","op":"MUL","pc":"219","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01","0x00","0x01","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95"]},{"gas":"25045","gasCost":"3","op":"OR","pc":"220","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01","0x00","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95"]},{"gas":"25042","gasCost":"3","op":"SWAP1","pc":"221","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95"]},{"gas":"25039","gasCost":"20000","op":"SSTORE","pc":"222","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95","0x01"]},{"gas":"5039","gasCost":"2","op":"POP","pc":"223","stack":["0x60fe47b1","0x55","0x38","0xd01f65e3472f24faf45f08f8698ec4da1bf32a95"]},{"gas":"5037","gasCost":"3","op":"PUSH1","pc":"224","stack":["0x60fe47b1","0x55","0x38"]},{"gas":"5034","gasCost":"3","op":"DUP2","pc":"226","stack":["0x60fe47b1","0x55","0x38","0x22"]},{"gas":"5031","gasCost":"3","op":"ADD","pc":"227","stack":["0x60fe47b1","0x55","0x38","0x22","0x38"]},{"gas":"5028","gasCost":"3","op":"PUSH1","pc":"228","stack":["0x60fe47b1","0x55","0x38","0x5a"]},{"gas":"5025","gasCost":"3","op":"PUSH1","pc":"230","stack":["0x60fe47b1","0x55","0x38","0x5a","0x00"]},{"gas":"5022","gasCost":"2","op":"POP","pc":"232","stack":["0x60fe47b1","0x55","0x38","0x5a","0x00","0x00"]},{"gas":"5020","gasCost":"3","op":"DUP2","pc":"233","stack":["0x60fe47b1","0x55","0x38","0x5a","0x00"]},{"gas":"5017","gasCost":"3","op":"SWAP1","pc":"234","stack":["0x60fe47b1","0x55","0x38","0x5a","0x00","0x5a"]},{"gas":"5014","gasCost":"5000","op":"SSTORE","pc":"235","stack":["0x60fe47b1","0x55","0x38","0x5a","0x5a","0x00"]},{"gas":"14","gasCost":"2","op":"POP","pc":"236","stack":["0x60fe47b1","0x55","0x38","0x5a"]},{"gas":"12","gasCost":"1","op":"JUMPDEST","pc":"237","stack":["0x60fe47b1","0x55","0x38"]},{"gas":"11","gasCost":"2","op":"POP","pc":"238","stack":["0x60fe47b1","0x55","0x38"]},{"gas":"9","gasCost":"8","op":"JUMP","pc":"239","stack":["0x60fe47b1","0x55"]},{"gas":"1","gasCost":"1","op":"JUMPDEST","pc":"85","stack":["0x60fe47b1"]},{"gas":"0","gasCost":"0","op":"STOP","pc":"86","stack":["0x60fe47b1"]}]} + } +} diff --git a/remix-debugger/test-browser/test/init.js b/remix-debugger/test-browser/test/init.js new file mode 100644 index 0000000000..4078b00185 --- /dev/null +++ b/remix-debugger/test-browser/test/init.js @@ -0,0 +1,172 @@ +module.exports = function (browser, callback) { + extendBrowser(browser) + browser + .url('http://127.0.0.1:8080') + .injectScript('test-browser/resources/insertTestWeb3.js', function () { + // wait for the script to load test web3... + setTimeout(function () { + callback() + }, 5000) + }) +} + +function extendBrowser (browser) { + browser.multipleClick = function (id, time) { + for (var k = 0; k < time; k++) { + browser.click(id) + } + return browser + } + + browser.assertCurrentSelectedItem = function (expected) { + browser.execute(function (id) { + var node = document.querySelector('#asmcodes div div[selected="selected"] span') + return node.innerText + }, [''], function (returnValue) { + browser.assert.equal(returnValue.value, expected) + }) + return browser + } + + browser.retrieveInnerText = function (selector, callback) { + browser.execute(function (selector) { + var node = document.querySelector(selector) + return node ? node.innerText : '' + }, [selector], function (returnValue) { + callback(returnValue.value) + }) + return browser + } + + browser.assertStepDetail = function (vmtracestepinfo, stepinfo, addmemoryinfo, gasinfo, remaininggasinfo, loadedaddressinfo) { + assertPanel('#stepdetail', browser, ['vmtracestep:' + vmtracestepinfo, 'executionstep:' + stepinfo, 'addmemory:' + addmemoryinfo, 'gas:' + gasinfo, 'remaininggas:' + remaininggasinfo, 'loadedaddress:' + loadedaddressinfo]) + return browser + } + + browser.assertStack = function (value) { + return assertPanel('#stackpanel', browser, value) + } + + browser.assertStorageChanges = function (value) { + return assertPanel('#storagepanel', browser, value) + } + + browser.assertMemory = function (value) { + return assertPanel('#memorypanel', browser, value) + } + + browser.assertCallData = function (value) { + return assertPanel('#calldatapanel', browser, value) + } + + browser.assertCallStack = function (value) { + return assertPanel('#callstackpanel', browser, value) + } + + browser.assertStackValue = function (index, value) { + return assertPanelValue('#stackpanel', browser, index, value) + } + + browser.assertStorageChangesValue = function (index, value) { + return assertPanelValue('#storagepanel', browser, index, value) + } + + browser.assertMemoryValue = function (index, value) { + return assertPanelValue('#memorypanel', browser, index, value) + } + + browser.assertCallStackValue = function (index, value) { + return assertPanelValue('#callstackpanel', browser, index, value) + } + + browser.debugerKeyCode = { + 'Enter': 13, + 'Up': 38, + 'Down': 40, + 'Right': '39', + 'Left': 37, + 'Esc': 27, + 'SpaceBar': 32, + 'Ctrl': 17, + 'Alt': 18, + 'Shift': 16 + } + +/* browser.sendKeys is not working for safari */ +/* still not working properly +browser.fireEvent = function (el, key, times, callback) { + var data = { + 'id': el.substring(1), + 'key': key, + 'times': times + } + browser.execute(function (data) { + data = JSON.parse(data) + var el = document.getElementById(data.id) + var eventObj + console.log(el) + console.log(data) + var k = 0 + if (document.createEventObject) { + eventObj = document.createEventObject() + eventObj.keyCode = data.key + while (k < data.times) { + console.log('firing brfore createEventObject') + el.fireEvent('onkeypress', eventObj) + console.log('firing') + k++ + } + } else if (typeof (KeyboardEvent) === 'function') { + eventObj = new KeyboardEvent('keyup') + eventObj.key = data.key + eventObj.which = data.key + while (k < data.times) { + console.log('firing brfore createEvent') + el.dispatchEvent(eventObj) + console.log('firing') + k++ + } + } + }, [JSON.stringify(data)], function () { + callback() + }) +} +*/ +} + +function assertPanel (id, browser, value) { + var selector = '.dropdownpanel div.dropdowncontent ul' + browser.execute(function (id, selector) { + var el = document.getElementById(id.replace('#', '').replace('.', '')) + var node = el.querySelector(selector) + var ret = [] + for (var k = 0; k < node.children.length; k++) { + if (node.children[k].innerText) { + ret.push(node.children[k].innerText) + } + } + return ret + }, [id, selector], function (returnValues) { + value.map(function (item, index) { + if (returnValues.value.length) { + var testValue = returnValues.value[index].replace(/\r\n/g, '').replace(/\t/g, '').replace(/\s/g, '') + browser.assert.equal(testValue, value[index]) + } else { + browser.assert.equal(item, '') + } + }) + }) + return browser +} + +function assertPanelValue (id, browser, index, value) { + var selector = id + ' .dropdownpanel .dropdowncontent ul' + browser.execute(function (id, index) { + var node = document.querySelector(id) + return node.children[index].innerText + }, [selector, index], function (returnValues) { + var testValue = returnValues.value.replace(/\r\n/g, '').replace(/\t/g, '').replace(/\s/g, '') + browser.assert.equal(testValue, value) + }) + return browser +} diff --git a/test-browser/sauce.js b/remix-debugger/test-browser/test/sauce.js similarity index 100% rename from test-browser/sauce.js rename to remix-debugger/test-browser/test/sauce.js diff --git a/test-browser/vmdebugger.js b/remix-debugger/test-browser/test/vmdebugger.js similarity index 55% rename from test-browser/vmdebugger.js rename to remix-debugger/test-browser/test/vmdebugger.js index 0a6bfdee50..f84555a0f3 100644 --- a/test-browser/vmdebugger.js +++ b/remix-debugger/test-browser/test/vmdebugger.js @@ -5,7 +5,9 @@ var sauce = require('./sauce') module.exports = { beforeEach: function (browser, done) { try { - init(browser, done) + init(browser, function () { + done() + }) } catch (e) { var mes = 'error ' + e.message console.log(mes) @@ -14,43 +16,54 @@ module.exports = { }, 'vmdebugger': function (browser) { - loadTrace(browser) - browser.click('#unload') loadTraceNotFound(browser) - browser.click('#unload') + .click('#unload') + loadTrace(browser) + .click('#unload') panels(browser) - browser.click('#unload') + .click('#unload') slider(browser) - browser.click('#unload') + .click('#unload') stepping(browser) - browser.click('#unload') - sticker(browser) - browser.end() + .click('#unload') + stepdetail(browser) + .end() }, tearDown: sauce } -function loadTrace (browser) { +function loadTraceNotFound (browser) { browser .clearValue('#txinput') .setValue('#txinput', '0x20ef65b8b186ca942zcccd634f37074dde49b541c27994fc7596740ef44cfd51') .click('#load') - .assert.containsText('#txhash', '') + .click('#txinfo .title') + .execute(function () { + return document.querySelector('#txinfo .dropdownpanel .dropdownrawcontent').innerHTML + }, [], function (result) { + console.log(result.value) + if (result.value.indexOf('not found') === -1) { + browser.assert.fail(' txinput panel does not contain ', 'info about error', '') + } + }) return browser } -function loadTraceNotFound (browser) { +function loadTrace (browser) { browser .clearValue('#txinput') .setValue('#txinput', '0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51') .click('#load') - .waitForElementVisible('#vmdebugger', 1000) - .expect.element('#txhash').text.to.equal('0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51') - browser.expect.element('#txfrom').text.to.equal('0x00101c5bfa3fc8bad02c9f5fd65b069306251915') - browser.expect.element('#txto').text.to.equal('0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5') - browser.expect.element('#txto').text.to.equal('0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5') - browser.click('#unload') + .click('#txinfo .title') + .execute(function () { + return document.querySelector('#txinfo .dropdownpanel .dropdownrawcontent').innerHTML + }, [], function (result) { + if (result.value.indexOf('0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51') === -1) { + browser.assert.fail(' txinput panel does not contain 0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51 ', 'info about error', '') + } + }) + .click('#unload') .waitForElementNotVisible('#vmdebugger', 1000) return browser } @@ -60,22 +73,21 @@ function panels (browser) { .clearValue('#txinput') .setValue('#txinput', '0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51') .click('#load') - .waitForElementVisible('#vmdebugger', 1000) - .click('#nextcall') - .assertStack('0x\n0x60\n0x65\n0x38\n0x55\n0x60fe47b1') - .assertStorageChanges('0x00 0x38') - .assertCallData('0x60fe47b10000000000000000000000000000000000000000000000000000000000000038') - .assertCallStack('0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5') - .assertStackValue(1, '0x60') - .assertMemoryValue(6, '0xc0 60 60 60 40 52 60 40 51 60 20 80 60 45 83 39 81 ????R??Q????E?9?') - .assertMemoryValue(7, '0xe0 01 60 40 52 80 80 51 90 60 20 01 90 91 90 50 50 ???R??Q???????PP') - .assertMemoryValue(8, '0x100 5b 80 60 01 01 60 00 60 00 50 81 90 55 50 5b 50 ?????????P??UP?P') + .multipleClick('#intoforward', 63) + .assertStack(['0:0x', '1:0x60', '2:0x65', '3:0x38', '4:0x55', '5:0x60fe47b1']) + .assertStorageChanges(['0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563:Objectkey:0x00value:0x38']) + .assertCallData(['0:0x60fe47b10000000000000000000000000000000000000000000000000000000000000038']) + .assertCallStack(['0:0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5']) + .assertStackValue(1, '1:0x60') + .assertMemoryValue(6, '0x60:60606040526040516020806045833981????R??Q????E?9?') + .assertMemoryValue(7, '0x70:01604052808051906020019091905050???R??Q???????PP') + .assertMemoryValue(8, '0x80:5b806001016000600050819055505b50?????????P??UP?P') .click('#intoforward') // CREATE - .assertStack('') - .assertStorageChanges('') - .assertMemory('') - .assertCallData('0x0000000000000000000000000000000000000000000000000000000000000000000000000000006060606040526040516020806045833981016040528080519060200190919050505b806001016000600050819055') - .assertCallStack('0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5\n(Contract Creation - Step 63)') + .assertStack(['']) + .assertStorageChanges([]) + .assertMemory(['']) + .assertCallData(['0:0x0000000000000000000000000000000000000000000000000000000000000000000000000000006060606040526040516020806045833981016040528080519060200190919050505b806001016000600050819055']) + .assertCallStack(['0:0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5', '1:(ContractCreation-Step63)']) return browser } @@ -84,7 +96,6 @@ function slider (browser) { .clearValue('#txinput') .setValue('#txinput', '0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51') .click('#load') - .waitForElementVisible('#vmdebugger', 1000) .click('#intoforward') .click('#intoforward') .click('#intoforward') @@ -105,7 +116,7 @@ function slider (browser) { .sendKeys('#slider', browser.Keys.RIGHT_ARROW) .sendKeys('#slider', browser.Keys.LEFT_ARROW) */ - .assertCurrentSelectedItem('041 PUSH 60fe47b1') + .assertCurrentSelectedItem('041 PUSH4 60fe47b1') return browser } @@ -114,34 +125,42 @@ function stepping (browser) { .clearValue('#txinput') .setValue('#txinput', '0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51') .click('#load') - .waitForElementVisible('#vmdebugger', 1000) .click('#intoforward') .click('#intoforward') .assertCurrentSelectedItem('004 MSTORE') .click('#intoforward') .click('#intoback') .click('#intoback') - .assertCurrentSelectedItem('002 PUSH 40') - .click('#nextcall') + .assertCurrentSelectedItem('002 PUSH1 40') + .multipleClick('#intoforward', 62) .assertCurrentSelectedItem('181 CREATE') .click('#intoforward') .click('#intoforward') .click('#intoforward') .click('#intoforward') .click('#overforward') - .assertCurrentSelectedItem('058 RETURN') - .click('#intoforward') + .assertCurrentSelectedItem('007 MLOAD') + .click('#intoback') + .click('#intoback') + .click('#intoback') + .click('#intoback') + .click('#intoback') + .click('#overforward') + .assertCurrentSelectedItem('182 PUSH1 01') + .click('#overforward') + .assertCurrentSelectedItem('184 PUSH1 00') + .click('#intoback') + .click('#intoback') .click('#overback') .assertCurrentSelectedItem('181 CREATE') return browser } -function sticker (browser) { +function stepdetail (browser) { browser .clearValue('#txinput') .setValue('#txinput', '0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51') .click('#load') - .waitForElementVisible('#vmdebugger', 1000) .click('#intoforward') .click('#intoforward') .click('#intoforward') @@ -161,13 +180,13 @@ function sticker (browser) { .end() }) */ - .assertSticker('6', '6', '', '3', '84476', '0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5') - .click('#nextcall') - .assertSticker('63', '63', '', '32000', '79283', '0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5') - .click('#intoforward') + .assertStepDetail('6', '6', '', '3', '84476', '0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5') + .multipleClick('#intoforward', 57) + .assertStepDetail('63', '63', '', '32000', '79283', '0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5') .click('#overforward') - .assertSticker('108', '44', '', '0', '27145', '(Contract Creation - Step 63)') + .click('#intoback') + .assertStepDetail('108', '44', '', '0', '27145', '(ContractCreation-Step63)') .click('#intoforward') - .assertSticker('109', '64', '', '3', '25145', '0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5') + .assertStepDetail('109', '64', '', '3', '25145', '0x0d3a18d64dfe4f927832ab58d6451cecc4e517c5') return browser } diff --git a/remix-lib/README.md b/remix-lib/README.md new file mode 100644 index 0000000000..566c21c99d --- /dev/null +++ b/remix-lib/README.md @@ -0,0 +1,24 @@ +# `remix-lib` + +Provides: + + { + EventManager: EventManager, + helpers: { + trace: traceHelper, + ui: uiHelper + }, + vm: { + Web3Providers: Web3Providers, + DummyProvider: DummyProvider, + Web3VMProvider: Web3VMProvider + }, + SourceMappingDecoder: SourceMappingDecoder, + SourceLocationTracker: SourceLocationTracker, + init: init, + util: util, + AstWalker: AstWalker, + ui: { + styleGuide: styleGuide + } + } diff --git a/remix-lib/index.js b/remix-lib/index.js new file mode 100644 index 0000000000..3e28b78028 --- /dev/null +++ b/remix-lib/index.js @@ -0,0 +1,63 @@ +'use strict' +var EventManager = require('./src/eventManager') +var traceHelper = require('./src/helpers/traceHelper') +var uiHelper = require('./src/helpers/uiHelper') +var compilerHelper = require('./src/helpers/compilerHelper') +var SourceMappingDecoder = require('./src/sourceMappingDecoder') +var SourceLocationTracker = require('./src/sourceLocationTracker') +var init = require('./src/init') +var util = require('./src/util') +var Web3Providers = require('./src/web3Provider/web3Providers') +var DummyProvider = require('./src/web3Provider/dummyProvider') +var Web3VMProvider = require('./src/web3Provider/web3VmProvider') +var AstWalker = require('./src/astWalker') +var Storage = require('./src/storage') + +var EventsDecoder = require('./src/execution/eventsDecoder') +var txExecution = require('./src/execution/txExecution') +var txHelper = require('./src/execution/txHelper') +var txFormat = require('./src/execution/txFormat') +var txListener = require('./src/execution/txListener') +var txRunner = require('./src/execution/txRunner') +var executionContext = require('./src/execution/execution-context') +var typeConversion = require('./src/execution/typeConversion') + +if (typeof (module) !== 'undefined' && typeof (module.exports) !== 'undefined') { + module.exports = modules() +} + +if (typeof (window) !== 'undefined') { + window.remix = modules() +} + +function modules () { + return { + EventManager: EventManager, + helpers: { + trace: traceHelper, + ui: uiHelper, + compiler: compilerHelper + }, + vm: { + Web3Providers: Web3Providers, + DummyProvider: DummyProvider, + Web3VMProvider: Web3VMProvider + }, + SourceMappingDecoder: SourceMappingDecoder, + SourceLocationTracker: SourceLocationTracker, + Storage: Storage, + init: init, + util: util, + AstWalker: AstWalker, + execution: { + EventsDecoder: EventsDecoder, + txExecution: txExecution, + txHelper: txHelper, + executionContext: executionContext, + txFormat: txFormat, + txListener: txListener, + txRunner: txRunner, + typeConversion: typeConversion + } + } +} diff --git a/remix-lib/package.json b/remix-lib/package.json new file mode 100644 index 0000000000..6a4d3627b3 --- /dev/null +++ b/remix-lib/package.json @@ -0,0 +1,90 @@ +{ + "name": "remix-lib", + "version": "0.2.9", + "description": "Ethereum IDE and tools for the web", + "contributors": [ + { + "name": "Yann Levreau", + "email": "yann@ethdev.com" + }, + { + "name": "Liana Husikyan", + "email": "liana@ethdev.com" + } + ], + "main": "./index.js", + "dependencies": { + "async": "^2.1.2", + "babel-eslint": "^7.1.1", + "babel-plugin-transform-object-assign": "^6.22.0", + "babel-preset-env": "^1.6.1", + "babel-preset-es2015": "^6.24.0", + "babel-preset-stage-0": "^6.24.1", + "babelify": "^7.3.0", + "ethereumjs-block": "^1.6.0", + "ethereumjs-tx": "^1.3.3", + "ethereumjs-util": "^5.1.2", + "ethereumjs-vm": "^2.3.3", + "ethers": "^3.0.15", + "fast-async": "^6.1.2", + "solc": "^0.4.13", + "standard": "^7.0.1", + "tape": "^4.6.0", + "web3": "^0.18.0" + }, + "scripts": { + "test": "standard && tape ./test/tests.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ethereum/remix.git" + }, + "author": "cpp-ethereum team", + "license": "MIT", + "bugs": { + "url": "https://github.com/ethereum/remix/issues" + }, + "homepage": "https://github.com/ethereum/remix#readme", + "standard": { + "ignore": [ + "node_modules/*", + "build/*", + "test/resources/*" + ] + }, + "browserify": { + "transform": [ + [ + "babelify", + { + "plugins": [ + [ + "fast-async", + { + "runtimePatten": null, + "compiler": { + "promises": true, + "es7": true, + "noRuntime": true, + "wrapAwait": true + } + } + ], + "transform-object-assign" + ] + } + ], + [ + "yo-yoify" + ], + [ + "babelify", + { + "presets": [ + "es2015" + ] + } + ] + ] + } +} diff --git a/remix-lib/src/astWalker.js b/remix-lib/src/astWalker.js new file mode 100644 index 0000000000..a5de98a0f6 --- /dev/null +++ b/remix-lib/src/astWalker.js @@ -0,0 +1,54 @@ +'use strict' +/** + * Crawl the given AST through the function walk(ast, callback) + */ +function AstWalker () { +} + +/** + * visit all the AST nodes + * + * @param {Object} ast - AST node + * @param {Object or Function} callback - if (Function) the function will be called for every node. + * - if (Object) callback[] will be called for + * every node of type . callback["*"] will be called fo all other nodes. + * in each case, if the callback returns false it does not descend into children. + * If no callback for the current type, children are visited. + */ +AstWalker.prototype.walk = function (ast, callback) { + if (callback instanceof Function) { + callback = {'*': callback} + } + if (!('*' in callback)) { + callback['*'] = function () { return true } + } + if (manageCallBack(ast, callback) && ast.children && ast.children.length > 0) { + for (var k in ast.children) { + var child = ast.children[k] + this.walk(child, callback) + } + } +} + +/** + * walk the given @astList + * + * @param {Object} sourcesList - sources list (containing root AST node) + * @param {Function} - callback used by AstWalker to compute response + */ +AstWalker.prototype.walkAstList = function (sourcesList, callback) { + var walker = new AstWalker() + for (var k in sourcesList) { + walker.walk(sourcesList[k].legacyAST, callback) + } +} + +function manageCallBack (node, callback) { + if (node.name in callback) { + return callback[node.name](node) + } else { + return callback['*'](node) + } +} + +module.exports = AstWalker diff --git a/remix-lib/src/eventManager.js b/remix-lib/src/eventManager.js new file mode 100644 index 0000000000..616e0f3732 --- /dev/null +++ b/remix-lib/src/eventManager.js @@ -0,0 +1,70 @@ +'use strict' + +function eventManager () { + this.registered = {} + this.anonymous = {} +} + +/* + * Unregister a listenner. + * Note that if obj is a function. the unregistration will be applied to the dummy obj {}. + * + * @param {String} eventName - the event name + * @param {Object or Func} obj - object that will listen on this event + * @param {Func} func - function of the listenners that will be executed +*/ +eventManager.prototype.unregister = function (eventName, obj, func) { + if (!this.registered[eventName]) { + return + } + if (obj instanceof Function) { + func = obj + obj = this.anonymous + } + for (var reg in this.registered[eventName]) { + if (this.registered[eventName][reg].obj === obj && this.registered[eventName][reg].func === func) { + this.registered[eventName].splice(reg, 1) + } + } +} + +/* + * Register a new listenner. + * Note that if obj is a function, the function registration will be associated with the dummy object {} + * + * @param {String} eventName - the event name + * @param {Object or Func} obj - object that will listen on this event + * @param {Func} func - function of the listenners that will be executed +*/ +eventManager.prototype.register = function (eventName, obj, func) { + if (!this.registered[eventName]) { + this.registered[eventName] = [] + } + if (obj instanceof Function) { + func = obj + obj = this.anonymous + } + this.registered[eventName].push({ + obj: obj, + func: func + }) +} + +/* + * trigger event. + * Every listenner have their associated function executed + * + * @param {String} eventName - the event name + * @param {Array}j - argument that will be passed to the exectued function. +*/ +eventManager.prototype.trigger = function (eventName, args) { + if (!this.registered[eventName]) { + return + } + for (var listener in this.registered[eventName]) { + var l = this.registered[eventName][listener] + l.func.apply(l.obj === this.anonymous ? {} : l.obj, args) + } +} + +module.exports = eventManager diff --git a/remix-lib/src/execution/eventsDecoder.js b/remix-lib/src/execution/eventsDecoder.js new file mode 100644 index 0000000000..1cd79ac72d --- /dev/null +++ b/remix-lib/src/execution/eventsDecoder.js @@ -0,0 +1,85 @@ +'use strict' +var ethJSUtil = require('ethereumjs-util') +var ethers = require('ethers') +var txHelper = require('./txHelper') + +/** + * Register to txListener and extract events + * + */ +class EventsDecoder { + constructor (opt = {}) { + this._api = opt.api + } + +/** + * use Transaction Receipt to decode logs. assume that the transaction as already been resolved by txListener. + * logs are decoded only if the contract if known by remix. + * + * @param {Object} tx - transaction object + * @param {Function} cb - callback + */ + parseLogs (tx, contractName, compiledContracts, cb) { + if (tx.isCall) return cb(null, { decoded: [], raw: [] }) + this._api.resolveReceipt(tx, (error, receipt) => { + if (error) return cb(error) + this._decodeLogs(tx, receipt, contractName, compiledContracts, cb) + }) + } + + _decodeLogs (tx, receipt, contract, contracts, cb) { + if (!contract || !receipt) { + return cb('cannot decode logs - contract or receipt not resolved ') + } + if (!receipt.logs) { + return cb(null, { decoded: [], raw: [] }) + } + this._decodeEvents(tx, receipt.logs, contract, contracts, cb) + } + + _eventABI (contract) { + var eventABI = {} + var abi = new ethers.Interface(contract.abi) + for (var e in abi.events) { + var event = abi.events[e] + eventABI[ethJSUtil.sha3(new Buffer(event.signature)).toString('hex')] = { event: event.name, inputs: event.inputs, object: event } + } + return eventABI + } + + _eventsABI (compiledContracts) { + var eventsABI = {} + txHelper.visitContracts(compiledContracts, (contract) => { + eventsABI[contract.name] = this._eventABI(contract.object) + }) + return eventsABI + } + + _event (hash, eventsABI) { + for (var k in eventsABI) { + if (eventsABI[k][hash]) { + return eventsABI[k][hash] + } + } + return null + } + + _decodeEvents (tx, logs, contractName, compiledContracts, cb) { + var eventsABI = this._eventsABI(compiledContracts) + var events = [] + for (var i in logs) { + // [address, topics, mem] + var log = logs[i] + var topicId = log.topics[0] + var abi = this._event(topicId.replace('0x', ''), eventsABI) + if (abi) { + events.push({ from: log.address, topic: topicId, event: abi.event, args: abi.object.parse(log.topics, log.data) }) + } else { + events.push({ from: log.address, data: log.data, topics: log.topics }) + } + } + cb(null, { decoded: events, raw: logs }) + } +} + +module.exports = EventsDecoder diff --git a/remix-lib/src/execution/execution-context.js b/remix-lib/src/execution/execution-context.js new file mode 100644 index 0000000000..b8c459973a --- /dev/null +++ b/remix-lib/src/execution/execution-context.js @@ -0,0 +1,253 @@ +'use strict' +var Web3 = require('web3') +var EventManager = require('../eventManager') +var EthJSVM = require('ethereumjs-vm') +var ethUtil = require('ethereumjs-util') +var StateManager = require('ethereumjs-vm/dist/stateManager') +var Web3VMProvider = require('../web3Provider/web3VmProvider') + +var rlp = ethUtil.rlp + +var injectedProvider + +var web3 +if (typeof window !== 'undefined' && typeof window.web3 !== 'undefined') { + injectedProvider = window.web3.currentProvider + web3 = new Web3(injectedProvider) +} else { + web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545')) +} + +var blankWeb3 = new Web3() + +/* + extend vm state manager and instanciate VM +*/ + +class StateManagerCommonStorageDump extends StateManager { + constructor (arg) { + super(arg) + this.keyHashes = {} + } + + putContractStorage (address, key, value, cb) { + this.keyHashes[ethUtil.sha3(key).toString('hex')] = ethUtil.bufferToHex(key) + super.putContractStorage(address, key, value, cb) + } + + dumpStorage (address, cb) { + var self = this + this._getStorageTrie(address, function (err, trie) { + if (err) { + return cb(err) + } + var storage = {} + var stream = trie.createReadStream() + stream.on('data', function (val) { + var value = rlp.decode(val.value) + storage['0x' + val.key.toString('hex')] = { + key: self.keyHashes[val.key.toString('hex')], + value: '0x' + value.toString('hex') + } + }) + stream.on('end', function () { + cb(storage) + }) + }) + } +} + +var stateManager = new StateManagerCommonStorageDump({}) +var vm = new EthJSVM({ + enableHomestead: true, + activatePrecompiles: true +}) + +// FIXME: move state manager in EthJSVM ctr +vm.stateManager = stateManager +vm.blockchain = stateManager.blockchain +vm.trie = stateManager.trie +vm.stateManager.checkpoint() + +var web3VM = new Web3VMProvider() +web3VM.setVM(vm) + +var mainNetGenesisHash = '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3' + +/* + trigger contextChanged, web3EndpointChanged +*/ +function ExecutionContext () { + var self = this + this.event = new EventManager() + + var executionContext = null + + this.blockGasLimitDefault = 4300000 + this.blockGasLimit = this.blockGasLimitDefault + + this.init = function (config) { + if (config.get('settings/always-use-vm')) { + executionContext = 'vm' + } else { + executionContext = injectedProvider ? 'injected' : 'vm' + } + } + + this.getProvider = function () { + return executionContext + } + + this.isVM = function () { + return executionContext === 'vm' + } + + this.web3 = function () { + return this.isVM() ? web3VM : web3 + } + + this.detectNetwork = function (callback) { + if (this.isVM()) { + callback(null, { id: '-', name: 'VM' }) + } else { + this.web3().version.getNetwork((err, id) => { + var name = null + if (err) name = 'Unknown' + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md + else if (id === '1') name = 'Main' + else if (id === '2') name = 'Morden (deprecated)' + else if (id === '3') name = 'Ropsten' + else if (id === '4') name = 'Rinkeby' + else if (id === '42') name = 'Kovan' + else name = 'Custom' + + if (id === '1') { + this.web3().eth.getBlock(0, (error, block) => { + if (error) console.log('cant query first block') + if (block && block.hash !== mainNetGenesisHash) name = 'Custom' + callback(err, { id, name }) + }) + } else { + callback(err, { id, name }) + } + }) + } + } + + this.internalWeb3 = function () { + return web3 + } + + this.blankWeb3 = function () { + return blankWeb3 + } + + this.vm = function () { + return vm + } + + this.setContext = function (context, endPointUrl, confirmCb, infoCb) { + executionContext = context + this.executionContextChange(context, endPointUrl, confirmCb, infoCb) + } + + this.executionContextChange = function (context, endPointUrl, confirmCb, infoCb, cb) { + if (!cb) cb = () => {} + + if (context === 'vm') { + executionContext = context + vm.stateManager.revert(function () { + vm.stateManager.checkpoint() + }) + self.event.trigger('contextChanged', ['vm']) + return cb() + } + + if (context === 'injected') { + if (injectedProvider === undefined) { + var alertMsg = 'No injected Web3 provider found. ' + alertMsg += 'Make sure your provider (e.g. MetaMask) is active and running ' + alertMsg += '(when recently activated you may have to reload the page).' + infoCb(alertMsg) + return cb() + } else { + executionContext = context + web3.setProvider(injectedProvider) + self._updateBlockGasLimit() + self.event.trigger('contextChanged', ['injected']) + return cb() + } + } + + if (context === 'web3') { + confirmCb(cb) + } + } + + this.currentblockGasLimit = function () { + return this.blockGasLimit + } + + this.stopListenOnLastBlock = function () { + if (this.listenOnLastBlockId) clearInterval(this.listenOnLastBlockId) + this.listenOnLastBlockId = null + } + + this._updateBlockGasLimit = function () { + if (this.getProvider() !== 'vm') { + web3.eth.getBlock('latest', (err, block) => { + if (!err) { + // we can't use the blockGasLimit cause the next blocks could have a lower limit : https://github.com/ethereum/remix/issues/506 + this.blockGasLimit = (block && block.gasLimit) ? Math.floor(block.gasLimit - (5 * block.gasLimit) / 1024) : this.blockGasLimitDefault + } else { + this.blockGasLimit = this.blockGasLimitDefault + } + }) + } + } + + this.listenOnLastBlock = function () { + this.listenOnLastBlockId = setInterval(() => { + this._updateBlockGasLimit() + }, 15000) + } + + // TODO: not used here anymore and needs to be moved + function setProviderFromEndpoint (endpoint, context, cb) { + var oldProvider = web3.currentProvider + + if (endpoint === 'ipc') { + web3.setProvider(new web3.providers.IpcProvider()) + } else { + web3.setProvider(new web3.providers.HttpProvider(endpoint)) + } + if (web3.isConnected()) { + executionContext = context + self._updateBlockGasLimit() + self.event.trigger('contextChanged', ['web3']) + self.event.trigger('web3EndpointChanged') + cb() + } else { + web3.setProvider(oldProvider) + var alertMsg = 'Not possible to connect to the Web3 provider. ' + alertMsg += 'Make sure the provider is running and a connection is open (via IPC or RPC).' + cb(alertMsg) + } + } + this.setProviderFromEndpoint = setProviderFromEndpoint + + this.txDetailsLink = function (network, hash) { + if (transactionDetailsLinks[network]) { + return transactionDetailsLinks[network] + hash + } + } +} + +var transactionDetailsLinks = { + 'Main': 'https://www.etherscan.io/tx/', + 'Rinkeby': 'https://rinkeby.etherscan.io/tx/', + 'Ropsten': 'https://ropsten.etherscan.io/tx/', + 'Kovan': 'https://kovan.etherscan.io/tx/' +} + +module.exports = new ExecutionContext() diff --git a/remix-lib/src/execution/txExecution.js b/remix-lib/src/execution/txExecution.js new file mode 100644 index 0000000000..7f9670eaeb --- /dev/null +++ b/remix-lib/src/execution/txExecution.js @@ -0,0 +1,104 @@ +'use strict' +var ethers = require('ethers') + +module.exports = { + /** + * deploy the given contract + * + * @param {String} from - sender address + * @param {String} data - data to send with the transaction ( return of txFormat.buildData(...) ). + * @param {String} value - decimal representation of value. + * @param {String} gasLimit - decimal representation of gas limit. + * @param {Object} txRunner - TxRunner.js instance + * @param {Object} callbacks - { confirmationCb, gasEstimationForceSend, promptCb } + * [validate transaction] confirmationCb (network, tx, gasEstimation, continueTxExecution, cancelCb) + * [transaction failed, force send] gasEstimationForceSend (error, continueTxExecution, cancelCb) + * [personal mode enabled, need password to continue] promptCb (okCb, cancelCb) + * @param {Function} finalCallback - last callback. + */ + createContract: function (from, data, value, gasLimit, txRunner, callbacks, finalCallback) { + if (!callbacks.confirmationCb || !callbacks.gasEstimationForceSend || !callbacks.promptCb) { + return finalCallback('all the callbacks must have been defined') + } + var tx = { from: from, to: null, data: data, useCall: false, value: value, gasLimit: gasLimit } + txRunner.rawRun(tx, callbacks.confirmationCb, callbacks.gasEstimationForceSend, callbacks.promptCb, (error, txResult) => { + // see universaldapp.js line 660 => 700 to check possible values of txResult (error case) + finalCallback(error, txResult) + }) + }, + + /** + * call the current given contract ! that will create a transaction ! + * + * @param {String} from - sender address + * @param {String} to - recipient address + * @param {String} data - data to send with the transaction ( return of txFormat.buildData(...) ). + * @param {String} value - decimal representation of value. + * @param {String} gasLimit - decimal representation of gas limit. + * @param {Object} txRunner - TxRunner.js instance + * @param {Object} callbacks - { confirmationCb, gasEstimationForceSend, promptCb } + * [validate transaction] confirmationCb (network, tx, gasEstimation, continueTxExecution, cancelCb) + * [transaction failed, force send] gasEstimationForceSend (error, continueTxExecution, cancelCb) + * [personal mode enabled, need password to continue] promptCb (okCb, cancelCb) + * @param {Function} finalCallback - last callback. + */ + callFunction: function (from, to, data, value, gasLimit, funAbi, txRunner, callbacks, finalCallback) { + var tx = { from: from, to: to, data: data, useCall: false, value: value, gasLimit: gasLimit } + txRunner.rawRun(tx, callbacks.confirmationCb, callbacks.gasEstimationForceSend, callbacks.promptCb, (error, txResult) => { + // see universaldapp.js line 660 => 700 to check possible values of txResult (error case) + finalCallback(error, txResult) + }) + }, + + /** + * check if the vm has errored + * + * @param {Object} txResult - the value returned by the vm + * @return {Object} - { error: true/false, message: DOMNode } + */ + checkVMError: function (txResult) { + var errorCode = { + OUT_OF_GAS: 'out of gas', + STACK_UNDERFLOW: 'stack underflow', + STACK_OVERFLOW: 'stack overflow', + INVALID_JUMP: 'invalid JUMP', + INVALID_OPCODE: 'invalid opcode', + REVERT: 'revert', + STATIC_STATE_CHANGE: 'static state change' + } + var ret = { + error: false, + message: '' + } + if (!txResult.result.vm.exceptionError) { + return ret + } + var exceptionError = txResult.result.vm.exceptionError.error || '' + var error = `VM error: ${exceptionError}.\n` + var msg + if (exceptionError === errorCode.INVALID_OPCODE) { + msg = `\t\n\tThe execution might have thrown.\n` + ret.error = true + } else if (exceptionError === errorCode.OUT_OF_GAS) { + msg = `\tThe transaction ran out of gas. Please increase the Gas Limit.\n` + ret.error = true + } else if (exceptionError === errorCode.REVERT) { + var returnData = txResult.result.vm.return + // It is the hash of Error(string) + if (returnData && (returnData.slice(0, 4).toString('hex') === '08c379a0')) { + var abiCoder = new ethers.utils.AbiCoder() + var reason = abiCoder.decode(['string'], returnData.slice(4))[0] + msg = `\tThe transaction has been reverted to the initial state.\nReason provided by the contract: "${reason}".` + } else { + msg = `\tThe transaction has been reverted to the initial state.\nNote: The constructor should be payable if you send value.` + } + ret.error = true + } else if (exceptionError === errorCode.STATIC_STATE_CHANGE) { + msg = `\tState changes is not allowed in Static Call context\n` + ret.error = true + } + ret.message = `${error}${exceptionError}${msg}\tDebug the transaction to get more information.` + return ret + } +} + diff --git a/remix-lib/src/execution/txFormat.js b/remix-lib/src/execution/txFormat.js new file mode 100644 index 0000000000..9796c02049 --- /dev/null +++ b/remix-lib/src/execution/txFormat.js @@ -0,0 +1,391 @@ +'use strict' +var ethers = require('ethers') +var helper = require('./txHelper') +var executionContext = require('./execution-context') +var asyncJS = require('async') +var solcLinker = require('solc/linker') +var ethJSUtil = require('ethereumjs-util') + +module.exports = { + + /** + * build the transaction data + * + * @param {Object} function abi + * @param {Object} values to encode + * @param {String} contractbyteCode + */ + encodeData: function (funABI, values, contractbyteCode) { + var encoded + var encodedHex + try { + encoded = helper.encodeParams(funABI, values) + encodedHex = encoded.toString('hex') + } catch (e) { + return { error: 'cannot encode arguments' } + } + if (contractbyteCode) { + return { data: '0x' + contractbyteCode + encodedHex.replace('0x', '') } + } else { + return { data: helper.encodeFunctionId(funABI) + encodedHex.replace('0x', '') } + } + }, + + /** + * encode function / constructor parameters + * + * @param {Object} params - input paramater of the function to call + * @param {Object} funAbi - abi definition of the function to call. null if building data for the ctor. + * @param {Function} callback - callback + */ + encodeParams: function (params, funAbi, callback) { + var data = '' + var dataHex = '' + var funArgs + if (params.indexOf('raw:0x') === 0) { + // in that case we consider that the input is already encoded and *does not* contain the method signature + dataHex = params.replace('raw:0x', '') + data = Buffer.from(dataHex, 'hex') + } else { + try { + params = params.replace(/(^|,\s+|,)(\d+)(\s+,|,|$)/g, '$1"$2"$3') // replace non quoted number by quoted number + params = params.replace(/(^|,\s+|,)(0[xX][0-9a-fA-F]+)(\s+,|,|$)/g, '$1"$2"$3') // replace non quoted hex string by quoted hex string + funArgs = JSON.parse('[' + params + ']') + } catch (e) { + callback('Error encoding arguments: ' + e) + return + } + if (funArgs.length > 0) { + try { + data = helper.encodeParams(funAbi, funArgs) + dataHex = data.toString('hex') + } catch (e) { + callback('Error encoding arguments: ' + e) + return + } + } + if (data.slice(0, 9) === 'undefined') { + dataHex = data.slice(9) + } + if (data.slice(0, 2) === '0x') { + dataHex = data.slice(2) + } + } + callback(null, { data: data, dataHex: dataHex, funArgs: funArgs }) + }, + + /** + * encode function call (function id + encoded parameters) + * + * @param {Object} params - input paramater of the function to call + * @param {Object} funAbi - abi definition of the function to call. null if building data for the ctor. + * @param {Function} callback - callback + */ + encodeFunctionCall: function (params, funAbi, callback) { + this.encodeParams(params, funAbi, (error, encodedParam) => { + if (error) return callback(error) + callback(null, { dataHex: helper.encodeFunctionId(funAbi) + encodedParam.dataHex, funAbi, funArgs: encodedParam.funArgs }) + }) + }, + + /** + * encode constructor creation and link with provided libraries if needed + * + * @param {Object} contract - input paramater of the function to call + * @param {Object} params - input paramater of the function to call + * @param {Object} funAbi - abi definition of the function to call. null if building data for the ctor. + * @param {Object} linkLibraries - contains {linkReferences} object which list all the addresses to be linked + * @param {Object} linkReferences - given by the compiler, contains the proper linkReferences + * @param {Function} callback - callback + */ + encodeConstructorCallAndLinkLibraries: function (contract, params, funAbi, linkLibraries, linkReferences, callback) { + this.encodeParams(params, funAbi, (error, encodedParam) => { + if (error) return callback(error) + var bytecodeToDeploy = contract.evm.bytecode.object + if (bytecodeToDeploy.indexOf('_') >= 0) { + if (linkLibraries && linkReferences) { + for (var libFile in linkLibraries) { + for (var lib in linkLibraries[libFile]) { + var address = linkLibraries[libFile][lib] + if (!ethJSUtil.isValidAddress(address)) return callback(address + ' is not a valid address. Please check the provided address is valid.') + bytecodeToDeploy = this.linkLibraryStandardFromlinkReferences(lib, address.replace('0x', ''), bytecodeToDeploy, linkReferences) + } + } + } + } + if (bytecodeToDeploy.indexOf('_') >= 0) { + return callback('Failed to link some libraries') + } + return callback(null, { dataHex: bytecodeToDeploy + encodedParam.dataHex, funAbi, funArgs: encodedParam.funArgs, contractBytecode: contract.evm.bytecode.object }) + }) + }, + + /** + * encode constructor creation and deploy librairies if needed + * + * @param {String} contractName - current contract name + * @param {Object} contract - input paramater of the function to call + * @param {Object} contracts - map of all compiled contracts. + * @param {Object} params - input paramater of the function to call + * @param {Object} funAbi - abi definition of the function to call. null if building data for the ctor. + * @param {Function} callback - callback + * @param {Function} callbackStep - callbackStep + * @param {Function} callbackDeployLibrary - callbackDeployLibrary + * @param {Function} callback - callback + */ + encodeConstructorCallAndDeployLibraries: function (contractName, contract, contracts, params, funAbi, callback, callbackStep, callbackDeployLibrary) { + this.encodeParams(params, funAbi, (error, encodedParam) => { + if (error) return callback(error) + var dataHex = '' + var contractBytecode = contract.evm.bytecode.object + var bytecodeToDeploy = contract.evm.bytecode.object + if (bytecodeToDeploy.indexOf('_') >= 0) { + this.linkBytecode(contract, contracts, (err, bytecode) => { + if (err) { + callback('Error deploying required libraries: ' + err) + } else { + bytecodeToDeploy = bytecode + dataHex + return callback(null, {dataHex: bytecodeToDeploy, funAbi, funArgs: encodedParam.funArgs, contractBytecode, contractName: contractName}) + } + }, callbackStep, callbackDeployLibrary) + return + } else { + dataHex = bytecodeToDeploy + encodedParam.dataHex + } + callback(null, {dataHex: bytecodeToDeploy, funAbi, funArgs: encodedParam.funArgs, contractBytecode, contractName: contractName}) + }) + }, + + /** + * (DEPRECATED) build the transaction data + * + * @param {String} contractName + * @param {Object} contract - abi definition of the current contract. + * @param {Object} contracts - map of all compiled contracts. + * @param {Bool} isConstructor - isConstructor. + * @param {Object} funAbi - abi definition of the function to call. null if building data for the ctor. + * @param {Object} params - input paramater of the function to call + * @param {Function} callback - callback + * @param {Function} callbackStep - callbackStep + * @param {Function} callbackDeployLibrary - callbackDeployLibrary + */ + buildData: function (contractName, contract, contracts, isConstructor, funAbi, params, callback, callbackStep, callbackDeployLibrary) { + var funArgs = '' + var data = '' + var dataHex = '' + + if (params.indexOf('raw:0x') === 0) { + // in that case we consider that the input is already encoded and *does not* contain the method signature + dataHex = params.replace('raw:0x', '') + data = Buffer.from(dataHex, 'hex') + } else { + try { + params = params.replace(/(^|,\s+|,)(\d+)(\s+,|,|$)/g, '$1"$2"$3') // replace non quoted number by quoted number + params = params.replace(/(^|,\s+|,)(0[xX][0-9a-fA-F]+)(\s+,|,|$)/g, '$1"$2"$3') // replace non quoted hex string by quoted hex string + funArgs = JSON.parse('[' + params + ']') + } catch (e) { + callback('Error encoding arguments: ' + e) + return + } + if (!isConstructor || funArgs.length > 0) { + try { + data = helper.encodeParams(funAbi, funArgs) + dataHex = data.toString('hex') + } catch (e) { + callback('Error encoding arguments: ' + e) + return + } + } + if (data.slice(0, 9) === 'undefined') { + dataHex = data.slice(9) + } + if (data.slice(0, 2) === '0x') { + dataHex = data.slice(2) + } + } + var contractBytecode + if (isConstructor) { + contractBytecode = contract.evm.bytecode.object + var bytecodeToDeploy = contract.evm.bytecode.object + if (bytecodeToDeploy.indexOf('_') >= 0) { + this.linkBytecode(contract, contracts, (err, bytecode) => { + if (err) { + callback('Error deploying required libraries: ' + err) + } else { + bytecodeToDeploy = bytecode + dataHex + return callback(null, {dataHex: bytecodeToDeploy, funAbi, funArgs, contractBytecode, contractName: contractName}) + } + }, callbackStep, callbackDeployLibrary) + return + } else { + dataHex = bytecodeToDeploy + dataHex + } + } else { + dataHex = helper.encodeFunctionId(funAbi) + dataHex + } + callback(null, { dataHex, funAbi, funArgs, contractBytecode, contractName: contractName }) + }, + + atAddress: function () {}, + + linkBytecodeStandard: function (contract, contracts, callback, callbackStep, callbackDeployLibrary) { + asyncJS.eachOfSeries(contract.evm.bytecode.linkReferences, (libs, file, cbFile) => { + asyncJS.eachOfSeries(contract.evm.bytecode.linkReferences[file], (libRef, libName, cbLibDeployed) => { + var library = contracts[file][libName] + if (library) { + this.deployLibrary(file + ':' + libName, libName, library, contracts, (error, address) => { + if (error) { + return cbLibDeployed(error) + } + var hexAddress = address.toString('hex') + if (hexAddress.slice(0, 2) === '0x') { + hexAddress = hexAddress.slice(2) + } + contract.evm.bytecode.object = this.linkLibraryStandard(libName, hexAddress, contract) + cbLibDeployed() + }, callbackStep, callbackDeployLibrary) + } else { + cbLibDeployed('Cannot find compilation data of library ' + libName) + } + }, (error) => { + cbFile(error) + }) + }, (error) => { + if (error) { + callbackStep(error) + } + callback(error, contract.evm.bytecode.object) + }) + }, + + linkBytecodeLegacy: function (contract, contracts, callback, callbackStep, callbackDeployLibrary) { + var libraryRefMatch = contract.evm.bytecode.object.match(/__([^_]{1,36})__/) + if (!libraryRefMatch) { + return callback('Invalid bytecode format.') + } + var libraryName = libraryRefMatch[1] + // file_name:library_name + var libRef = libraryName.match(/(.*):(.*)/) + if (!libRef) { + return callback('Cannot extract library reference ' + libraryName) + } + if (!contracts[libRef[1]] || !contracts[libRef[1]][libRef[2]]) { + return callback('Cannot find library reference ' + libraryName) + } + var libraryShortName = libRef[2] + var library = contracts[libRef[1]][libraryShortName] + if (!library) { + return callback('Library ' + libraryName + ' not found.') + } + this.deployLibrary(libraryName, libraryShortName, library, contracts, (err, address) => { + if (err) { + return callback(err) + } + var hexAddress = address.toString('hex') + if (hexAddress.slice(0, 2) === '0x') { + hexAddress = hexAddress.slice(2) + } + contract.evm.bytecode.object = this.linkLibrary(libraryName, hexAddress, contract.evm.bytecode.object) + this.linkBytecode(contract, contracts, callback, callbackStep, callbackDeployLibrary) + }, callbackStep, callbackDeployLibrary) + }, + + linkBytecode: function (contract, contracts, callback, callbackStep, callbackDeployLibrary) { + if (contract.evm.bytecode.object.indexOf('_') < 0) { + return callback(null, contract.evm.bytecode.object) + } + if (contract.evm.bytecode.linkReferences && Object.keys(contract.evm.bytecode.linkReferences).length) { + this.linkBytecodeStandard(contract, contracts, callback, callbackStep, callbackDeployLibrary) + } else { + this.linkBytecodeLegacy(contract, contracts, callback, callbackStep, callbackDeployLibrary) + } + }, + + deployLibrary: function (libraryName, libraryShortName, library, contracts, callback, callbackStep, callbackDeployLibrary) { + var address = library.address + if (address) { + return callback(null, address) + } + var bytecode = library.evm.bytecode.object + if (bytecode.indexOf('_') >= 0) { + this.linkBytecode(library, contracts, (err, bytecode) => { + if (err) callback(err) + else this.deployLibrary(libraryName, libraryShortName, library, contracts, callback, callbackStep, callbackDeployLibrary) + }, callbackStep, callbackDeployLibrary) + } else { + callbackStep(`creation of library ${libraryName} pending...`) + var data = {dataHex: bytecode, funAbi: {type: 'constructor'}, funArgs: [], contractBytecode: bytecode, contractName: libraryShortName} + callbackDeployLibrary({ data: data, useCall: false }, (err, txResult) => { + if (err) { + return callback(err) + } + var address = executionContext.isVM() ? txResult.result.createdAddress : txResult.result.contractAddress + library.address = address + callback(err, address) + }) + } + }, + + linkLibraryStandardFromlinkReferences: function (libraryName, address, bytecode, linkReferences) { + for (var file in linkReferences) { + for (var libName in linkReferences[file]) { + if (libraryName === libName) { + bytecode = this.setLibraryAddress(address, bytecode, linkReferences[file][libName]) + } + } + } + return bytecode + }, + + linkLibraryStandard: function (libraryName, address, contract) { + return this.linkLibraryStandardFromlinkReferences(libraryName, address, contract.evm.bytecode.object, contract.evm.bytecode.linkReferences) + }, + + setLibraryAddress: function (address, bytecodeToLink, positions) { + if (positions) { + for (var pos of positions) { + var regpos = bytecodeToLink.match(new RegExp(`(.{${2 * pos.start}})(.{${2 * pos.length}})(.*)`)) + if (regpos) { + bytecodeToLink = regpos[1] + address + regpos[3] + } + } + } + return bytecodeToLink + }, + + linkLibrary: function (libraryName, address, bytecodeToLink) { + return solcLinker.linkBytecode(bytecodeToLink, { [libraryName]: ethJSUtil.addHexPrefix(address) }) + }, + + decodeResponse: function (response, fnabi) { + // Only decode if there supposed to be fields + if (fnabi.outputs && fnabi.outputs.length > 0) { + try { + var i + + var outputTypes = [] + for (i = 0; i < fnabi.outputs.length; i++) { + var type = fnabi.outputs[i].type + outputTypes.push(type === 'tuple' ? helper.makeFullTupleTypeDefinition(fnabi.outputs[i]) : type) + } + + if (!response.length) response = new Uint8Array(32 * fnabi.outputs.length) // ensuring the data is at least filled by 0 cause `AbiCoder` throws if there's not engouh data + // decode data + var abiCoder = new ethers.utils.AbiCoder() + var decodedObj = abiCoder.decode(outputTypes, response) + + var json = {} + for (i = 0; i < outputTypes.length; i++) { + var name = fnabi.outputs[i].name + json[i] = outputTypes[i] + ': ' + (name ? name + ' ' + decodedObj[i] : decodedObj[i]) + } + + return json + } catch (e) { + return { error: 'Failed to decode output: ' + e } + } + } + return {} + } +} + diff --git a/remix-lib/src/execution/txHelper.js b/remix-lib/src/execution/txHelper.js new file mode 100644 index 0000000000..938ca2413c --- /dev/null +++ b/remix-lib/src/execution/txHelper.js @@ -0,0 +1,130 @@ +'use strict' +var ethers = require('ethers') + +module.exports = { + makeFullTupleTypeDefinition: function (typeDef) { + if (typeDef && typeDef.type === 'tuple' && typeDef.components) { + var innerTypes = typeDef.components.map((innerType) => innerType.type) + return 'tuple(' + innerTypes.join(',') + ')' + } + return typeDef.type + }, + + encodeParams: function (funABI, args) { + var types = [] + if (funABI.inputs && funABI.inputs.length) { + for (var i = 0; i < funABI.inputs.length; i++) { + var type = funABI.inputs[i].type + types.push(type === 'tuple' ? this.makeFullTupleTypeDefinition(funABI.inputs[i]) : type) + if (args.length < types.length) { + args.push('') + } + } + } + + // NOTE: the caller will concatenate the bytecode and this + // it could be done here too for consistency + var abiCoder = new ethers.utils.AbiCoder() + return abiCoder.encode(types, args) + }, + + encodeFunctionId: function (funABI) { + if (funABI.type === 'fallback') return '0x' + var abi = new ethers.Interface([funABI]) + abi = abi.functions[funABI.name] + return abi.sighash + }, + + sortAbiFunction: function (contractabi) { + // Sorts the list of ABI entries. Constant functions will appear first, + // followed by non-constant functions. Within those t wo groupings, functions + // will be sorted by their names. + return contractabi.sort(function (a, b) { + if (a.constant === true && b.constant !== true) { + return 1 + } else if (b.constant === true && a.constant !== true) { + return -1 + } + // If we reach here, either a and b are both constant or both not; sort by name then + // special case for fallback and constructor + if (a.type === 'function' && typeof a.name !== 'undefined') { + return a.name.localeCompare(b.name) + } else if (a.type === 'constructor' || a.type === 'fallback') { + return 1 + } + }) + }, + + getConstructorInterface: function (abi) { + var funABI = { 'name': '', 'inputs': [], 'type': 'constructor', 'outputs': [] } + if (typeof abi === 'string') { + try { + abi = JSON.parse(abi) + } catch (e) { + console.log('exception retrieving ctor abi ' + abi) + return funABI + } + } + + for (var i = 0; i < abi.length; i++) { + if (abi[i].type === 'constructor') { + funABI.inputs = abi[i].inputs || [] + break + } + } + + return funABI + }, + + getFunction: function (abi, fnName) { + for (var i = 0; i < abi.length; i++) { + if (abi[i].name === fnName) { + return abi[i] + } + } + return null + }, + + getFallbackInterface: function (abi) { + for (var i = 0; i < abi.length; i++) { + if (abi[i].type === 'fallback') { + return abi[i] + } + } + }, + + /** + * return the contract obj of the given @arg name. Uses last compilation result. + * return null if not found + * @param {String} name - contract name + * @returns contract obj and associated file: { contract, file } or null + */ + getContract: (contractName, contracts) => { + for (var file in contracts) { + if (contracts[file][contractName]) { + return { object: contracts[file][contractName], file: file } + } + } + return null + }, + + /** + * call the given @arg cb (function) for all the contracts. Uses last compilation result + * stop visiting when cb return true + * @param {Function} cb - callback + */ + visitContracts: (contracts, cb) => { + for (var file in contracts) { + for (var name in contracts[file]) { + if (cb({ name: name, object: contracts[file][name], file: file })) return + } + } + }, + + inputParametersDeclarationToString: function (abiinputs) { + var inputs = (abiinputs || []).map((inp) => inp.type + ' ' + inp.name) + return inputs.join(', ') + } + +} + diff --git a/remix-lib/src/execution/txListener.js b/remix-lib/src/execution/txListener.js new file mode 100644 index 0000000000..6f32d347ba --- /dev/null +++ b/remix-lib/src/execution/txListener.js @@ -0,0 +1,368 @@ +'use strict' +var async = require('async') +var ethers = require('ethers') +var ethJSUtil = require('ethereumjs-util') +var EventManager = require('../eventManager') +var codeUtil = require('../util') + +var executionContext = require('./execution-context') +var txFormat = require('./txFormat') +var txHelper = require('./txHelper') + +/** + * poll web3 each 2s if web3 + * listen on transaction executed event if VM + * attention: blocks returned by the event `newBlock` have slightly different json properties whether web3 or the VM is used + * trigger 'newBlock' + * + */ +class TxListener { + constructor (opt) { + this.event = new EventManager() + this._api = opt.api + this._resolvedTransactions = {} + this._resolvedContracts = {} + this._isListening = false + this._listenOnNetwork = false + this._loopId = null + this.init() + executionContext.event.register('contextChanged', (context) => { + if (this._isListening) { + this.stopListening() + this.startListening() + } + }) + + opt.event.udapp.register('callExecuted', (error, from, to, data, lookupOnly, txResult) => { + if (error) return + // we go for that case if + // in VM mode + // in web3 mode && listen remix txs only + if (!this._isListening) return // we don't listen + if (this._loopId && executionContext.getProvider() !== 'vm') return // we seems to already listen on a "web3" network + + var call = { + from: from, + to: to, + input: data, + hash: txResult.transactionHash ? txResult.transactionHash : 'call' + (from || '') + to + data, + isCall: true, + returnValue: executionContext.isVM() ? txResult.result.vm.return : ethJSUtil.toBuffer(txResult.result), + envMode: executionContext.getProvider() + } + + addExecutionCosts(txResult, call) + this._resolveTx(call, call, (error, resolvedData) => { + if (!error) { + this.event.trigger('newCall', [call]) + } + }) + }) + + opt.event.udapp.register('transactionExecuted', (error, from, to, data, lookupOnly, txResult) => { + if (error) return + if (lookupOnly) return + // we go for that case if + // in VM mode + // in web3 mode && listen remix txs only + if (!this._isListening) return // we don't listen + if (this._loopId && executionContext.getProvider() !== 'vm') return // we seems to already listen on a "web3" network + executionContext.web3().eth.getTransaction(txResult.transactionHash, (error, tx) => { + if (error) return console.log(error) + + addExecutionCosts(txResult, tx) + tx.envMode = executionContext.getProvider() + tx.status = txResult.result.status // 0x0 or 0x1 + this._resolve([tx], () => { + }) + }) + }) + + function addExecutionCosts (txResult, tx) { + if (txResult && txResult.result) { + if (txResult.result.vm) { + tx.returnValue = txResult.result.vm.return + if (txResult.result.vm.gasUsed) tx.executionCost = txResult.result.vm.gasUsed.toString(10) + } + if (txResult.result.gasUsed) tx.transactionCost = txResult.result.gasUsed.toString(10) + } + } + } + + /** + * define if txlistener should listen on the network or if only tx created from remix are managed + * + * @param {Bool} type - true if listen on the network + */ + setListenOnNetwork (listenOnNetwork) { + this._listenOnNetwork = listenOnNetwork + if (this._loopId) { + clearInterval(this._loopId) + } + if (this._listenOnNetwork) { + this._startListenOnNetwork() + } + } + + /** + * reset recorded transactions + */ + init () { + this.blocks = [] + this.lastBlock = null + } + + /** + * start listening for incoming transactions + * + * @param {String} type - type/name of the provider to add + * @param {Object} obj - provider + */ + startListening () { + this.init() + this._isListening = true + if (this._listenOnNetwork && executionContext.getProvider() !== 'vm') { + this._startListenOnNetwork() + } + } + + /** + * stop listening for incoming transactions. do not reset the recorded pool. + * + * @param {String} type - type/name of the provider to add + * @param {Object} obj - provider + */ + stopListening () { + if (this._loopId) { + clearInterval(this._loopId) + } + this._loopId = null + this._isListening = false + } + + _startListenOnNetwork () { + this._loopId = setInterval(() => { + var currentLoopId = this._loopId + executionContext.web3().eth.getBlockNumber((error, blockNumber) => { + if (this._loopId === null) return + if (error) return console.log(error) + if (currentLoopId === this._loopId && (!this.lastBlock || blockNumber > this.lastBlock)) { + if (!this.lastBlock) this.lastBlock = blockNumber - 1 + var current = this.lastBlock + 1 + this.lastBlock = blockNumber + while (blockNumber >= current) { + try { + this._manageBlock(current) + } catch (e) { + console.log(e) + } + current++ + } + } + }) + }, 2000) + } + + _manageBlock (blockNumber) { + executionContext.web3().eth.getBlock(blockNumber, true, (error, result) => { + if (!error) { + this._newBlock(Object.assign({type: 'web3'}, result)) + } + }) + } + + /** + * try to resolve the contract name from the given @arg address + * + * @param {String} address - contract address to resolve + * @return {String} - contract name + */ + resolvedContract (address) { + return this._resolvedContracts[address] + } + + /** + * try to resolve the transaction from the given @arg txHash + * + * @param {String} txHash - contract address to resolve + * @return {String} - contract name + */ + resolvedTransaction (txHash) { + return this._resolvedTransactions[txHash] + } + + _newBlock (block) { + this.blocks.push(block) + this._resolve(block.transactions, () => { + this.event.trigger('newBlock', [block]) + }) + } + + _resolve (transactions, callback) { + async.each(transactions, (tx, cb) => { + this._api.resolveReceipt(tx, (error, receipt) => { + if (error) return cb(error) + this._resolveTx(tx, receipt, (error, resolvedData) => { + if (error) cb(error) + if (resolvedData) { + this.event.trigger('txResolved', [tx, receipt, resolvedData]) + } + this.event.trigger('newTransaction', [tx, receipt]) + cb() + }) + }) + }, () => { + callback() + }) + } + + _resolveTx (tx, receipt, cb) { + var contracts = this._api.contracts() + if (!contracts) return cb() + var contractName + var fun + if (!tx.to || tx.to === '0x0') { // testrpc returns 0x0 in that case + // contract creation / resolve using the creation bytes code + // if web3: we have to call getTransactionReceipt to get the created address + // if VM: created address already included + var code = tx.input + contractName = this._tryResolveContract(code, contracts, true) + if (contractName) { + var address = receipt.contractAddress + this._resolvedContracts[address] = contractName + fun = this._resolveFunction(contractName, contracts, tx, true) + if (this._resolvedTransactions[tx.hash]) { + this._resolvedTransactions[tx.hash].contractAddress = address + } + return cb(null, {to: null, contractName: contractName, function: fun, creationAddress: address}) + } + return cb() + } else { + // first check known contract, resolve against the `runtimeBytecode` if not known + contractName = this._resolvedContracts[tx.to] + if (!contractName) { + executionContext.web3().eth.getCode(tx.to, (error, code) => { + if (error) return cb(error) + if (code) { + var contractName = this._tryResolveContract(code, contracts, false) + if (contractName) { + this._resolvedContracts[tx.to] = contractName + var fun = this._resolveFunction(contractName, contracts, tx, false) + return cb(null, {to: tx.to, contractName: contractName, function: fun}) + } + } + return cb() + }) + return + } + if (contractName) { + fun = this._resolveFunction(contractName, contracts, tx, false) + return cb(null, {to: tx.to, contractName: contractName, function: fun}) + } + return cb() + } + } + + _resolveFunction (contractName, compiledContracts, tx, isCtor) { + var contract = txHelper.getContract(contractName, compiledContracts) + if (!contract) { + console.log('txListener: cannot resolve ' + contractName) + return + } + var abi = contract.object.abi + var inputData = tx.input.replace('0x', '') + if (!isCtor) { + var methodIdentifiers = contract.object.evm.methodIdentifiers + for (var fn in methodIdentifiers) { + if (methodIdentifiers[fn] === inputData.substring(0, 8)) { + var fnabi = getFunction(abi, fn) + this._resolvedTransactions[tx.hash] = { + contractName: contractName, + to: tx.to, + fn: fn, + params: this._decodeInputParams(inputData.substring(8), fnabi) + } + if (tx.returnValue) { + this._resolvedTransactions[tx.hash].decodedReturnValue = txFormat.decodeResponse(tx.returnValue, fnabi) + } + return this._resolvedTransactions[tx.hash] + } + } + // fallback function + this._resolvedTransactions[tx.hash] = { + contractName: contractName, + to: tx.to, + fn: '(fallback)', + params: null + } + } else { + var bytecode = contract.object.evm.bytecode.object + var params = null + if (bytecode && bytecode.length) { + params = this._decodeInputParams(inputData.substring(bytecode.length), getConstructorInterface(abi)) + } + this._resolvedTransactions[tx.hash] = { + contractName: contractName, + to: null, + fn: '(constructor)', + params: params + } + } + return this._resolvedTransactions[tx.hash] + } + + _tryResolveContract (codeToResolve, compiledContracts, isCreation) { + var found = null + txHelper.visitContracts(compiledContracts, (contract) => { + var bytes = isCreation ? contract.object.evm.bytecode.object : contract.object.evm.deployedBytecode.object + if (codeUtil.compareByteCode(codeToResolve, '0x' + bytes)) { + found = contract.name + return true + } + }) + return found + } + + _decodeInputParams (data, abi) { + data = ethJSUtil.toBuffer('0x' + data) + if (!data.length) data = new Uint8Array(32 * abi.inputs.length) // ensuring the data is at least filled by 0 cause `AbiCoder` throws if there's not engouh data + + var inputTypes = [] + for (var i = 0; i < abi.inputs.length; i++) { + var type = abi.inputs[i].type + inputTypes.push(type === 'tuple' ? txHelper.makeFullTupleTypeDefinition(abi.inputs[i]) : type) + } + var abiCoder = new ethers.utils.AbiCoder() + var decoded = abiCoder.decode(inputTypes, data) + var ret = {} + for (var k in abi.inputs) { + ret[abi.inputs[k].type + ' ' + abi.inputs[k].name] = decoded[k] + } + return ret + } +} + +// those function will be duplicate after the merged of the compile and run tabs split +function getConstructorInterface (abi) { + var funABI = { 'name': '', 'inputs': [], 'type': 'constructor', 'outputs': [] } + for (var i = 0; i < abi.length; i++) { + if (abi[i].type === 'constructor') { + funABI.inputs = abi[i].inputs || [] + break + } + } + + return funABI +} + +function getFunction (abi, fnName) { + fnName = fnName.split('(')[0] + for (var i = 0; i < abi.length; i++) { + if (abi[i].name === fnName) { + return abi[i] + } + } + return null +} + +module.exports = TxListener diff --git a/remix-lib/src/execution/txRunner.js b/remix-lib/src/execution/txRunner.js new file mode 100644 index 0000000000..30b60fa6fd --- /dev/null +++ b/remix-lib/src/execution/txRunner.js @@ -0,0 +1,212 @@ +'use strict' +var EthJSTX = require('ethereumjs-tx') +var EthJSBlock = require('ethereumjs-block') +var ethJSUtil = require('ethereumjs-util') +var BN = ethJSUtil.BN +var executionContext = require('./execution-context') +var EventManager = require('../eventManager') + +function TxRunner (vmaccounts, api) { + this.event = new EventManager() + this._api = api + this.blockNumber = 0 + this.runAsync = true + if (executionContext.isVM()) { + this.blockNumber = 1150000 // The VM is running in Homestead mode, which started at this block. + this.runAsync = false // We have to run like this cause the VM Event Manager does not support running multiple txs at the same time. + } + this.pendingTxs = {} + this.vmaccounts = vmaccounts + this.queusTxs = [] +} + +TxRunner.prototype.rawRun = function (args, confirmationCb, gasEstimationForceSend, promptCb, cb) { + run(this, args, Date.now(), confirmationCb, gasEstimationForceSend, promptCb, cb) +} + +TxRunner.prototype._executeTx = function (tx, gasPrice, api, promptCb, callback) { + if (gasPrice) tx.gasPrice = executionContext.web3().toHex(gasPrice) + if (api.personalMode()) { + promptCb( + (value) => { + this._sendTransaction(executionContext.web3().personal.sendTransaction, tx, value, callback) + }, + () => { + return callback('Canceled by user.') + } + ) + } else { + this._sendTransaction(executionContext.web3().eth.sendTransaction, tx, null, callback) + } +} + +TxRunner.prototype._sendTransaction = function (sendTx, tx, pass, callback) { + var self = this + var cb = function (err, resp) { + if (err) { + return callback(err, resp) + } + self.event.trigger('transactionBroadcasted', [resp]) + tryTillResponse(resp, callback) + } + var args = pass !== null ? [tx, pass, cb] : [tx, cb] + try { + sendTx.apply({}, args) + } catch (e) { + return callback(`Send transaction failed: ${e.message} . if you use an injected provider, please check it is properly unlocked. `) + } +} + +TxRunner.prototype.execute = function (args, confirmationCb, gasEstimationForceSend, promptCb, callback) { + var self = this + + var data = args.data + if (data.slice(0, 2) !== '0x') { + data = '0x' + data + } + + if (!executionContext.isVM()) { + self.runInNode(args.from, args.to, data, args.value, args.gasLimit, args.useCall, confirmationCb, gasEstimationForceSend, promptCb, callback) + } else { + try { + self.runInVm(args.from, args.to, data, args.value, args.gasLimit, args.useCall, callback) + } catch (e) { + callback(e, null) + } + } +} + +TxRunner.prototype.runInVm = function (from, to, data, value, gasLimit, useCall, callback) { + const self = this + var account = self.vmaccounts[from] + if (!account) { + return callback('Invalid account selected') + } + var tx = new EthJSTX({ + nonce: new BN(account.nonce++), + gasPrice: new BN(1), + gasLimit: new BN(gasLimit, 10), + to: to, + value: new BN(value, 10), + data: new Buffer(data.slice(2), 'hex') + }) + tx.sign(account.privateKey) + + const coinbases = [ '0x0e9281e9c6a0808672eaba6bd1220e144c9bb07a', '0x8945a1288dc78a6d8952a92c77aee6730b414778', '0x94d76e24f818426ae84aa404140e8d5f60e10e7e' ] + const difficulties = [ new BN('69762765929000', 10), new BN('70762765929000', 10), new BN('71762765929000', 10) ] + var block = new EthJSBlock({ + header: { + timestamp: new Date().getTime() / 1000 | 0, + number: self.blockNumber, + coinbase: coinbases[self.blockNumber % coinbases.length], + difficulty: difficulties[self.blockNumber % difficulties.length], + gasLimit: new BN(gasLimit, 10).imuln(2) + }, + transactions: [], + uncleHeaders: [] + }) + if (!useCall) { + ++self.blockNumber + } else { + executionContext.vm().stateManager.checkpoint() + } + + executionContext.vm().runTx({block: block, tx: tx, skipBalance: true, skipNonce: true}, function (err, result) { + if (useCall) { + executionContext.vm().stateManager.revert(function () {}) + } + err = err ? err.message : err + if (result) { + result.status = '0x' + result.vm.exception.toString(16) + } + callback(err, { + result: result, + transactionHash: ethJSUtil.bufferToHex(new Buffer(tx.hash())) + }) + }) +} + +TxRunner.prototype.runInNode = function (from, to, data, value, gasLimit, useCall, confirmCb, gasEstimationForceSend, promptCb, callback) { + const self = this + var tx = { from: from, to: to, data: data, value: value } + + if (useCall) { + tx.gas = gasLimit + return executionContext.web3().eth.call(tx, function (error, result) { + callback(error, { + result: result, + transactionHash: result.transactionHash + }) + }) + } + executionContext.web3().eth.estimateGas(tx, function (err, gasEstimation) { + gasEstimationForceSend(err, () => { + // callback is called whenever no error + tx.gas = !gasEstimation ? gasLimit : gasEstimation + + if (self._api.config.getUnpersistedProperty('doNotShowTransactionConfirmationAgain')) { + return self._executeTx(tx, null, self._api, promptCb, callback) + } + + self._api.detectNetwork((err, network) => { + if (err) { + console.log(err) + return + } + + confirmCb(network, tx, tx.gas, (gasPrice) => { + return self._executeTx(tx, gasPrice, self._api, promptCb, callback) + }, (error) => { + callback(error) + }) + }) + }, () => { + var blockGasLimit = executionContext.currentblockGasLimit() + // NOTE: estimateGas very likely will return a large limit if execution of the code failed + // we want to be able to run the code in order to debug and find the cause for the failure + if (err) return callback(err) + + var warnEstimation = ' An important gas estimation might also be the sign of a problem in the contract code. Please check loops and be sure you did not sent value to a non payable function (that\'s also the reason of strong gas estimation). ' + warnEstimation += ' ' + err + + if (gasEstimation > gasLimit) { + return callback('Gas required exceeds limit: ' + gasLimit + '. ' + warnEstimation) + } + if (gasEstimation > blockGasLimit) { + return callback('Gas required exceeds block gas limit: ' + gasLimit + '. ' + warnEstimation) + } + }) + }) +} + +function tryTillResponse (txhash, done) { + executionContext.web3().eth.getTransactionReceipt(txhash, function (err, result) { + if (err || !result) { + // Try again with a bit of delay if error or if result still null + setTimeout(function () { tryTillResponse(txhash, done) }, 500) + } else { + done(err, { + result: result, + transactionHash: result.transactionHash + }) + } + }) +} + +function run (self, tx, stamp, confirmationCb, gasEstimationForceSend, promptCb, callback) { + if (!self.runAsync && Object.keys(self.pendingTxs).length) { + self.queusTxs.push({ tx, stamp, callback }) + } else { + self.pendingTxs[stamp] = tx + self.execute(tx, confirmationCb, gasEstimationForceSend, promptCb, (error, result) => { + delete self.pendingTxs[stamp] + callback(error, result) + if (self.queusTxs.length) { + var next = self.queusTxs.pop() + run(self, next.tx, next.stamp, next.callback) + } + }) + } +} + +module.exports = TxRunner diff --git a/remix-lib/src/execution/typeConversion.js b/remix-lib/src/execution/typeConversion.js new file mode 100644 index 0000000000..1b5d879fc3 --- /dev/null +++ b/remix-lib/src/execution/typeConversion.js @@ -0,0 +1,42 @@ +'use strict' +var ethJSUtil = require('ethereumjs-util') +var BN = ethJSUtil.BN + +module.exports = { + toInt: (h) => { + if (h.indexOf && h.indexOf('0x') === 0) { + return (new BN(h.replace('0x', ''), 16)).toString(10) + } else if (h.constructor && h.constructor.name === 'BigNumber' || BN.isBN(h)) { + return h.toString(10) + } + return h + }, + stringify: stringify +} + +function stringify (v) { + try { + if (v instanceof Array) { + var ret = [] + for (var k in v) { + ret.push(stringify(v[k])) + } + return ret + } else if (BN.isBN(v) || (v.constructor && v.constructor.name === 'BigNumber')) { + return v.toString(10) + } else if (v._isBuffer) { + return ethJSUtil.bufferToHex(v) + } else if (typeof v === 'object') { + var retObject = {} + for (var i in v) { + retObject[i] = stringify(v[i]) + } + return retObject + } else { + return v + } + } catch (e) { + console.log(e) + return v + } +} diff --git a/remix-lib/src/helpers/compilerHelper.js b/remix-lib/src/helpers/compilerHelper.js new file mode 100644 index 0000000000..0e5767cff2 --- /dev/null +++ b/remix-lib/src/helpers/compilerHelper.js @@ -0,0 +1,26 @@ +module.exports = { + compilerInput: compilerInput +} + +function compilerInput (contracts) { + return JSON.stringify({ + language: 'Solidity', + sources: { + 'test.sol': { + content: contracts + } + }, + settings: { + optimizer: { + enabled: false, + runs: 200 + }, + outputSelection: { + '*': { + '': [ 'legacyAST' ], + '*': [ 'abi', 'metadata', 'evm.legacyAssembly', 'evm.bytecode', 'evm.deployedBytecode', 'evm.methodIdentifiers', 'evm.gasEstimates' ] + } + } + } + }) +} diff --git a/src/helpers/traceHelper.js b/remix-lib/src/helpers/traceHelper.js similarity index 63% rename from src/helpers/traceHelper.js rename to remix-lib/src/helpers/traceHelper.js index 38c3bd9f4b..a4fb9695eb 100644 --- a/src/helpers/traceHelper.js +++ b/remix-lib/src/helpers/traceHelper.js @@ -1,31 +1,6 @@ 'use strict' -var ui = require('../helpers/ui') +var ui = require('./uiHelper') module.exports = { - // util section - findLowerBound: function (target, changes) { - if (changes.length === 0) { - return undefined - } - - if (changes.length === 1) { - if (changes[0] > target) { - // we only a closest maximum, returning O - return 0 - } else { - return changes[0] - } - } - - var middle = Math.floor(changes.length / 2) - if (changes[middle] > target) { - return this.findLowerBound(target, changes.slice(0, middle)) - } else if (changes[middle] < target) { - return this.findLowerBound(target, changes.slice(middle, changes.length)) - } else { - return changes[middle] - } - }, - // vmTraceIndex has to point to a CALL, CODECALL, ... resolveCalledAddress: function (vmTraceIndex, trace) { var step = trace[vmTraceIndex] @@ -33,7 +8,7 @@ module.exports = { return this.contractCreationToken(vmTraceIndex) } else if (this.isCallInstruction(step)) { var stack = step.stack // callcode, delegatecall, ... - return ui.normalizeHex(stack[stack.length - 2]) + return ui.normalizeHexAddress(stack[stack.length - 2]) } return undefined }, @@ -50,10 +25,22 @@ module.exports = { return step.op === 'RETURN' }, + isJumpDestInstruction: function (step) { + return step.op === 'JUMPDEST' + }, + + isStopInstruction: function (step) { + return step.op === 'STOP' + }, + isSSTOREInstruction: function (step) { return step.op === 'SSTORE' }, + isSHA3Instruction: function (step) { + return step.op === 'SHA3' + }, + newContextStorage: function (step) { return step.op === 'CREATE' || step.op === 'CALL' }, @@ -62,7 +49,7 @@ module.exports = { // if stack empty => this is not a precompiled contract var step = trace[index] if (this.isCallInstruction(step)) { - return trace[index + 1].stack.length !== 0 + return index + 1 < trace.length && trace[index + 1].stack.length !== 0 } else { return false } diff --git a/src/helpers/ui.js b/remix-lib/src/helpers/uiHelper.js similarity index 50% rename from src/helpers/ui.js rename to remix-lib/src/helpers/uiHelper.js index f2e50a166b..99dfeb5970 100644 --- a/src/helpers/ui.js +++ b/remix-lib/src/helpers/uiHelper.js @@ -1,7 +1,7 @@ 'use strict' module.exports = { formatMemory: function (mem, width) { - var ret = '' + var ret = {} if (!mem) { return ret } @@ -13,7 +13,7 @@ module.exports = { for (var k = 0; k < mem.length; k += (width * 2)) { var memory = mem.substr(k, width * 2) var content = this.tryConvertAsciiFormat(memory) - ret += '0x' + k.toString(16) + ' ' + content.raw + ' ' + content.ascii + '\n' + ret['0x' + (k / 2).toString(16)] = content.raw + '\t' + content.ascii } return ret }, @@ -28,17 +28,32 @@ module.exports = { ascii = '?' } ret.ascii += ascii - ret.raw += ' ' + raw + ret.raw += raw } return ret }, + /** + * format @args css1, css2, css3 to css inline style + * + * @param {Object} css1 - css inline declaration + * @param {Object} css2 - css inline declaration + * @param {Object} css3 - css inline declaration + * @param {Object} ... + * @return {String} css inline style + * if the key start with * the value is direcly appended to the inline style (which should be already inline style formatted) + * used if multiple occurences of the same key is needed + */ formatCss: function (css1, css2) { var ret = '' for (var arg in arguments) { for (var k in arguments[arg]) { if (arguments[arg][k] && ret.indexOf(k) === -1) { - ret += k + ':' + arguments[arg][k] + ';' + if (k.indexOf('*') === 0) { + ret += arguments[arg][k] + } else { + ret += k + ':' + arguments[arg][k] + ';' + } } } } @@ -51,5 +66,21 @@ module.exports = { } hex = hex.replace(/^0+/, '') return '0x' + hex + }, + + normalizeHexAddress: function (hex) { + if (hex.indexOf('0x') === 0) hex = hex.replace('0x', '') + if (hex.length >= 40) { + var reg = /(.{40})$/.exec(hex) + if (reg) { + return '0x' + reg[0] + } + } else { + return '0x' + (new Array(40 - hex.length + 1).join('0')) + hex + } + }, + + runInBrowser: function () { + return typeof window !== 'undefined' } } diff --git a/remix-lib/src/init.js b/remix-lib/src/init.js new file mode 100644 index 0000000000..32261870d6 --- /dev/null +++ b/remix-lib/src/init.js @@ -0,0 +1,75 @@ +'use strict' +var Web3 = require('web3') + +module.exports = { + loadWeb3: function (url) { + if (!url) url = 'http://localhost:8545' + var web3 = new Web3() + web3.setProvider(new web3.providers.HttpProvider(url)) + this.extend(web3) + return web3 + }, + + extendWeb3: function (web3) { + this.extend(web3) + }, + + setProvider: function (web3, url) { + web3.setProvider(new web3.providers.HttpProvider(url)) + }, + + web3DebugNode: function (network) { + if (web3DebugNodes[network]) { + return this.loadWeb3(web3DebugNodes[network]) + } + return null + }, + + extend: function (web3) { + if (!web3._extend) { + return + } + // DEBUG + var methods = [] + if (!(web3.debug && web3.debug.preimage)) { + methods.push(new web3._extend.Method({ + name: 'preimage', + call: 'debug_preimage', + inputFormatter: [null], + params: 1 + })) + } + + if (!(web3.debug && web3.debug.traceTransaction)) { + methods.push(new web3._extend.Method({ + name: 'traceTransaction', + call: 'debug_traceTransaction', + inputFormatter: [null, null], + params: 2 + })) + } + + if (!(web3.debug && web3.debug.storageRangeAt)) { + methods.push(new web3._extend.Method({ + name: 'storageRangeAt', + call: 'debug_storageRangeAt', + inputFormatter: [null, null, null, null, null], + params: 5 + })) + } + if (methods.length > 0) { + web3._extend({ + property: 'debug', + methods: methods, + properties: [] + }) + } + } +} + +var web3DebugNodes = { + 'Main': 'https://mainnet.infura.io/remix', + 'Rinkeby': 'http://52.56.126.137:8545', + 'Ropsten': 'http://35.178.125.92:8545', + 'Kovan': 'http://35.176.227.86:8545' +} diff --git a/remix-lib/src/sourceLocationTracker.js b/remix-lib/src/sourceLocationTracker.js new file mode 100644 index 0000000000..0309faa2af --- /dev/null +++ b/remix-lib/src/sourceLocationTracker.js @@ -0,0 +1,97 @@ +'use strict' +var EventManager = require('./eventManager') +var helper = require('./helpers/traceHelper') +var SourceMappingDecoder = require('./sourceMappingDecoder') +var util = require('./util') + +/** + * Process the source code location for the current executing bytecode + */ +function SourceLocationTracker (_codeManager) { + this.codeManager = _codeManager + this.event = new EventManager() + this.sourceMappingDecoder = new SourceMappingDecoder() + this.sourceMapByAddress = {} +} + +/** + * Return the source location associated with the given @arg index + * + * @param {String} address - contract address from which the source location is retrieved + * @param {Int} index - index in the instruction list from where the source location is retrieved + * @param {Object} contractDetails - AST of compiled contracts + * @param {Function} cb - callback function + */ +SourceLocationTracker.prototype.getSourceLocationFromInstructionIndex = function (address, index, contracts, cb) { + var self = this + extractSourceMap(this, this.codeManager, address, contracts, function (error, sourceMap) { + if (error) { + cb(error) + } else { + cb(null, self.sourceMappingDecoder.atIndex(index, sourceMap)) + } + }) +} + +/** + * Return the source location associated with the given @arg pc + * + * @param {String} address - contract address from which the source location is retrieved + * @param {Int} vmtraceStepIndex - index of the current code in the vmtrace + * @param {Object} contractDetails - AST of compiled contracts + * @param {Function} cb - callback function + */ +SourceLocationTracker.prototype.getSourceLocationFromVMTraceIndex = function (address, vmtraceStepIndex, contracts, cb) { + var self = this + extractSourceMap(this, this.codeManager, address, contracts, function (error, sourceMap) { + if (!error) { + self.codeManager.getInstructionIndex(address, vmtraceStepIndex, function (error, index) { + if (error) { + cb(error) + } else { + cb(null, self.sourceMappingDecoder.atIndex(index, sourceMap)) + } + }) + } else { + cb(error) + } + }) +} + +SourceLocationTracker.prototype.clearCache = function () { + this.sourceMapByAddress = {} +} + +function getSourceMap (address, code, contracts) { + var isCreation = helper.isContractCreation(address) + var bytes + for (var file in contracts) { + for (var contract in contracts[file]) { + bytes = isCreation ? contracts[file][contract].evm.bytecode.object : contracts[file][contract].evm.deployedBytecode.object + if (util.compareByteCode(code, '0x' + bytes)) { + return isCreation ? contracts[file][contract].evm.bytecode.sourceMap : contracts[file][contract].evm.deployedBytecode.sourceMap + } + } + } + return null +} + +function extractSourceMap (self, codeManager, address, contracts, cb) { + if (self.sourceMapByAddress[address]) return cb(null, self.sourceMapByAddress[address]) + + codeManager.getCode(address, function (error, result) { + if (!error) { + var sourceMap = getSourceMap(address, result.bytecode, contracts) + if (sourceMap) { + if (!helper.isContractCreation(address)) self.sourceMapByAddress[address] = sourceMap + cb(null, sourceMap) + } else { + cb('no sourcemap associated with the code ' + address) + } + } else { + cb(error) + } + }) +} + +module.exports = SourceLocationTracker diff --git a/remix-lib/src/sourceMappingDecoder.js b/remix-lib/src/sourceMappingDecoder.js new file mode 100644 index 0000000000..ce5fc50ed8 --- /dev/null +++ b/remix-lib/src/sourceMappingDecoder.js @@ -0,0 +1,222 @@ +'use strict' +var util = require('./util') +var AstWalker = require('./astWalker') + +/** + * Decompress the source mapping given by solc-bin.js + */ +function SourceMappingDecoder () { + // s:l:f:j +} + +/** + * get a list of nodes that are at the given @arg position + * + * @param {String} astNodeType - type of node to return + * @param {Int} position - cursor position + * @return {Object} ast object given by the compiler + */ +SourceMappingDecoder.prototype.nodesAtPosition = nodesAtPosition + +/** + * Decode the source mapping for the given @arg index + * + * @param {Integer} index - source mapping index to decode + * @param {String} mapping - compressed source mapping given by solc-bin + * @return {Object} returns the decompressed source mapping for the given index {start, length, file, jump} + */ +SourceMappingDecoder.prototype.atIndex = atIndex + +/** + * Decode the given @arg value + * + * @param {string} value - source location to decode ( should be start:length:file ) + * @return {Object} returns the decompressed source mapping {start, length, file} + */ +SourceMappingDecoder.prototype.decode = function (value) { + if (value) { + value = value.split(':') + return { + start: parseInt(value[0]), + length: parseInt(value[1]), + file: parseInt(value[2]) + } + } +} + +/** + * Decode the source mapping for the given compressed mapping + * + * @param {String} mapping - compressed source mapping given by solc-bin + * @return {Array} returns the decompressed source mapping. Array of {start, length, file, jump} + */ +SourceMappingDecoder.prototype.decompressAll = function (mapping) { + var map = mapping.split(';') + var ret = [] + for (var k in map) { + var compressed = map[k].split(':') + var sourceMap = { + start: compressed[0] ? parseInt(compressed[0]) : ret[ret.length - 1].start, + length: compressed[1] ? parseInt(compressed[1]) : ret[ret.length - 1].length, + file: compressed[2] ? parseInt(compressed[2]) : ret[ret.length - 1].file, + jump: compressed[3] ? compressed[3] : ret[ret.length - 1].jump + } + ret.push(sourceMap) + } + return ret +} + +/** + * Retrieve line/column position of each source char + * + * @param {String} source - contract source code + * @return {Arrray} returns an array containing offset of line breaks + */ +SourceMappingDecoder.prototype.getLinebreakPositions = function (source) { + var ret = [] + for (var pos = source.indexOf('\n'); pos >= 0; pos = source.indexOf('\n', pos + 1)) { + ret.push(pos) + } + return ret +} + +/** + * Retrieve the line/colum position for the given source mapping + * + * @param {Object} sourceLocation - object containing attributes {source} and {length} + * @param {Array} lineBreakPositions - array returned by the function 'getLinebreakPositions' + * @return {Object} returns an object {start: {line, column}, end: {line, column}} (line/column count start at 0) + */ +SourceMappingDecoder.prototype.convertOffsetToLineColumn = function (sourceLocation, lineBreakPositions) { + if (sourceLocation.start >= 0 && sourceLocation.length >= 0) { + return { + start: convertFromCharPosition(sourceLocation.start, lineBreakPositions), + end: convertFromCharPosition(sourceLocation.start + sourceLocation.length, lineBreakPositions) + } + } else { + return { + start: null, + end: null + } + } +} + +/** + * Retrieve the first @arg astNodeType that include the source map at arg instIndex + * + * @param {String} astNodeType - node type that include the source map instIndex + * @param {String} instIndex - instruction index used to retrieve the source map + * @param {String} sourceMap - source map given by the compilation result + * @param {Object} ast - ast given by the compilation result + */ +SourceMappingDecoder.prototype.findNodeAtInstructionIndex = findNodeAtInstructionIndex + +function convertFromCharPosition (pos, lineBreakPositions) { + var line = util.findLowerBound(pos, lineBreakPositions) + if (lineBreakPositions[line] !== pos) { + line = line + 1 + } + var beginColumn = line === 0 ? 0 : (lineBreakPositions[line - 1] + 1) + var column = pos - beginColumn + return { + line: line, + column: column + } +} + +function sourceLocationFromAstNode (astNode) { + if (astNode.src) { + var split = astNode.src.split(':') + return { + start: parseInt(split[0]), + length: parseInt(split[1]), + file: parseInt(split[2]) + } + } + return null +} + +function findNodeAtInstructionIndex (astNodeType, instIndex, sourceMap, ast) { + var sourceLocation = atIndex(instIndex, sourceMap) + return findNodeAtSourceLocation(astNodeType, sourceLocation, ast) +} + +function findNodeAtSourceLocation (astNodeType, sourceLocation, ast) { + var astWalker = new AstWalker() + var callback = {} + var found = null + callback['*'] = function (node) { + var nodeLocation = sourceLocationFromAstNode(node) + if (!nodeLocation) { + return true + } + if (nodeLocation.start <= sourceLocation.start && nodeLocation.start + nodeLocation.length >= sourceLocation.start + sourceLocation.length) { + if (astNodeType === node.name) { + found = node + return false + } else { + return true + } + } else { + return false + } + } + astWalker.walk(ast.legacyAST, callback) + return found +} + +function nodesAtPosition (astNodeType, position, ast) { + var astWalker = new AstWalker() + var callback = {} + var found = [] + callback['*'] = function (node) { + var nodeLocation = sourceLocationFromAstNode(node) + if (!nodeLocation) { + return + } + if (nodeLocation.start <= position && nodeLocation.start + nodeLocation.length >= position) { + if (!astNodeType || astNodeType === node.name) { + found.push(node) + if (astNodeType) return false + } + return true + } else { + return false + } + } + astWalker.walk(ast.legacyAST, callback) + return found +} + +function atIndex (index, mapping) { + var ret = {} + var map = mapping.split(';') + if (index >= map.length) { + index = map.length - 1 + } + for (var k = index; k >= 0; k--) { + var current = map[k] + if (!current.length) { + continue + } + current = current.split(':') + if (ret.start === undefined && current[0] && current[0] !== '-1' && current[0].length) { + ret.start = parseInt(current[0]) + } + if (ret.length === undefined && current[1] && current[1] !== '-1' && current[1].length) { + ret.length = parseInt(current[1]) + } + if (ret.file === undefined && current[2] && current[2] !== '-1' && current[2].length) { + ret.file = parseInt(current[2]) + } + if (ret.jump === undefined && current[3] && current[3].length) { + ret.jump = current[3] + } + if (ret.start !== undefined && ret.length !== undefined && ret.file !== undefined && ret.jump !== undefined) { + break + } + } + return ret +} + +module.exports = SourceMappingDecoder diff --git a/remix-lib/src/storage.js b/remix-lib/src/storage.js new file mode 100644 index 0000000000..305c8cf9e0 --- /dev/null +++ b/remix-lib/src/storage.js @@ -0,0 +1,79 @@ +'use strict' + +function Storage (prefix) { + this.exists = function (name) { + if (typeof window !== 'undefined') { + return this.get(name) !== null + } + } + + this.get = function (name) { + if (typeof window !== 'undefined') { + return window.localStorage.getItem(prefix + name) + } + } + + this.set = function (name, content) { + try { + if (typeof window !== 'undefined') { + window.localStorage.setItem(prefix + name, content) + } + } catch (exception) { + return false + } + return true + } + + this.remove = function (name) { + if (typeof window !== 'undefined') { + window.localStorage.removeItem(prefix + name) + return true + } else { + return true + } + } + + this.rename = function (originalName, newName) { + var content = this.get(originalName) + if (!this.set(newName, content)) { + return false + } + this.remove(originalName) + return true + } + + function safeKeys () { + // NOTE: this is a workaround for some browsers + if (typeof window !== 'undefined') { + return Object.keys(window.localStorage).filter(function (item) { return item !== null && item !== undefined }) + } else { + return [] + } + } + + this.keys = function () { + return safeKeys() + // filter any names not including the prefix + .filter(function (item) { return item.indexOf(prefix, 0) === 0 }) + // remove prefix from filename and add the 'browser' path + .map(function (item) { return item.substr(prefix.length) }) + } + + // on startup, upgrade the old storage layout + if (typeof window !== 'undefined') { + safeKeys().forEach(function (name) { + if (name.indexOf('sol-cache-file-', 0) === 0) { + var content = window.localStorage.getItem(name) + window.localStorage.setItem(name.replace(/^sol-cache-file-/, 'sol:'), content) + window.localStorage.removeItem(name) + } + }) + } + + // remove obsolete key + if (typeof window !== 'undefined') { + window.localStorage.removeItem('editor-size-cache') + } +} + +module.exports = Storage diff --git a/remix-lib/src/util.js b/remix-lib/src/util.js new file mode 100644 index 0000000000..a30b8b9d56 --- /dev/null +++ b/remix-lib/src/util.js @@ -0,0 +1,259 @@ +'use strict' +var ethutil = require('ethereumjs-util') + +/* + contains misc util: @TODO should be splitted + - hex convertion + - binary search + - CALL related look up + - sha3 calculation + - swarm hash extraction + - bytecode comparison +*/ +module.exports = { + /* + ints: IntArray + */ + hexConvert: function (ints) { + var ret = '0x' + for (var i = 0; i < ints.length; i++) { + var h = ints[i] + if (h) { + ret += (h <= 0xf ? '0' : '') + h.toString(16) + } else { + ret += '00' + } + } + return ret + }, + + /** + * Converts a hex string to an array of integers. + */ + hexToIntArray: function (hexString) { + if (hexString.slice(0, 2) === '0x') { + hexString = hexString.slice(2) + } + var integers = [] + for (var i = 0; i < hexString.length; i += 2) { + integers.push(parseInt(hexString.slice(i, i + 2), 16)) + } + return integers + }, + + /* + ints: list of BNs + */ + hexListFromBNs: function (bnList) { + var ret = [] + for (var k in bnList) { + var v = bnList[k] + if (ethutil.BN.isBN(v)) { + ret.push('0x' + v.toString('hex', 64)) + } else { + ret.push('0x' + (new ethutil.BN(v)).toString('hex', 64)) // TEMP FIX TO REMOVE ONCE https://github.com/ethereumjs/ethereumjs-vm/pull/293 is released + } + } + return ret + }, + + /* + ints: list of IntArrays + */ + hexListConvert: function (intsList) { + var ret = [] + for (var k in intsList) { + ret.push(this.hexConvert(intsList[k])) + } + return ret + }, + + /* + ints: ints: IntArray + */ + formatMemory: function (mem) { + var hexMem = this.hexConvert(mem).substr(2) + var ret = [] + for (var k = 0; k < hexMem.length; k += 32) { + var row = hexMem.substr(k, 32) + ret.push(row) + } + return ret + }, + + /* + Binary Search: + Assumes that @arg array is sorted increasingly + return largest i such that array[i] <= target; return -1 if array[0] > target || array is empty + */ + findLowerBound: function (target, array) { + var start = 0 + var length = array.length + while (length > 0) { + var half = length >> 1 + var middle = start + half + if (array[middle] <= target) { + length = length - 1 - half + start = middle + 1 + } else { + length = half + } + } + return start - 1 + }, + + /* + Binary Search: + Assumes that @arg array is sorted increasingly + return largest array[i] such that array[i] <= target; return null if array[0] > target || array is empty + */ + findLowerBoundValue: function (target, array) { + var index = this.findLowerBound(target, array) + return index >= 0 ? array[index] : null + }, + + /* + Binary Search: + Assumes that @arg array is sorted increasingly + return Return i such that |array[i] - target| is smallest among all i and -1 for an empty array. + Returns the smallest i for multiple candidates. + */ + findClosestIndex: function (target, array) { + if (array.length === 0) { + return -1 + } + var index = this.findLowerBound(target, array) + if (index < 0) { + return 0 + } else if (index >= array.length - 1) { + return array.length - 1 + } else { + var middle = (array[index] + array[index + 1]) / 2 + return target <= middle ? index : index + 1 + } + }, + + /** + * Find the call from @args rootCall which contains @args index (recursive) + * + * @param {Int} index - index of the vmtrace + * @param {Object} rootCall - call tree, built by the trace analyser + * @return {Object} - return the call which include the @args index + */ + findCall: findCall, + + /** + * Find calls path from @args rootCall which leads to @args index (recursive) + * + * @param {Int} index - index of the vmtrace + * @param {Object} rootCall - call tree, built by the trace analyser + * @return {Array} - return the calls path to @args index + */ + buildCallPath: buildCallPath, + + /** + * sha3 the given @arg value (left pad to 32 bytes) + * + * @param {String} value - value to sha3 + * @return {Object} - return sha3ied value + */ + sha3_256: function (value) { + if (typeof value === 'string' && value.indexOf('0x') !== 0) { + value = '0x' + value + } + var ret = ethutil.bufferToHex(ethutil.setLengthLeft(value, 32)) + ret = ethutil.sha3(ret) + return ethutil.bufferToHex(ret) + }, + + /** + * return a regex which extract the swarmhash from the bytecode. + * + * @return {RegEx} + */ + swarmHashExtraction: function () { + return /a165627a7a72305820([0-9a-f]{64})0029$/ + }, + + /** + * Compare bytecode. return true if the code is equal (handle swarm hash and library references) + * @param {String} code1 - the bytecode that is actually deployed (contains resolved library reference and a potentially different swarmhash) + * @param {String} code2 - the bytecode generated by the compiler (contains unresolved library reference and a potentially different swarmhash) + this will return false if the generated bytecode is empty (asbtract contract cannot be deployed) + * + * @return {bool} + */ + compareByteCode: function (code1, code2) { + if (code1 === code2) return true + if (code2 === '0x') return false // abstract contract. see comment + + if (code2.substr(2, 46) === '7300000000000000000000000000000000000000003014') { + // testing the following signature: PUSH20 00..00 ADDRESS EQ + // in the context of a library, that slot contains the address of the library (pushed by the compiler to avoid calling library other than with a DELEGATECALL) + // if code2 is not a library, well we still suppose that the comparison remain relevant even if we remove some information from `code1` + code1 = replaceLibReference(code1, 4) + } + var pos = -1 + while ((pos = code2.search(/__(.*)__/)) !== -1) { + code2 = replaceLibReference(code2, pos) + code1 = replaceLibReference(code1, pos) + } + code1 = code1.replace(this.swarmHashExtraction(), '') + code2 = code2.replace(this.swarmHashExtraction(), '') + if (code1 && code2 && code1.indexOf(code2) === 0) { + return true + } + return false + }, + groupBy: groupBy, + concatWithSeperator: concatWithSeperator, + escapeRegExp: escapeRegExp +} + +function replaceLibReference (code, pos) { + return code.substring(0, pos) + '0000000000000000000000000000000000000000' + code.substring(pos + 40) +} + +function buildCallPath (index, rootCall) { + var ret = [] + findCallInternal(index, rootCall, ret) + return ret +} + +function findCall (index, rootCall) { + var ret = buildCallPath(index, rootCall) + return ret[ret.length - 1] +} + +function findCallInternal (index, rootCall, callsPath) { + var calls = Object.keys(rootCall.calls) + var ret = rootCall + callsPath.push(rootCall) + for (var k in calls) { + var subCall = rootCall.calls[calls[k]] + if (index >= subCall.start && index <= subCall.return) { + findCallInternal(index, subCall, callsPath) + break + } + } + return ret +} + +/* util extracted out from remix-ide. @TODO split this file, cause it mix real util fn with solidity related stuff ... */ +function groupBy (arr, key) { + return arr.reduce((sum, item) => { + const groupByVal = item[key] + var groupedItems = sum[groupByVal] || [] + groupedItems.push(item) + sum[groupByVal] = groupedItems + return sum + }, {}) +} + +function concatWithSeperator (list, seperator) { + return list.reduce((sum, item) => sum + item + seperator, '').slice(0, -seperator.length) +} + +function escapeRegExp (str) { + return str.replace(/[-[\]/{}()+?.\\^$|]/g, '\\$&') +} diff --git a/src/web3Provider/dummyProvider.js b/remix-lib/src/web3Provider/dummyProvider.js similarity index 80% rename from src/web3Provider/dummyProvider.js rename to remix-lib/src/web3Provider/dummyProvider.js index 57738c6438..5a898b0600 100644 --- a/src/web3Provider/dummyProvider.js +++ b/remix-lib/src/web3Provider/dummyProvider.js @@ -7,7 +7,7 @@ function dummyProvider () { this.eth.getTransactionFromBlock = function (blockNumber, txIndex, cb) { return self.getTransactionFromBlock(blockNumber, txIndex, cb) } this.eth.getBlockNumber = function (cb) { return self.getBlockNumber(cb) } this.debug.traceTransaction = function (hash, options, cb) { return self.traceTransaction(hash, options, cb) } - this.debug.storageAt = function (blockNumber, txIndex, address, cb) { return self.storageAt(blockNumber, txIndex, address, cb) } + this.debug.storageRangeAt = function (blockNumber, txIndex, address, start, end, maxLength, cb) { return self.storageRangeAt(blockNumber, txIndex, address, start, end, maxLength, cb) } this.providers = { 'HttpProvider': function (url) {} } this.currentProvider = {'host': ''} } @@ -25,7 +25,7 @@ dummyProvider.prototype.traceTransaction = function (txHash, options, cb) { return {} } -dummyProvider.prototype.storageAt = function (blockNumber, txIndex, address, cb) { cb(null, {}) } +dummyProvider.prototype.storageRangeAt = function (blockNumber, txIndex, address, start, end, maxLength, cb) { cb(null, {}) } dummyProvider.prototype.getBlockNumber = function (cb) { cb(null, '') } diff --git a/src/web3Provider/web3Providers.js b/remix-lib/src/web3Provider/web3Providers.js similarity index 72% rename from src/web3Provider/web3Providers.js rename to remix-lib/src/web3Provider/web3Providers.js index 4fbe0b673c..ad53b8b2e6 100644 --- a/src/web3Provider/web3Providers.js +++ b/remix-lib/src/web3Provider/web3Providers.js @@ -1,5 +1,5 @@ var Web3VMProvider = require('./web3VmProvider') -var init = require('../helpers/init') +var init = require('../init') function Web3Providers () { this.modes = {} @@ -9,19 +9,16 @@ Web3Providers.prototype.addProvider = function (type, obj) { if (type === 'INTERNAL') { var web3 = init.loadWeb3() this.addWeb3(type, web3) - } else if (type === 'EXTERNAL') { - init.extendWeb3(obj) - this.addWeb3(type, obj) - } else if (type === 'VM') { - this.addVM(obj) + } else if (type === 'vm') { + this.addVM(type, obj) } else { + init.extendWeb3(obj) this.addWeb3(type, obj) } } Web3Providers.prototype.get = function (type, cb) { if (this.modes[type]) { - this.currentMode = type cb(null, this.modes[type]) } else { cb('error: this provider has not been setup (' + type + ')', null) @@ -32,10 +29,10 @@ Web3Providers.prototype.addWeb3 = function (type, web3) { this.modes[type] = web3 } -Web3Providers.prototype.addVM = function (vm) { +Web3Providers.prototype.addVM = function (type, vm) { var vmProvider = new Web3VMProvider() vmProvider.setVM(vm) - this.modes['VM'] = vmProvider + this.modes[type] = vmProvider } module.exports = Web3Providers diff --git a/remix-lib/src/web3Provider/web3VmProvider.js b/remix-lib/src/web3Provider/web3VmProvider.js new file mode 100644 index 0000000000..189a44617e --- /dev/null +++ b/remix-lib/src/web3Provider/web3VmProvider.js @@ -0,0 +1,298 @@ +var util = require('../util') +var uiutil = require('../helpers/uiHelper') +var traceHelper = require('../helpers/traceHelper') +var ethutil = require('ethereumjs-util') +var Web3 = require('web3') + +function web3VmProvider () { + var self = this + this.web3 = new Web3() + this.vm + this.vmTraces = {} + this.txs = {} + this.txsReceipt = {} + this.processingHash + this.processingAddress + this.processingIndex + this.previousDepth = 0 + this.incr = 0 + this.eth = {} + this.debug = {} + this.eth.getCode = function (address, cb) { return self.getCode(address, cb) } + this.eth.getTransaction = function (hash, cb) { return self.getTransaction(hash, cb) } + this.eth.getTransactionReceipt = function (hash, cb) { return self.getTransactionReceipt(hash, cb) } + this.eth.getTransactionFromBlock = function (blockNumber, txIndex, cb) { return self.getTransactionFromBlock(blockNumber, txIndex, cb) } + this.eth.getBlockNumber = function (cb) { return self.getBlockNumber(cb) } + this.debug.traceTransaction = function (hash, options, cb) { return self.traceTransaction(hash, options, cb) } + this.debug.storageRangeAt = function (blockNumber, txIndex, address, start, end, maxLength, cb) { return self.storageRangeAt(blockNumber, txIndex, address, start, end, maxLength, cb) } + this.debug.preimage = function (hashedKey, cb) { return self.preimage(hashedKey, cb) } + this.providers = { 'HttpProvider': function (url) {} } + this.currentProvider = {'host': 'vm provider'} + this.storageCache = {} + this.sha3Preimages = {} + // util + this.sha3 = function () { return self.web3.sha3.apply(self.web3, arguments) } + this.toHex = function () { return self.web3.toHex.apply(self.web3, arguments) } + this.toAscii = function () { return self.web3.toAscii.apply(self.web3, arguments) } + this.fromAscii = function () { return self.web3.fromAscii.apply(self.web3, arguments) } + this.fromDecimal = function () { return self.web3.fromDecimal.apply(self.web3, arguments) } + this.fromWei = function () { return self.web3.fromWei.apply(self.web3, arguments) } + this.toWei = function () { return self.web3.toWei.apply(self.web3, arguments) } + this.toBigNumber = function () { return self.web3.toBigNumber.apply(self.web3, arguments) } + this.isAddress = function () { return self.web3.isAddress.apply(self.web3, arguments) } +} + +web3VmProvider.prototype.setVM = function (vm) { + if (this.vm === vm) return + var self = this + this.vm = vm + this.vm.on('step', function (data) { + self.pushTrace(self, data) + }) + this.vm.on('afterTx', function (data) { + self.txProcessed(self, data) + }) + this.vm.on('beforeTx', function (data) { + self.txWillProcess(self, data) + }) +} + +web3VmProvider.prototype.releaseCurrentHash = function () { + var ret = this.processingHash + this.processingHash = undefined + return ret +} + +web3VmProvider.prototype.txWillProcess = function (self, data) { + self.incr++ + self.processingHash = util.hexConvert(data.hash()) + self.vmTraces[self.processingHash] = { + gas: '0x0', + return: '0x0', + structLogs: [] + } + var tx = {} + tx.hash = self.processingHash + tx.from = util.hexConvert(data.getSenderAddress()) + if (data.to && data.to.length) { + tx.to = util.hexConvert(data.to) + } + this.processingAddress = tx.to + tx.data = util.hexConvert(data.data) + tx.input = util.hexConvert(data.input) + tx.gas = (new ethutil.BN(util.hexConvert(data.gas).replace('0x', ''), 16)).toString(10) + if (data.value) { + tx.value = util.hexConvert(data.value) + } + self.txs[self.processingHash] = tx + self.txsReceipt[self.processingHash] = tx + self.storageCache[self.processingHash] = {} + if (tx.to) { + self.vm.stateManager.dumpStorage(tx.to, function (storage) { + self.storageCache[self.processingHash][tx.to] = storage + }) + } + this.processingIndex = 0 +} + +web3VmProvider.prototype.txProcessed = function (self, data) { + var lastOp = self.vmTraces[self.processingHash].structLogs[self.processingIndex - 1] + if (lastOp) { + lastOp.error = lastOp.op !== 'RETURN' && lastOp.op !== 'STOP' && lastOp.op !== 'SELFDESTRUCT' + } + self.vmTraces[self.processingHash].gas = '0x' + data.gasUsed.toString(16) + + var logs = [] + for (var l in data.vm.logs) { + var log = data.vm.logs[l] + var topics = [] + if (log[1].length > 0) { + for (var k in log[1]) { + topics.push('0x' + log[1][k].toString('hex')) + } + } else { + topics.push('0x') + } + logs.push({ + address: '0x' + log[0].toString('hex'), + data: '0x' + log[2].toString('hex'), + topics: topics, + rawVMResponse: log + }) + } + self.txsReceipt[self.processingHash].logs = logs + self.txsReceipt[self.processingHash].transactionHash = self.processingHash + + if (data.createdAddress) { + var address = util.hexConvert(data.createdAddress) + self.vmTraces[self.processingHash].return = address + self.txsReceipt[self.processingHash].contractAddress = address + } else if (data.vm.return) { + self.vmTraces[self.processingHash].return = util.hexConvert(data.vm.return) + } else { + self.vmTraces[self.processingHash].return = '0x' + } + this.processingIndex = null + this.processingAddress = null + this.previousDepth = 0 +} + +web3VmProvider.prototype.pushTrace = function (self, data) { + var depth = data.depth + 1 // geth starts the depth from 1 + if (!self.processingHash) { + console.log('no tx processing') + return + } + var previousopcode + if (self.vmTraces[self.processingHash] && self.vmTraces[self.processingHash].structLogs[this.processingIndex - 1]) { + previousopcode = self.vmTraces[self.processingHash].structLogs[this.processingIndex - 1] + } + + if (this.previousDepth > depth && previousopcode) { + // returning from context, set error it is not STOP, RETURN + previousopcode.invalidDepthChange = previousopcode.op !== 'RETURN' && previousopcode.op !== 'STOP' + } + var step = { + stack: util.hexListFromBNs(data.stack), + memory: util.formatMemory(data.memory), + storage: data.storage, + op: data.opcode.name, + pc: data.pc, + gasCost: data.opcode.fee.toString(), + gas: data.gasLeft.toString(), + depth: depth, + error: data.error === false ? undefined : data.error + } + self.vmTraces[self.processingHash].structLogs.push(step) + if (traceHelper.newContextStorage(step)) { + if (step.op === 'CREATE') { + this.processingAddress = traceHelper.contractCreationToken(this.processingIndex) + this.storageCache[this.processingHash][this.processingAddress] = {} + } else { + this.processingAddress = uiutil.normalizeHexAddress(step.stack[step.stack.length - 2]) + if (!self.storageCache[self.processingHash][this.processingAddress]) { + self.vm.stateManager.dumpStorage(this.processingAddress, function (storage) { + self.storageCache[self.processingHash][self.processingAddress] = storage + }) + } + } + } + if (previousopcode && traceHelper.isSHA3Instruction(previousopcode)) { + var preimage = getSha3Input(previousopcode.stack, previousopcode.memory) + var imageHash = step.stack[step.stack.length - 1].replace('0x', '') + self.sha3Preimages[imageHash] = { + 'preimage': preimage + } + } + + this.processingIndex++ + this.previousDepth = depth +} + +web3VmProvider.prototype.getCode = function (address, cb) { + this.vm.stateManager.getContractCode(address, function (error, result) { + cb(error, util.hexConvert(result)) + }) +} + +web3VmProvider.prototype.setProvider = function (provider) {} + +web3VmProvider.prototype.traceTransaction = function (txHash, options, cb) { + if (this.vmTraces[txHash]) { + if (cb) { + cb(null, this.vmTraces[txHash]) + } + return this.vmTraces[txHash] + } else { + if (cb) { + cb('unable to retrieve traces ' + txHash, null) + } + } +} + +web3VmProvider.prototype.storageRangeAt = function (blockNumber, txIndex, address, start, maxLength, cb) { // txIndex is the hash in the case of the VM + // we don't use the range params here + if (this.storageCache[txIndex] && this.storageCache[txIndex][address]) { + var storage = this.storageCache[txIndex][address] + return cb(null, { + storage: JSON.parse(JSON.stringify(storage)), + nextKey: null + }) + } else { + cb('unable to retrieve storage ' + txIndex + ' ' + address) + } +} + +web3VmProvider.prototype.getBlockNumber = function (cb) { cb(null, 'vm provider') } + +web3VmProvider.prototype.getTransaction = function (txHash, cb) { + if (this.txs[txHash]) { + if (cb) { + cb(null, this.txs[txHash]) + } + return this.txs[txHash] + } else { + if (cb) { + cb('unable to retrieve tx ' + txHash, null) + } + } +} + +web3VmProvider.prototype.getTransactionReceipt = function (txHash, cb) { + // same as getTransaction but return the created address also + if (this.txsReceipt[txHash]) { + if (cb) { + cb(null, this.txsReceipt[txHash]) + } + return this.txsReceipt[txHash] + } else { + if (cb) { + cb('unable to retrieve txReceipt ' + txHash, null) + } + } +} + +web3VmProvider.prototype.getTransactionFromBlock = function (blockNumber, txIndex, cb) { + var mes = 'not supposed to be needed by remix in vmmode' + console.log(mes) + if (cb) { + cb(mes, null) + } +} + +web3VmProvider.prototype.preimage = function (hashedKey, cb) { + hashedKey = hashedKey.replace('0x', '') + cb(null, this.sha3Preimages[hashedKey] !== undefined ? this.sha3Preimages[hashedKey].preimage : null) +} + +function getSha3Input (stack, memory) { + var memoryStart = stack[stack.length - 1] + var memoryLength = stack[stack.length - 2] + var memStartDec = (new ethutil.BN(memoryStart.replace('0x', ''), 16)).toString(10) + memoryStart = parseInt(memStartDec) * 2 + var memLengthDec = (new ethutil.BN(memoryLength.replace('0x', ''), 16).toString(10)) + memoryLength = parseInt(memLengthDec) * 2 + + var i = Math.floor(memoryStart / 32) + var maxIndex = Math.floor(memoryLength / 32) + i + if (!memory[i]) { + return emptyFill(memoryLength) + } + var sha3Input = memory[i].slice(memoryStart - 32 * i) + i++ + while (i < maxIndex) { + sha3Input += memory[i] ? memory[i] : emptyFill(32) + i++ + } + if (sha3Input.length < memoryLength) { + var leftSize = memoryLength - sha3Input.length + sha3Input += memory[i] ? memory[i].slice(0, leftSize) : emptyFill(leftSize) + } + return sha3Input +} + +function emptyFill (size) { + return (new Array(size)).join('0') +} + +module.exports = web3VmProvider diff --git a/remix-lib/test/astwalker.js b/remix-lib/test/astwalker.js new file mode 100644 index 0000000000..1191001333 --- /dev/null +++ b/remix-lib/test/astwalker.js @@ -0,0 +1,61 @@ +'use strict' +var tape = require('tape') +var AstWalker = require('../src/astWalker') +var node = require('./resources/ast') + +tape('ASTWalker', function (t) { + t.test('ASTWalker.walk', function (st) { + st.plan(24) + var astwalker = new AstWalker() + + astwalker.walk(node.ast.legacyAST, function (node) { + if (node.name === 'ContractDefinition') { + checkContract(st, node) + } + if (node.name === 'FunctionDefinition') { + checkSetFunction(st, node) + } + return true + }) + + var callback = {} + callback.FunctionDefinition = function (node) { + st.equal(node.name, 'FunctionDefinition') + st.equal(node.attributes.name === 'set' || node.attributes.name === 'get', true) + return true + } + astwalker.walk(node.ast.legacyAST, callback) + }) +}) + +function checkContract (st, node) { + st.equal(node.attributes.name, 'test') + st.equal(node.children[0].attributes.name, 'x') + st.equal(node.children[0].attributes.type, 'int256') + st.equal(node.children[1].attributes.name, 'y') + st.equal(node.children[1].attributes.type, 'int256') + st.equal(node.children[2].name, 'FunctionDefinition') + st.equal(node.children[2].attributes.constant, false) + st.equal(node.children[2].attributes.name, 'set') + st.equal(node.children[2].attributes.public, true) +} + +function checkSetFunction (st, node) { + if (node.attributes.name === 'set') { + st.equal(node.children[0].name, 'ParameterList') + st.equal(node.children[1].name, 'ParameterList') + st.equal(node.children[2].name, 'Block') + st.equal(node.children[2].children[1].name, 'ExpressionStatement') + checkExpressionStatement(st, node.children[2].children[0]) + } +} + +function checkExpressionStatement (st, node) { + st.equal(node.children[0].name, 'Assignment') + st.equal(node.children[0].attributes.operator, '=') + st.equal(node.children[0].attributes.type, 'int256') + st.equal(node.children[0].children[0].name, 'Identifier') + st.equal(node.children[0].children[0].attributes.value, 'x') + st.equal(node.children[0].children[1].name, 'Identifier') + st.equal(node.children[0].children[1].attributes.value, '_x') +} diff --git a/remix-lib/test/eventManager.js b/remix-lib/test/eventManager.js new file mode 100644 index 0000000000..41ca659fe1 --- /dev/null +++ b/remix-lib/test/eventManager.js @@ -0,0 +1,35 @@ +'use strict' +var tape = require('tape') +var EventManager = require('../src/eventManager') +tape('eventManager', function (t) { + t.test('eventManager', function (st) { + var events = new EventManager() + var listenner = {} + + var trace = '' + listenner.listen = function (data1) { + trace += data1 + } + var registeredFunction = function (data) { + trace += data + } + events.register('event1', listenner, listenner.listen) + events.register('event2', registeredFunction) + events.trigger('event1', ['event1']) + events.trigger('event2', ['event2']) + st.equal(trace, 'event1event2') + + events.unregister('event1', listenner.listen) + st.equal(events.registered['event1'].length, 1) + st.equal(events.registered['event2'].length, 1) + + events.unregister('event1', listenner, listenner.listen) + st.equal(events.registered['event1'].length, 0) + st.equal(events.registered['event2'].length, 1) + + events.unregister('event2', registeredFunction) + st.equal(events.registered['event1'].length, 0) + st.equal(events.registered['event2'].length, 0) + st.end() + }) +}) diff --git a/remix-lib/test/resources/ast.js b/remix-lib/test/resources/ast.js new file mode 100644 index 0000000000..7fa15f2615 --- /dev/null +++ b/remix-lib/test/resources/ast.js @@ -0,0 +1,23 @@ +var node = {} + +node.ast = {"legacyAST":{"children":[{"attributes":{"fullyImplemented":true,"isLibrary":false,"linearizedBaseContracts":[5640396],"name":"test"},"children":[{"attributes":{"name":"x","type":"int256"},"children":[{"attributes":{"name":"int"},"id":5657860,"name":"ElementaryTypeName","src":"21:3:11"}],"id":5658100,"name":"VariableDeclaration","src":"21:5:11"},{"attributes":{"name":"y","type":"int256"},"children":[{"attributes":{"name":"int"},"id":5658180,"name":"ElementaryTypeName","src":"38:3:11"}],"id":5658268,"name":"VariableDeclaration","src":"38:5:11"},{"attributes":{"constant":false,"name":"set","public":true},"children":[{"children":[{"attributes":{"name":"_x","type":"int256"},"children":[{"attributes":{"name":"int"},"id":5658404,"name":"ElementaryTypeName","src":"68:3:11"}],"id":5658492,"name":"VariableDeclaration","src":"68:6:11"}],"id":5658572,"name":"ParameterList","src":"67:8:11"},{"children":[{"attributes":{"name":"_r","type":"int256"},"children":[{"attributes":{"name":"int"},"id":5658628,"name":"ElementaryTypeName","src":"85:3:11"}],"id":5658716,"name":"VariableDeclaration","src":"85:6:11"}],"id":5658796,"name":"ParameterList","src":"84:8:11"},{"children":[{"children":[{"attributes":{"operator":"=","type":"int256"},"children":[{"attributes":{"type":"int256","value":"x"},"id":5658900,"name":"Identifier","src":"108:1:11"},{"attributes":{"type":"int256","value":"_x"},"id":5658980,"name":"Identifier","src":"112:2:11"}],"id":5657492,"name":"Assignment","src":"108:6:11"}],"id":5659028,"name":"ExpressionStatement","src":"108:6:11"},{"children":[{"attributes":{"operator":"=","type":"int256"},"children":[{"attributes":{"type":"int256","value":"y"},"id":5659116,"name":"Identifier","src":"125:1:11"},{"attributes":{"string":null,"type":"int_const 10","value":"10"},"id":5659196,"name":"Literal","src":"129:2:11"}],"id":5659252,"name":"Assignment","src":"125:6:11"}],"id":5659316,"name":"ExpressionStatement","src":"125:6:11"},{"children":[{"attributes":{"operator":"=","type":"int256"},"children":[{"attributes":{"type":"int256","value":"_r"},"id":5659428,"name":"Identifier","src":"141:2:11"},{"attributes":{"type":"int256","value":"x"},"id":5639308,"name":"Identifier","src":"146:1:11"}],"id":5639356,"name":"Assignment","src":"141:6:11"}],"id":5639420,"name":"ExpressionStatement","src":"141:6:11"}],"id":5639516,"name":"Block","src":"97:57:11"}],"id":5639612,"name":"FunctionDefinition","src":"55:99:11"},{"attributes":{"constant":false,"name":"get","public":true},"children":[{"children":[],"id":5639764,"name":"ParameterList","src":"179:2:11"},{"children":[{"attributes":{"name":"x","type":"uint256"},"children":[{"attributes":{"name":"uint"},"id":5639820,"name":"ElementaryTypeName","src":"191:4:11"}],"id":5639908,"name":"VariableDeclaration","src":"191:6:11"},{"attributes":{"name":"y","type":"uint256"},"children":[{"attributes":{"name":"uint"},"id":5639988,"name":"ElementaryTypeName","src":"199:4:11"}],"id":5640076,"name":"VariableDeclaration","src":"199:6:11"}],"id":5640156,"name":"ParameterList","src":"190:16:11"},{"children":[],"id":5640212,"name":"Block","src":"212:17:11"}],"id":5640276,"name":"FunctionDefinition","src":"167:62:11"}],"id":5640396,"name":"ContractDefinition","src":"0:231:11"}],"name":"SourceUnit"}} + +node.source = `contract test { + int x; + + int y; + + function set(int _x) returns (int _r) + { + x = _x; + y = 10; + _r = x; + } + + function get() returns (uint x, uint y) + { + + } +}` + +module.exports = node \ No newline at end of file diff --git a/remix-lib/test/resources/sourceMapping.js b/remix-lib/test/resources/sourceMapping.js new file mode 100644 index 0000000000..f2f64a830e --- /dev/null +++ b/remix-lib/test/resources/sourceMapping.js @@ -0,0 +1,22 @@ +var sourceRuntimeMapping = {} +sourceRuntimeMapping.mapping = '0:205:4:-;;;;;;;;;;;;;;;;;;;;;;55:74;;;;;;;;;;;;;;;;;;;;;;;;;;142:61;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;55:74;103:2;99:1;;:6;;;;;120:2;116:1;;:6;;;;;55:74;;;:::o;142:61::-;166:6;174;142:61;;;:::o' +sourceRuntimeMapping.source = `contract test { + int x; + + int y; + + function set(int _x, int _y) + { + x = _x; + y = _y; + } + + function get() returns (uint x, uint y) + { + + } +}` + +if (typeof (module) !== 'undefined' && typeof (module.exports) !== 'undefined') { + module.exports = sourceRuntimeMapping +} diff --git a/remix-lib/test/sourceMappingDecoder.js b/remix-lib/test/sourceMappingDecoder.js new file mode 100644 index 0000000000..1a6513e384 --- /dev/null +++ b/remix-lib/test/sourceMappingDecoder.js @@ -0,0 +1,119 @@ +'use strict' +var tape = require('tape') +var sourceMapping = require('./resources/sourceMapping') +var SourceMappingDecoder = require('../src/sourceMappingDecoder') +var compiler = require('solc') +var compilerInput = require('../src/helpers/compilerHelper').compilerInput + +tape('SourceMappingDecoder', function (t) { + t.test('SourceMappingDecoder.findNodeAtInstructionIndex', function (st) { + var output = compiler.compileStandardWrapper(compilerInput(contracts)) + output = JSON.parse(output) + var sourceMappingDecoder = new SourceMappingDecoder() + var node = sourceMappingDecoder.findNodeAtInstructionIndex('FunctionDefinition', 2, output.contracts['test.sol']['test'].evm.deployedBytecode.sourceMap, output.sources['test.sol']) + st.equal(node, null) + node = sourceMappingDecoder.findNodeAtInstructionIndex('FunctionDefinition', 80, output.contracts['test.sol']['test'].evm.deployedBytecode.sourceMap, output.sources['test.sol']) + st.notEqual(node, null) + if (node) { + st.equal(node.attributes.name, 'f1') + } + st.end() + }) + + var testSourceMapping = {} + t.test('sourceMappingDecoder', function (st) { + st.plan(28) + var sourceMappingDecoder = new SourceMappingDecoder() + console.log('test decompressAll') + var result = sourceMappingDecoder.decompressAll(sourceMapping.mapping) + st.equal(result[0].start, 0) + st.equal(result[0].length, 205) + st.equal(result[0].file, 4) + st.equal(result[0].jump, '-') + + st.equal(result[21].start, 0) + st.equal(result[21].length, 205) + st.equal(result[21].file, 4) + st.equal(result[21].jump, '-') + testSourceMapping[21] = result[21] + + st.equal(result[22].start, 55) + st.equal(result[22].length, 74) + st.equal(result[22].file, 4) + st.equal(result[22].jump, '-') + + var last = result.length - 1 + st.equal(result[last].start, 142) + st.equal(result[last].length, 61) + st.equal(result[last].file, 4) + st.equal(result[last].jump, 'o') + testSourceMapping['last'] = result[last] + + console.log('test decompress') + result = sourceMappingDecoder.atIndex(22, sourceMapping.mapping) + console.log(result) + st.equal(result.start, 55) + st.equal(result.length, 74) + st.equal(result.file, 4) + st.equal(result.jump, '-') + testSourceMapping[22] = result + + result = sourceMappingDecoder.atIndex(82, sourceMapping.mapping) + console.log(result) + st.equal(result.start, 103) + st.equal(result.length, 2) + st.equal(result.file, 4) + st.equal(result.jump, '-') + testSourceMapping[82] = result + + result = sourceMappingDecoder.atIndex(85, sourceMapping.mapping) + console.log(result) + st.equal(result.start, 99) + st.equal(result.length, 6) + st.equal(result.file, 4) + st.equal(result.jump, '-') + testSourceMapping[85] = result + }) + + t.test('sourceMappingLineColumnConverter', function (st) { + st.plan(14) + var sourceMappingDecoder = new SourceMappingDecoder() + var linesbreak = sourceMappingDecoder.getLinebreakPositions(sourceMapping.source) + st.equal(linesbreak[0], 16) + st.equal(linesbreak[5], 84) + var result = sourceMappingDecoder.convertOffsetToLineColumn(testSourceMapping[21], linesbreak) + st.equal(result.start.line, 0) + st.equal(result.start.column, 0) + st.equal(result.end.line, 15) + st.equal(result.end.column, 1) + result = sourceMappingDecoder.convertOffsetToLineColumn(testSourceMapping[82], linesbreak) + st.equal(result.start.line, 7) + st.equal(result.start.column, 12) + st.equal(result.end.line, 7) + st.equal(result.end.column, 14) + + var res = { // point to \n + start: 103, + length: 4, + file: 4, + jump: '-' + } + result = sourceMappingDecoder.convertOffsetToLineColumn(res, linesbreak) + st.equal(result.start.line, 7) + st.equal(result.start.column, 12) + st.equal(result.end.line, 7) + st.equal(result.end.column, 16) + }) +}) + +var contracts = `contract test { + function f1() returns (uint) { + uint t = 4; + return t; + } + + function f2() { + + } +} +` diff --git a/remix-lib/test/tests.js b/remix-lib/test/tests.js new file mode 100644 index 0000000000..b0d0b69044 --- /dev/null +++ b/remix-lib/test/tests.js @@ -0,0 +1,5 @@ +require('./astwalker.js') +require('./eventManager.js') +require('./sourceMappingDecoder.js') +require('./util.js') +require('./txFormat.js') diff --git a/remix-lib/test/txFormat.js b/remix-lib/test/txFormat.js new file mode 100644 index 0000000000..0ad6428a7d --- /dev/null +++ b/remix-lib/test/txFormat.js @@ -0,0 +1,253 @@ +'use strict' +var tape = require('tape') +var txFormat = require('../src/execution/txFormat') +var txHelper = require('../src/execution/txHelper') +var util = require('../src/util') +var compiler = require('solc') +var compilerInput = require('../src/helpers/compilerHelper').compilerInput +var executionContext = require('../src/execution/execution-context') + +/* tape *********************************************************** */ + +var context +tape('ContractParameters - (TxFormat.buildData) - format input parameters', function (t) { + var output = compiler.compileStandardWrapper(compilerInput(uintContract)) + output = JSON.parse(output) + var contract = output.contracts['test.sol']['uintContractTest'] + context = { output, contract } + var bytecode = '608060405234801561001057600080fd5b50610111806100206000396000f300608060405260043610603f576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680634b521953146044575b600080fd5b348015604f57600080fd5b50609660048036038101908080359060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803590602001909291905050506098565b005b8260008190555081600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055505050505600a165627a7a723058209f014353c529016497846216787d1cdca7fe9116c4c1acc2218a45aae9fe6e520029' + t.test('(TxFormat.buildData)', function (st) { + st.plan(3) + testWithInput(st, '123123, "0xf7a10e525d4b168f45f74db1b61f63d3e7619ea8", "34"', bytecode + '000000000000000000000000000000000000000000000000000000000001e0f3000000000000000000000000f7a10e525d4b168f45f74db1b61f63d3e7619ea80000000000000000000000000000000000000000000000000000000000000022') + testWithInput(st, '"123123" , 0xf7a10e525d4b168f45f74db1b61f63d3e7619ea8, 654 ', bytecode + '000000000000000000000000000000000000000000000000000000000001e0f3000000000000000000000000f7a10e525d4b168f45f74db1b61f63d3e7619ea8000000000000000000000000000000000000000000000000000000000000028e') + // parsing this as javascript number should overflow + testWithInput(st, '90071992547409910000, 0xf7a10e525d4b168f45f74db1b61f63d3e7619ea8, 0', bytecode + '000000000000000000000000000000000000000000000004e1ffffffffffd8f0000000000000000000000000f7a10e525d4b168f45f74db1b61f63d3e7619ea80000000000000000000000000000000000000000000000000000000000000000') + }) +}) + +function testWithInput (st, params, expected) { + txFormat.buildData('uintContractTest', context.contract, context.output.contracts, true, context.contract.abi[0], params, (error, data) => { + if (error) { return st.fails(error) } + console.log(data) + st.equal(data.dataHex, expected) + }, () => {}, () => {}) +} + +/* tape *********************************************************** */ + +tape('ContractParameters - (TxFormat.buildData) - link Libraries', function (t) { + executionContext.setContext('vm') + var compileData = compiler.compileStandardWrapper(compilerInput(deploySimpleLib)) + + var fakeDeployedContracts = { + lib1: '0xf7a10e525d4b168f45f74db1b61f63d3e7619e11', + lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2: '0xf7a10e525d4b168f45f74db1b61f63d3e7619e33', + testContractLinkLibrary: '0xf7a10e525d4b168f45f74db1b61f63d3e7619e22' + } + var callbackDeployLibraries = (param, callback) => { + callback(null, { + result: { + createdAddress: fakeDeployedContracts[param.data.contractName] + } + }) + } // fake + + t.test('(TxFormat.buildData and link library (standard way))', function (st) { + st.plan(6) + var output = JSON.parse(compileData) + var contract = output.contracts['test.sol']['testContractLinkLibrary'] + context = { output, contract } + testLinkLibrary(st, fakeDeployedContracts, callbackDeployLibraries) + }) + + t.test('(TxFormat.encodeConstructorCallAndLinkLibraries and link library (standard way))', function (st) { + st.plan(12) + var output = JSON.parse(compileData) + var contract = output.contracts['test.sol']['testContractLinkLibrary'] + context = { output, contract } + testLinkLibrary2(st, callbackDeployLibraries) + }) +}) + +function testLinkLibrary (st, fakeDeployedContracts, callbackDeployLibraries) { + var deployMsg = ['creation of library test.sol:lib1 pending...', + 'creation of library test.sol:lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2 pending...'] + txFormat.buildData('testContractLinkLibrary', context.contract, context.output.contracts, true, context.contract.abi[0], '', (error, data) => { + if (error) { return st.fails(error) } + console.log(data) + var linkedbyteCode = data.dataHex + var libReference = context.contract.evm.bytecode.linkReferences['test.sol']['lib1'] + st.equal(linkedbyteCode.substr(2 * libReference[0].start, 40), fakeDeployedContracts['lib1'].replace('0x', '')) + st.equal(linkedbyteCode.substr(2 * libReference[1].start, 40), fakeDeployedContracts['lib1'].replace('0x', '')) + + libReference = context.contract.evm.bytecode.linkReferences['test.sol']['lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2'] + st.equal(linkedbyteCode.substr(2 * libReference[0].start, 40), fakeDeployedContracts['lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2'].replace('0x', '')) + st.equal(linkedbyteCode.substr(2 * libReference[1].start, 40), fakeDeployedContracts['lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2'].replace('0x', '')) + }, (msg) => { + st.equal(msg, deployMsg[0]) + deployMsg.shift() + }, callbackDeployLibraries) +} + +function testLinkLibrary2 (st, callbackDeployLibraries) { + var librariesReference = { + 'test.sol': { + 'lib1': '0xf7a10e525d4b168f45f74db1b61f63d3e7619e11', + 'lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2': '0xf7a10e525d4b168f45f74db1b61f63d3e7619e33' + } + } + var data = '608060405234801561001057600080fd5b5061026b806100206000396000f300608060405260043610610041576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680636d4ce63c14610046575b600080fd5b34801561005257600080fd5b5061005b61005d565b005b73f7a10e525d4b168f45f74db1b61f63d3e7619e116344733ae16040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160006040518083038186803b1580156100bd57600080fd5b505af41580156100d1573d6000803e3d6000fd5b5050505073f7a10e525d4b168f45f74db1b61f63d3e7619e336344733ae16040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160006040518083038186803b15801561013557600080fd5b505af4158015610149573d6000803e3d6000fd5b5050505073f7a10e525d4b168f45f74db1b61f63d3e7619e336344733ae16040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160006040518083038186803b1580156101ad57600080fd5b505af41580156101c1573d6000803e3d6000fd5b5050505073f7a10e525d4b168f45f74db1b61f63d3e7619e116344733ae16040518163ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040160006040518083038186803b15801561022557600080fd5b505af4158015610239573d6000803e3d6000fd5b505050505600a165627a7a72305820006fbc873d7822d8c26cfbdfeb92ee52e618dd5d048cfb6c6f7a313ec53dd16d0029' + + var deployMsg = ['creation of library test.sol:lib1 pending...', + 'creation of library test.sol:lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2 pending...'] + txFormat.encodeConstructorCallAndLinkLibraries(context.contract, '', context.contract.abi[0], librariesReference, context.contract.evm.bytecode.linkReferences, (error, result) => { + console.log(error, result) + st.equal(data, result.dataHex) + var linkedbyteCode = result.dataHex + var libReference = context.contract.evm.bytecode.linkReferences['test.sol']['lib1'] + st.equal(linkedbyteCode.substr(2 * libReference[0].start, 40), librariesReference['test.sol']['lib1'].replace('0x', '')) + st.equal(linkedbyteCode.substr(2 * libReference[1].start, 40), librariesReference['test.sol']['lib1'].replace('0x', '')) + + libReference = context.contract.evm.bytecode.linkReferences['test.sol']['lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2'] + st.equal(linkedbyteCode.substr(2 * libReference[0].start, 40), librariesReference['test.sol']['lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2'].replace('0x', '')) + st.equal(linkedbyteCode.substr(2 * libReference[1].start, 40), librariesReference['test.sol']['lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2'].replace('0x', '')) + }) + + txFormat.encodeConstructorCallAndDeployLibraries('testContractLinkLibrary', context.contract, context.output.contracts, '', context.contract.abi[0], (error, result) => { + console.log(error, result) + st.equal(data, result.dataHex) + var linkedbyteCode = result.dataHex + var libReference = context.contract.evm.bytecode.linkReferences['test.sol']['lib1'] + st.equal(linkedbyteCode.substr(2 * libReference[0].start, 40), librariesReference['test.sol']['lib1'].replace('0x', '')) + st.equal(linkedbyteCode.substr(2 * libReference[1].start, 40), librariesReference['test.sol']['lib1'].replace('0x', '')) + + libReference = context.contract.evm.bytecode.linkReferences['test.sol']['lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2'] + st.equal(linkedbyteCode.substr(2 * libReference[0].start, 40), librariesReference['test.sol']['lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2'].replace('0x', '')) + st.equal(linkedbyteCode.substr(2 * libReference[1].start, 40), librariesReference['test.sol']['lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2'].replace('0x', '')) + }, (msg) => { + st.equal(msg, deployMsg[0]) + deployMsg.shift() + }, callbackDeployLibraries) +} + +/* tape *********************************************************** */ + +tape('EncodeParameter', function (t) { + t.test('(TxFormat.encodeFunctionCall)', function (st) { + st.plan(1) + encodeFunctionCallTest(st) + }) +}) + +function encodeFunctionCallTest (st) { + var output = compiler.compileStandardWrapper(compilerInput(encodeFunctionCall)) + output = JSON.parse(output) + var contract = output.contracts['test.sol']['testContractLinkLibrary'] + txFormat.encodeFunctionCall('123, "test string"', contract.abi[0], (error, encoded) => { + console.log(error) + st.equal(encoded.dataHex, '0x805da4ad000000000000000000000000000000000000000000000000000000000000007b0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000b7465737420737472696e67000000000000000000000000000000000000000000') + }) +} + +/* *********************************************************** */ + +tape('test fallback function', function (t) { + t.test('(fallback)', function (st) { + st.plan(2) + var output = compiler.compileStandardWrapper(compilerInput(fallbackFunction)) + output = JSON.parse(output) + var contract = output.contracts['test.sol']['fallbackFunctionContract'] + st.equal(txHelper.encodeFunctionId(contract.abi[0]), '0x805da4ad') + st.equal(txHelper.encodeFunctionId(contract.abi[1]), '0x') + }) +}) + +tape('test abiEncoderV2', function (t) { + var functionId = '0x56d89238' + var encodedData = '0x000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000170000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000042ed123b0bd8203c2700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000090746573745f737472696e675f746573745f737472696e675f746573745f737472696e675f746573745f737472696e675f746573745f737472696e675f746573745f737472696e675f746573745f737472696e675f746573745f737472696e675f746573745f737472696e675f746573745f737472696e675f746573745f737472696e675f746573745f737472696e675f00000000000000000000000000000000' + var value1 = '1' + var value2 = '1234567890123456789543' + var value3 = 'test_string_test_string_test_string_test_string_test_string_test_string_test_string_test_string_test_string_test_string_test_string_test_string_' + var decodedData = `[${value1}, ${value2}, "${value3}"], 23` + t.test('(abiEncoderV2)', function (st) { + st.plan(2) + var output = compiler.compileStandardWrapper(compilerInput(abiEncoderV2)) + output = JSON.parse(output) + var contract = output.contracts['test.sol']['test'] + txFormat.encodeFunctionCall(decodedData, contract.abi[0], (error, encoded) => { + console.log(error) + st.equal(encoded.dataHex, functionId + encodedData.replace('0x', '')) + }) + var decoded = txFormat.decodeResponse(util.hexToIntArray(encodedData), contract.abi[0]) + console.log(decoded) + st.equal(decoded[0], `tuple(uint256,uint256,string): ${value1},${value2},${value3}`) + }) +}) + +var uintContract = `contract uintContractTest { + uint _tp; + address _ap; + function test(uint _t, address _a, uint _i) { + _tp = _t; + _ap = _a; + } +}` + +var deploySimpleLib = `pragma solidity ^0.4.4; + +library lib1 { + function getEmpty () { + } +} + +library lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2 { + function getEmpty () { + } +} + +contract testContractLinkLibrary { + function get () { + lib1.getEmpty(); + lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2.getEmpty(); + lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2_lib2.getEmpty(); + lib1.getEmpty(); + } + }` + +var encodeFunctionCall = `pragma solidity ^0.4.4; + +contract testContractLinkLibrary { + function get (uint _p, string _o) { + + } + }` + +var fallbackFunction = `pragma solidity ^0.4.4; + +contract fallbackFunctionContract { + function get (uint _p, string _o) { + + } + + function () {} + }` + +var abiEncoderV2 = `pragma experimental ABIEncoderV2; + +contract test { + struct p { + uint a; + uint b; + string s; + } + function t (p _p, uint _i) returns (p) { + return _p; + } + + function t () returns (p) { + p mm; + mm.a = 123; + mm.b = 133; + return mm; + } +}` diff --git a/remix-lib/test/util.js b/remix-lib/test/util.js new file mode 100644 index 0000000000..7ca502e3f8 --- /dev/null +++ b/remix-lib/test/util.js @@ -0,0 +1,79 @@ +'use strict' +var tape = require('tape') +var util = require('../src/util') + +tape('Util', function (t) { + t.test('lowerbound', function (st) { + st.plan(7) + var array = [2, 5, 8, 9, 45, 56, 78] + var lowerBound = util.findLowerBound(10, array) + st.equal(lowerBound, 3) + + lowerBound = util.findLowerBound(3, array) + st.equal(lowerBound, 0) + + lowerBound = util.findLowerBound(100, array) + st.equal(lowerBound, 6) + + lowerBound = util.findLowerBound(1, array) + st.equal(lowerBound, -1) + + lowerBound = util.findLowerBound(45, array) + st.equal(lowerBound, 4) + + array = [2, 5, 8, 9, 9, 45, 56, 78] + lowerBound = util.findLowerBound(9, array) + st.equal(lowerBound, 4) + + lowerBound = util.findLowerBound(9, []) + st.equal(lowerBound, -1) + }) +}) + +tape('util.groupBy on valid input', function (t) { + t.plan(1) + + var result = util.groupBy([ + {category: 'GAS', name: 'a'}, + {category: 'SEC', name: 'b'}, + {category: 'GAS', name: 'c'} + + ], 'category') + + var expectedResult = { + 'GAS': [ + {category: 'GAS', name: 'a'}, + {category: 'GAS', name: 'c'} + ], + 'SEC': [ + {category: 'SEC', name: 'b'} + ] + } + + t.deepEqual(result, expectedResult) +}) + +tape('util.concatWithSeperator valid output', function (t) { + t.plan(4) + t.notEqual(util.concatWithSeperator(['a', 'b', 'c'], ','), 'a, b, c', 'Concat with comma should not produce spaces') + t.equal(util.concatWithSeperator(['a', 'b', 'c'], ','), 'a,b,c', 'Concat with comma should not produce spaces') + t.equal(util.concatWithSeperator(['a', 'b', 'c'], ', '), 'a, b, c', 'Concat with comma space should not produce trailing comma') + t.equal(util.concatWithSeperator(['a', 'b', 'c'], '+'), 'a+b+c', 'Concat with plus') +}) + +tape('util.escapeRegExp', function (t) { + t.plan(3) + var original = 'function (uint256) returns (bool)' + t.equal(util.escapeRegExp('abcd'), 'abcd', 'String with no regex') + t.equal(util.escapeRegExp(original), 'function \\(uint256\\) returns \\(bool\\)', 'function string with regex') + t.ok(new RegExp(util.escapeRegExp(original)).test(original), 'should still test for original string') +}) + +tape('util.compareByteCode', function (t) { + t.plan(1) + var address = 'c2a9cef5420203c2672f0e4325cca774893cca98' + var nullAddress = '0000000000000000000000000000000000000000' + var deployedLibraryByteCode = '0x73c2a9cef5420203c2672f0e4325cca774893cca983014608060405260043610610058576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063f26ea02c1461005d575b600080fd5b81801561006957600080fd5b506100886004803603810190808035906020019092919050505061008a565b005b600081600101600060648110151561009e57fe5b600502016002018190555060008160010160006064811015156100bd57fe5b600502016004018190555060008160010160006064811015156100dc57fe5b6005020160030181905550600081600001819055506001816101f501819055816101f601819055506064816101f70181905550505600a165627a7a723058203a6f106db7413fd9cad962bc12ba2327799d6b1334335f7bb854eab04200b3bf0029' + t.ok(util.compareByteCode(deployedLibraryByteCode, deployedLibraryByteCode.replace(address, nullAddress)), 'library bytecode should be the same') +}) + diff --git a/remix-solidity/README.md b/remix-solidity/README.md new file mode 100644 index 0000000000..b0c874596e --- /dev/null +++ b/remix-solidity/README.md @@ -0,0 +1,11 @@ +# `remix-solidity` + +Provides: + + { + InternalCallTree: InternalCallTree, + SolidityProxy: SolidityProxy, + localDecoder: localDecoder, + stateDecoder: stateDecoder, + CodeAnalysis: CodeAnalysis + } diff --git a/remix-solidity/index.js b/remix-solidity/index.js new file mode 100644 index 0000000000..c64fc96932 --- /dev/null +++ b/remix-solidity/index.js @@ -0,0 +1,7 @@ +var Compiler = require('./src/compiler/compiler') +var CompilerInput = require('./src/compiler/compiler-input') + +module.exports = { + Compiler: Compiler, + CompilerInput: CompilerInput +} diff --git a/remix-solidity/package.json b/remix-solidity/package.json new file mode 100644 index 0000000000..76489192db --- /dev/null +++ b/remix-solidity/package.json @@ -0,0 +1,85 @@ +{ + "name": "remix-solidity", + "version": "0.1.11", + "description": "Ethereum IDE and tools for the web", + "contributors": [ + { + "name": "Yann Levreau", + "email": "yann@ethdev.com" + }, + { + "name": "Liana Husikyan", + "email": "liana@ethdev.com" + } + ], + "main": "./index.js", + "dependencies": { + "babel-eslint": "^7.1.1", + "babel-plugin-transform-object-assign": "^6.22.0", + "babel-preset-es2015": "^6.24.0", + "babelify": "^7.3.0", + "ethereumjs-util": "^4.5.0", + "ethereumjs-vm": "^2.3.3", + "fast-async": "^6.1.2", + "npm-run-all": "^4.0.2", + "remix-lib": "^0.2.9", + "solc": "https://github.com/ethereum/solc-js", + "standard": "^7.0.1", + "tape": "^4.6.0", + "webworkify": "^1.2.1" + }, + "scripts": { + "downloadsolc": "cd node_modules/solc && (test -e soljson.js || wget --no-check-certificate https://solc-bin.ethereum.org/soljson.js) && cd ..", + "prepublish": "mkdirp build; npm-run-all -ls downloadsolc", + "postinstall": "npm-run-all -ls downloadsolc" + }, + "standard": { + "ignore": [ + "node_modules/*", + "soljson.js" + ], + "parser": "babel-eslint" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ethereum/remix.git" + }, + "author": "cpp-ethereum team", + "license": "MIT", + "bugs": { + "url": "https://github.com/ethereum/remix/issues" + }, + "homepage": "https://github.com/ethereum/remix#readme", + "browserify": { + "transform": [ + [ + "babelify", + { + "plugins": [ + [ + "fast-async", + { + "runtimePatten": null, + "compiler": { + "promises": true, + "es7": true, + "noRuntime": true, + "wrapAwait": true + } + } + ], + "transform-object-assign" + ] + } + ], + [ + "babelify", + { + "presets": [ + "es2015" + ] + } + ] + ] + } +} diff --git a/remix-solidity/src/compiler/compiler-input.js b/remix-solidity/src/compiler/compiler-input.js new file mode 100644 index 0000000000..c5de89f735 --- /dev/null +++ b/remix-solidity/src/compiler/compiler-input.js @@ -0,0 +1,21 @@ +'use strict' + +module.exports = (sources, opts) => { + return JSON.stringify({ + language: 'Solidity', + sources: sources, + settings: { + optimizer: { + enabled: opts.optimize === true || opts.optimize === 1, + runs: 200 + }, + libraries: opts.libraries, + outputSelection: { + '*': { + '': [ 'legacyAST' ], + '*': [ 'abi', 'metadata', 'devdoc', 'userdoc', 'evm.legacyAssembly', 'evm.bytecode', 'evm.deployedBytecode', 'evm.methodIdentifiers', 'evm.gasEstimates' ] + } + } + } + }) +} diff --git a/remix-solidity/src/compiler/compiler-worker.js b/remix-solidity/src/compiler/compiler-worker.js new file mode 100644 index 0000000000..14e630aeaf --- /dev/null +++ b/remix-solidity/src/compiler/compiler-worker.js @@ -0,0 +1,45 @@ +'use strict' + +var solc = require('solc/wrapper') + +var compileJSON = function () { return '' } +var missingInputs = [] + +module.exports = function (self) { + self.addEventListener('message', function (e) { + var data = e.data + switch (data.cmd) { + case 'loadVersion': + delete self.Module + // NOTE: workaround some browsers? + self.Module = undefined + + compileJSON = null + + self.importScripts(data.data) + + var compiler = solc(self.Module) + + compileJSON = function (input) { + try { + return compiler.compileStandardWrapper(input, function (path) { + missingInputs.push(path) + return { 'error': 'Deferred import' } + }) + } catch (exception) { + return JSON.stringify({ error: 'Uncaught JavaScript exception:\n' + exception }) + } + } + + self.postMessage({ + cmd: 'versionLoaded', + data: compiler.version() + }) + break + case 'compile': + missingInputs.length = 0 + self.postMessage({cmd: 'compiled', job: data.job, data: compileJSON(data.input), missingInputs: missingInputs}) + break + } + }, false) +} diff --git a/remix-solidity/src/compiler/compiler.js b/remix-solidity/src/compiler/compiler.js new file mode 100644 index 0000000000..4238cb6a0f --- /dev/null +++ b/remix-solidity/src/compiler/compiler.js @@ -0,0 +1,368 @@ +'use strict' + +var solc = require('solc/wrapper') +var solcABI = require('solc/abi') + +var webworkify = require('webworkify') + +var compilerInput = require('./compiler-input') + +var remixLib = require('remix-lib') +var EventManager = remixLib.EventManager + +var txHelper = require('./txHelper') + +/* + trigger compilationFinished, compilerLoaded, compilationStarted, compilationDuration +*/ +function Compiler (handleImportCall) { + var self = this + this.event = new EventManager() + + var compileJSON + + var worker = null + + var currentVersion + + var optimize = false + + this.setOptimize = function (_optimize) { + optimize = _optimize + } + + var compilationStartTime = null + this.event.register('compilationFinished', (success, data, source) => { + if (success && compilationStartTime) { + this.event.trigger('compilationDuration', [(new Date().getTime()) - compilationStartTime]) + } + compilationStartTime = null + }) + + this.event.register('compilationStarted', () => { + compilationStartTime = new Date().getTime() + }) + + var internalCompile = function (files, target, missingInputs) { + gatherImports(files, target, missingInputs, function (error, input) { + if (error) { + self.lastCompilationResult = null + self.event.trigger('compilationFinished', [false, {'error': { formattedMessage: error, severity: 'error' }}, files]) + } else { + compileJSON(input, optimize ? 1 : 0) + } + }) + } + + var compile = function (files, target) { + self.event.trigger('compilationStarted', []) + internalCompile(files, target) + } + this.compile = compile + + function setCompileJSON (_compileJSON) { + compileJSON = _compileJSON + } + this.setCompileJSON = setCompileJSON // this is exposed for testing + + function onCompilerLoaded (version) { + currentVersion = version + self.event.trigger('compilerLoaded', [version]) + } + + function onInternalCompilerLoaded () { + if (worker === null) { + var compiler + if (typeof (window) === 'undefined') { + compiler = require('solc') + } else { + compiler = solc(window.Module) + } + + compileJSON = function (source, optimize, cb) { + var missingInputs = [] + var missingInputsCallback = function (path) { + missingInputs.push(path) + return { error: 'Deferred import' } + } + + var result + try { + var input = compilerInput(source.sources, {optimize: optimize, target: source.target}) + result = compiler.compileStandardWrapper(input, missingInputsCallback) + result = JSON.parse(result) + } catch (exception) { + result = { error: 'Uncaught JavaScript exception:\n' + exception } + } + + compilationFinished(result, missingInputs, source) + } + onCompilerLoaded(compiler.version()) + } + } + // exposed for use in node + this.onInternalCompilerLoaded = onInternalCompilerLoaded + + this.lastCompilationResult = { + data: null, + source: null + } + + /** + * return the contract obj of the given @arg name. Uses last compilation result. + * return null if not found + * @param {String} name - contract name + * @returns contract obj and associated file: { contract, file } or null + */ + this.getContract = (name) => { + if (this.lastCompilationResult.data && this.lastCompilationResult.data.contracts) { + return txHelper.getContract(name, this.lastCompilationResult.data.contracts) + } + return null + } + + /** + * call the given @arg cb (function) for all the contracts. Uses last compilation result + * @param {Function} cb - callback + */ + this.visitContracts = (cb) => { + if (this.lastCompilationResult.data && this.lastCompilationResult.data.contracts) { + return txHelper.visitContracts(this.lastCompilationResult.data.contracts, cb) + } + return null + } + + /** + * return the compiled contracts from the last compilation result + * @return {Object} - contracts + */ + this.getContracts = () => { + if (this.lastCompilationResult.data && this.lastCompilationResult.data.contracts) { + return this.lastCompilationResult.data.contracts + } + return null + } + + /** + * return the sources from the last compilation result + * @param {Object} cb - map of sources + */ + this.getSources = () => { + if (this.lastCompilationResult.source) { + return this.lastCompilationResult.source.sources + } + return null + } + + /** + * return the sources @arg fileName from the last compilation result + * @param {Object} cb - map of sources + */ + this.getSource = (fileName) => { + if (this.lastCompilationResult.source) { + return this.lastCompilationResult.source.sources[fileName] + } + return null + } + + /** + * return the source from the last compilation result that has the given index. null if source not found + * @param {Int} index - index of the source + */ + this.getSourceName = (index) => { + if (this.lastCompilationResult.data && this.lastCompilationResult.data.sources) { + return Object.keys(this.lastCompilationResult.data.sources)[index] + } + return null + } + + function compilationFinished (data, missingInputs, source) { + var noFatalErrors = true // ie warnings are ok + + function isValidError (error) { + // The deferred import is not a real error + // FIXME: maybe have a better check? + if (/Deferred import/.exec(error.message)) { + return false + } + + return error.severity !== 'warning' + } + + if (data['error'] !== undefined) { + // Ignore warnings (and the 'Deferred import' error as those are generated by us as a workaround + if (isValidError(data['error'])) { + noFatalErrors = false + } + } + if (data['errors'] !== undefined) { + data['errors'].forEach(function (err) { + // Ignore warnings and the 'Deferred import' error as those are generated by us as a workaround + if (isValidError(err)) { + noFatalErrors = false + } + }) + } + + if (!noFatalErrors) { + // There are fatal errors - abort here + self.lastCompilationResult = null + self.event.trigger('compilationFinished', [false, data, source]) + } else if (missingInputs !== undefined && missingInputs.length > 0) { + // try compiling again with the new set of inputs + internalCompile(source.sources, source.target, missingInputs) + } else { + data = updateInterface(data) + + self.lastCompilationResult = { + data: data, + source: source + } + self.event.trigger('compilationFinished', [true, data, source]) + } + } + + // TODO: needs to be changed to be more node friendly + this.loadVersion = function (usingWorker, url) { + console.log('Loading ' + url + ' ' + (usingWorker ? 'with worker' : 'without worker')) + self.event.trigger('loadingCompiler', [url, usingWorker]) + + if (usingWorker) { + loadWorker(url) + } else { + loadInternal(url) + } + } + + function loadInternal (url) { + delete window.Module + // NOTE: workaround some browsers? + window.Module = undefined + + // Set a safe fallback until the new one is loaded + setCompileJSON(function (source, optimize) { + compilationFinished({ error: { formattedMessage: 'Compiler not yet loaded.' } }) + }) + + var newScript = document.createElement('script') + newScript.type = 'text/javascript' + newScript.src = url + document.getElementsByTagName('head')[0].appendChild(newScript) + var check = window.setInterval(function () { + if (!window.Module) { + return + } + window.clearInterval(check) + onInternalCompilerLoaded() + }, 200) + } + + function loadWorker (url) { + if (worker !== null) { + worker.terminate() + } + worker = webworkify(require('./compiler-worker.js')) + var jobs = [] + worker.addEventListener('message', function (msg) { + var data = msg.data + switch (data.cmd) { + case 'versionLoaded': + onCompilerLoaded(data.data) + break + case 'compiled': + var result + try { + result = JSON.parse(data.data) + } catch (exception) { + result = { 'error': 'Invalid JSON output from the compiler: ' + exception } + } + var sources = {} + if (data.job in jobs !== undefined) { + sources = jobs[data.job].sources + delete jobs[data.job] + } + compilationFinished(result, data.missingInputs, sources) + break + } + }) + worker.onerror = function (msg) { + compilationFinished({ error: 'Worker error: ' + msg.data }) + } + worker.addEventListener('error', function (msg) { + compilationFinished({ error: 'Worker error: ' + msg.data }) + }) + compileJSON = function (source, optimize) { + jobs.push({sources: source}) + worker.postMessage({cmd: 'compile', job: jobs.length - 1, input: compilerInput(source.sources, {optimize: optimize, target: source.target})}) + } + worker.postMessage({cmd: 'loadVersion', data: url}) + } + + function gatherImports (files, target, importHints, cb) { + importHints = importHints || [] + + // FIXME: This will only match imports if the file begins with one. + // It should tokenize by lines and check each. + // eslint-disable-next-line no-useless-escape + var importRegex = /^\s*import\s*[\'\"]([^\'\"]+)[\'\"];/g + + for (var fileName in files) { + var match + while ((match = importRegex.exec(files[fileName].content))) { + var importFilePath = match[1] + if (importFilePath.startsWith('./')) { + var path = /(.*\/).*/.exec(target) + if (path !== null) { + importFilePath = importFilePath.replace('./', path[1]) + } else { + importFilePath = importFilePath.slice(2) + } + } + + // FIXME: should be using includes or sets, but there's also browser compatibility.. + if (importHints.indexOf(importFilePath) === -1) { + importHints.push(importFilePath) + } + } + } + + while (importHints.length > 0) { + var m = importHints.pop() + if (m in files) { + continue + } + + if (handleImportCall) { + handleImportCall(m, function (err, content) { + if (err) { + cb(err) + } else { + files[m] = { content } + gatherImports(files, target, importHints, cb) + } + }) + } + + return + } + + cb(null, { 'sources': files, 'target': target }) + } + + function truncateVersion (version) { + var tmp = /^(\d+.\d+.\d+)/.exec(version) + if (tmp) { + return tmp[1] + } + return version + } + + function updateInterface (data) { + txHelper.visitContracts(data.contracts, (contract) => { + data.contracts[contract.file][contract.name].abi = solcABI.update(truncateVersion(currentVersion), contract.object.abi) + }) + return data + } +} + +module.exports = Compiler diff --git a/remix-solidity/src/compiler/txHelper.js b/remix-solidity/src/compiler/txHelper.js new file mode 100644 index 0000000000..b9337cf36c --- /dev/null +++ b/remix-solidity/src/compiler/txHelper.js @@ -0,0 +1,33 @@ +'use strict' + +module.exports = { + + /** + * return the contract obj of the given @arg name. Uses last compilation result. + * return null if not found + * @param {String} name - contract name + * @returns contract obj and associated file: { contract, file } or null + */ + getContract: (contractName, contracts) => { + for (var file in contracts) { + if (contracts[file][contractName]) { + return { object: contracts[file][contractName], file: file } + } + } + return null + }, + + /** + * call the given @arg cb (function) for all the contracts. Uses last compilation result + * stop visiting when cb return true + * @param {Function} cb - callback + */ + visitContracts: (contracts, cb) => { + for (var file in contracts) { + for (var name in contracts[file]) { + if (cb({ name: name, object: contracts[file][name], file: file })) return + } + } + } + +} diff --git a/src/code/codeManager.js b/src/code/codeManager.js deleted file mode 100644 index cbd45a5a8a..0000000000 --- a/src/code/codeManager.js +++ /dev/null @@ -1,100 +0,0 @@ -'use strict' -var traceHelper = require('../helpers/traceHelper') -var codeResolver = require('./codeResolver') -var util = require('../helpers/global') -var EventManager = require('../lib/eventManager') - -/* - resolve contract code referenced by vmtrace in order to be used by asm listview. - events: - - indexChanged: triggered when an item is selected - - codeChanged: triggered when an item (in a different context) is selected - - loadingCode: triggerred when loading new code - - resolvingStep: when CodeManager resolves code/selected instruction of a new step -*/ - -function CodeManager (_traceManager) { - util.extend(this, new EventManager()) - this.isLoading = false - this.traceManager = _traceManager - this.currentAddress = '' - this.codeResolver = codeResolver -} - -CodeManager.prototype.resolveStep = function (stepIndex, tx) { - if (stepIndex < 0) return - this.trigger('resolvingStep') - var self = this - if (stepIndex === 0) { - self.ensureCodeLoaded(tx.to, stepIndex, tx) - } else { - this.traceManager.getCurrentCalledAddressAt(stepIndex, function (error, address) { - if (error) { - console.log(error) - } else { - self.ensureCodeLoaded(address, stepIndex, tx) - } - }) - } -} - -CodeManager.prototype.ensureCodeLoaded = function (address, currentStep, tx) { - var self = this - if (address !== this.currentAddress) { - if (traceHelper.isContractCreation(address)) { - this.traceManager.getContractCreationCode(address, function (error, hexCode) { - // contract creation - if (error) { - console.log(error) - } else { - var codes = codeResolver.cacheExecutingCode(address, hexCode) - self.trigger('loadingCode', [address]) - self.getInstructionIndex(address, currentStep, function (error, result) { - if (!error) { - self.trigger('codeChanged', [codes.code, address, result]) - self.trigger('indexChanged', [result]) - self.currentAddress = address - } else { - console.log(error) - } - }) - } - }) - } else { - codeResolver.resolveCode(address, currentStep, tx, function (address, code) { - // resoling code from stack - self.trigger('loadingCode', [address]) - self.getInstructionIndex(address, currentStep, function (error, result) { - if (!error) { - self.trigger('codeChanged', [code, address, result]) - self.trigger('indexChanged', [result]) - self.currentAddress = address - } else { - console.log(error) - } - }) - }) - } - } else { - // only set selected item - this.getInstructionIndex(this.currentAddress, currentStep, function (error, result) { - if (!error) { - self.trigger('indexChanged', [result]) - } - }) - } -} - -CodeManager.prototype.getInstructionIndex = function (address, step, callback) { - this.traceManager.getCurrentPC(step, function (error, instIndex) { - if (error) { - console.log(error) - callback('Cannot retrieve current PC for ' + step, null) - } else { - var itemIndex = codeResolver.getInstructionIndex(address, instIndex) - callback(null, itemIndex) - } - }) -} - -module.exports = CodeManager diff --git a/src/code/codeResolver.js b/src/code/codeResolver.js deleted file mode 100644 index 0d2a858b85..0000000000 --- a/src/code/codeResolver.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict' -var codeUtils = require('./codeUtils') -var util = require('../helpers/global') - -module.exports = { - codes: {}, // assembly items instructions list by contract addesses - instructionsIndexByBytesOffset: {}, // mapping between bytes offset and instructions index. - - resolveCode: function (address, vmTraceIndex, transaction, callBack) { - var cache = this.getExecutingCodeFromCache(address) - if (cache) { - callBack(address, cache.code) - return - } - - var self = this - this.loadCode(address, function (code) { - callBack(address, self.cacheExecutingCode(address, code).code) - }) - }, - - loadCode: function (address, callback) { - console.log('loading new code from web3 ' + address) - util.web3.eth.getCode(address, function (error, result) { - if (error) { - console.log(error) - } else { - callback(result) - } - }) - }, - - cacheExecutingCode: function (address, hexCode) { - var codes = this.formatCode(hexCode) - this.codes[address] = codes.code - this.instructionsIndexByBytesOffset[address] = codes.instructionsIndexByBytesOffset - return codes - }, - - formatCode: function (hexCode) { - var code = codeUtils.nameOpCodes(new Buffer(hexCode.substring(2), 'hex')) - return { - code: code[0], - instructionsIndexByBytesOffset: code[1] - } - }, - - getExecutingCodeFromCache: function (address) { - if (this.codes[address]) { - return { - code: this.codes[address], - instructionsIndexByBytesOffset: this.instructionsIndexByBytesOffset[address] - } - } else { - return null - } - }, - - getInstructionIndex: function (address, pc) { - return this.getExecutingCodeFromCache(address).instructionsIndexByBytesOffset[pc] - } -} diff --git a/src/code/opcodes.js b/src/code/opcodes.js deleted file mode 100644 index dd51c2bb20..0000000000 --- a/src/code/opcodes.js +++ /dev/null @@ -1,179 +0,0 @@ -'use strict' -var codes = { - // 0x0 range - arithmetic ops - // name, baseCost, off stack, on stack, dynamic - 0x00: ['STOP', 0, 0, 0, false], - 0x01: ['ADD', 3, 2, 1, false], - 0x02: ['MUL', 5, 2, 1, false], - 0x03: ['SUB', 3, 2, 1, false], - 0x04: ['DIV', 5, 2, 1, false], - 0x05: ['SDIV', 5, 2, 1, false], - 0x06: ['MOD', 5, 2, 1, false], - 0x07: ['SMOD', 5, 2, 1, false], - 0x08: ['ADDMOD', 8, 3, 1, false], - 0x09: ['MULMOD', 8, 3, 1, false], - 0x0a: ['EXP', 10, 2, 1, false], - 0x0b: ['SIGNEXTEND', 5, 1, 1, false], - - // 0x10 range - bit ops - 0x10: ['LT', 3, 2, 1, false], - 0x11: ['GT', 3, 2, 1, false], - 0x12: ['SLT', 3, 2, 1, false], - 0x13: ['SGT', 3, 2, 1, false], - 0x14: ['EQ', 3, 2, 1, false], - 0x15: ['ISZERO', 3, 1, 1, false], - 0x16: ['AND', 3, 2, 1, false], - 0x17: ['OR', 3, 2, 1, false], - 0x18: ['XOR', 3, 2, 1, false], - 0x19: ['NOT', 3, 1, 1, false], - 0x1a: ['BYTE', 3, 2, 1, false], - - // 0x20 range - crypto - 0x20: ['SHA3', 30, 2, 1, false], - - // 0x30 range - closure state - 0x30: ['ADDRESS', 2, 0, 1, true], - 0x31: ['BALANCE', 20, 1, 1, true], - 0x32: ['ORIGIN', 2, 0, 1, true], - 0x33: ['CALLER', 2, 0, 1, true], - 0x34: ['CALLVALUE', 2, 0, 1, true], - 0x35: ['CALLDATALOAD', 3, 1, 1, true], - 0x36: ['CALLDATASIZE', 2, 0, 1, true], - 0x37: ['CALLDATACOPY', 3, 3, 0, true], - 0x38: ['CODESIZE', 2, 0, 1, false], - 0x39: ['CODECOPY', 3, 3, 0, false], - 0x3a: ['GASPRICE', 2, 0, 1, false], - 0x3b: ['EXTCODESIZE', 20, 1, 1, true], - 0x3c: ['EXTCODECOPY', 20, 4, 0, true], - - // '0x40' range - block operations - 0x40: ['BLOCKHASH', 20, 1, 1, true], - 0x41: ['COINBASE', 2, 0, 1, true], - 0x42: ['TIMESTAMP', 2, 0, 1, true], - 0x43: ['NUMBER', 2, 0, 1, true], - 0x44: ['DIFFICULTY', 2, 0, 1, true], - 0x45: ['GASLIMIT', 2, 0, 1, true], - - // 0x50 range - 'storage' and execution - 0x50: ['POP', 2, 1, 0, false], - 0x51: ['MLOAD', 3, 1, 1, false], - 0x52: ['MSTORE', 3, 2, 0, false], - 0x53: ['MSTORE8', 3, 2, 0, false], - 0x54: ['SLOAD', 50, 1, 1, true], - 0x55: ['SSTORE', 0, 2, 0, true], - 0x56: ['JUMP', 8, 1, 0, false], - 0x57: ['JUMPI', 10, 2, 0, false], - 0x58: ['PC', 2, 0, 1, false], - 0x59: ['MSIZE', 2, 0, 1, false], - 0x5a: ['GAS', 2, 0, 1, false], - 0x5b: ['JUMPDEST', 1, 0, 0, false], - - // 0x60, range - 0x60: ['PUSH', 3, 0, 1, false], - 0x61: ['PUSH', 3, 0, 1, false], - 0x62: ['PUSH', 3, 0, 1, false], - 0x63: ['PUSH', 3, 0, 1, false], - 0x64: ['PUSH', 3, 0, 1, false], - 0x65: ['PUSH', 3, 0, 1, false], - 0x66: ['PUSH', 3, 0, 1, false], - 0x67: ['PUSH', 3, 0, 1, false], - 0x68: ['PUSH', 3, 0, 1, false], - 0x69: ['PUSH', 3, 0, 1, false], - 0x6a: ['PUSH', 3, 0, 1, false], - 0x6b: ['PUSH', 3, 0, 1, false], - 0x6c: ['PUSH', 3, 0, 1, false], - 0x6d: ['PUSH', 3, 0, 1, false], - 0x6e: ['PUSH', 3, 0, 1, false], - 0x6f: ['PUSH', 3, 0, 1, false], - 0x70: ['PUSH', 3, 0, 1, false], - 0x71: ['PUSH', 3, 0, 1, false], - 0x72: ['PUSH', 3, 0, 1, false], - 0x73: ['PUSH', 3, 0, 1, false], - 0x74: ['PUSH', 3, 0, 1, false], - 0x75: ['PUSH', 3, 0, 1, false], - 0x76: ['PUSH', 3, 0, 1, false], - 0x77: ['PUSH', 3, 0, 1, false], - 0x78: ['PUSH', 3, 0, 1, false], - 0x79: ['PUSH', 3, 0, 1, false], - 0x7a: ['PUSH', 3, 0, 1, false], - 0x7b: ['PUSH', 3, 0, 1, false], - 0x7c: ['PUSH', 3, 0, 1, false], - 0x7d: ['PUSH', 3, 0, 1, false], - 0x7e: ['PUSH', 3, 0, 1, false], - 0x7f: ['PUSH', 3, 0, 1, false], - - 0x80: ['DUP', 3, 0, 1, false], - 0x81: ['DUP', 3, 0, 1, false], - 0x82: ['DUP', 3, 0, 1, false], - 0x83: ['DUP', 3, 0, 1, false], - 0x84: ['DUP', 3, 0, 1, false], - 0x85: ['DUP', 3, 0, 1, false], - 0x86: ['DUP', 3, 0, 1, false], - 0x87: ['DUP', 3, 0, 1, false], - 0x88: ['DUP', 3, 0, 1, false], - 0x89: ['DUP', 3, 0, 1, false], - 0x8a: ['DUP', 3, 0, 1, false], - 0x8b: ['DUP', 3, 0, 1, false], - 0x8c: ['DUP', 3, 0, 1, false], - 0x8d: ['DUP', 3, 0, 1, false], - 0x8e: ['DUP', 3, 0, 1, false], - 0x8f: ['DUP', 3, 0, 1, false], - - 0x90: ['SWAP', 3, 0, 0, false], - 0x91: ['SWAP', 3, 0, 0, false], - 0x92: ['SWAP', 3, 0, 0, false], - 0x93: ['SWAP', 3, 0, 0, false], - 0x94: ['SWAP', 3, 0, 0, false], - 0x95: ['SWAP', 3, 0, 0, false], - 0x96: ['SWAP', 3, 0, 0, false], - 0x97: ['SWAP', 3, 0, 0, false], - 0x98: ['SWAP', 3, 0, 0, false], - 0x99: ['SWAP', 3, 0, 0, false], - 0x9a: ['SWAP', 3, 0, 0, false], - 0x9b: ['SWAP', 3, 0, 0, false], - 0x9c: ['SWAP', 3, 0, 0, false], - 0x9d: ['SWAP', 3, 0, 0, false], - 0x9e: ['SWAP', 3, 0, 0, false], - 0x9f: ['SWAP', 3, 0, 0, false], - - 0xa0: ['LOG', 375, 2, 0, false], - 0xa1: ['LOG', 375, 3, 0, false], - 0xa2: ['LOG', 375, 4, 0, false], - 0xa3: ['LOG', 375, 5, 0, false], - 0xa4: ['LOG', 375, 6, 0, false], - - // '0xf0' range - closures - 0xf0: ['CREATE', 32000, 3, 1, true], - 0xf1: ['CALL', 40, 7, 1, true], - 0xf2: ['CALLCODE', 40, 7, 1, true], - 0xf3: ['RETURN', 0, 2, 0, false], - 0xf4: ['DELEGATECALL', 40, 6, 1, true], - - // '0x70', range - other - 0xff: ['SUICIDE', 0, 1, 0, false] -} - -module.exports = function (op, full) { - var code = codes[op] ? codes[op] : ['INVALID', 0] - var opcode = code[0] - - if (full) { - if (opcode === 'LOG') { - opcode += op - 0xa0 - } - - if (opcode === 'PUSH') { - opcode += op - 0x5f - } - - if (opcode === 'DUP') { - opcode += op - 0x7f - } - - if (opcode === 'SWAP') { - opcode += op - 0x8f - } - } - - return {name: opcode, fee: code[1], in: code[2], out: code[3], dynamic: code[4], async: code[5]} -} diff --git a/src/helpers/global.js b/src/helpers/global.js deleted file mode 100644 index e652127824..0000000000 --- a/src/helpers/global.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict' -module.exports = { - extend: function (destination, source) { - for (var property in source) { - destination[property] = source[property] - } - }, - web3: null -} diff --git a/src/helpers/init.js b/src/helpers/init.js deleted file mode 100644 index fdf71cf492..0000000000 --- a/src/helpers/init.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict' -var Web3 = require('web3') -var Web3Admin = require('../util/web3Admin') - -module.exports = { - loadWeb3: function () { - var web3 = new Web3() - web3.setProvider(new web3.providers.HttpProvider('http://localhost:8545')) - Web3Admin.extend(web3) - return web3 - }, - - extendWeb3: function (web3) { - Web3Admin.extend(web3) - }, - - setProvider: function (web3, url) { - web3.setProvider(new web3.providers.HttpProvider(url)) - } -} diff --git a/src/helpers/util.js b/src/helpers/util.js deleted file mode 100644 index bd0e69bfe9..0000000000 --- a/src/helpers/util.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict' -module.exports = { - /* - ints: IntArray - */ - hexConvert: function (ints) { - var ret = '0x' - for (var i = 0; i < ints.length; i++) { - var h = ints[i] - if (h) { - h = h.toString(16) - ret += ('0x' + h) < 0x10 ? '0' + h : h - } else { - ret += '00' - } - } - return ret - }, - - /* - ints: list of IntArrays - */ - hexListConvert: function (intsList) { - var ret = [] - for (var k in intsList) { - ret.push(this.hexConvert(intsList[k])) - } - return ret - }, - - /* - ints: ints: IntArray - */ - formatMemory: function (mem) { - var hexMem = this.hexConvert(mem).substr(2) - var ret = [] - for (var k = 0; k < hexMem.length; k += 32) { - var row = hexMem.substr(k, 32) - ret.push(row) - } - return ret - } -} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index d15b366946..0000000000 --- a/src/index.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict' -var VMDebugger = require('./ui/VmDebugger') -var Debugger = require('./ui/Ethdebugger') -var BasicPanel = require('./ui/BasicPanel') -var TraceManager = require('./trace/traceManager') -var CodeManager = require('./code/codeManager') - -if (typeof (module) !== 'undefined' && typeof (module.exports) !== 'undefined') { - module.exports = modules() -} -if (window) { - window.remix = modules() -} - -function modules () { - return { - code: { - codeManager: CodeManager - }, - trace: { - traceManager: TraceManager - }, - ui: { - Debugger: Debugger, - VMdebugger: VMDebugger, - BasicPanel: BasicPanel - } - } -} - diff --git a/src/lib/eventManager.js b/src/lib/eventManager.js deleted file mode 100644 index d16bd169fa..0000000000 --- a/src/lib/eventManager.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict' -function EventManager () { - this.registered = {} -} - -EventManager.prototype.unregister = function (eventName, obj) { - for (var reg in this.registered[eventName]) { - if (this.registered[eventName][reg] && this.registered[eventName][reg].obj === obj) { - this.registered[eventName].splice(reg, 1) - return - } - } -} - -EventManager.prototype.register = function (eventName, obj, func) { - if (!this.registered[eventName]) { - this.registered[eventName] = [] - } - this.registered[eventName].push({ - obj: obj, - func: func - }) -} - -EventManager.prototype.trigger = function (eventName, args) { - for (var listener in this.registered[eventName]) { - var l = this.registered[eventName][listener] - l.func.apply(l.obj, args) - } -} - -module.exports = EventManager diff --git a/src/trace/traceRetriever.js b/src/trace/traceRetriever.js deleted file mode 100644 index 4face2e815..0000000000 --- a/src/trace/traceRetriever.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict' -var traceHelper = require('../helpers/traceHelper') -var util = require('../helpers/global') - -function TraceRetriever () { - this.storages = {} // contains all intial storage (by addresses) -} - -TraceRetriever.prototype.getTrace = function (txHash, callback) { - var options = { - disableStorage: true, - disableMemory: false, - disableStack: false, - fullStorage: false - } - util.web3.debug.traceTransaction(txHash, options, function (error, result) { - callback(error, result) - }) -} - -TraceRetriever.prototype.getStorage = function (tx, address, callback) { - if (traceHelper.isContractCreation(address)) { - callback(null, {}) - } else if (this.storages[address]) { - callback(null, this.storages[address]) - } else { - // we always return an empty storage ... storage changes will be displayed instead of the full contract storage - callback(null, {}) - /* - var self = this - util.web3.debug.storageAt(tx.blockNumber.toString(), tx.transactionIndex, address, function (error, result) { - self.storages[address] = result - callback(error, result) - }) - */ - } -} - -TraceRetriever.prototype.debugStorageAtAvailable = function () { - return false // util.web3.version.node.toLowerCase().indexOf('geth') === -1 // storageAt not available if using geth -} - -module.exports = TraceRetriever diff --git a/src/trace/traceStepManager.js b/src/trace/traceStepManager.js deleted file mode 100644 index 5f20aa25d2..0000000000 --- a/src/trace/traceStepManager.js +++ /dev/null @@ -1,81 +0,0 @@ -'use strict' -var traceHelper = require('../helpers/traceHelper') - -function TraceStepManager (_traceAnalyser) { - this.traceAnalyser = _traceAnalyser -} - -TraceStepManager.prototype.isCallInstruction = function (index) { - var state = this.traceAnalyser.trace[index] - return traceHelper.isCallInstruction(state) -} - -TraceStepManager.prototype.isReturnInstruction = function (index) { - var state = this.traceAnalyser.trace[index] - return traceHelper.isReturnInstruction(state) -} - -TraceStepManager.prototype.findStepOverBack = function (currentStep) { - if (currentStep === 0) return 0 - return this.findStepOutBack(currentStep) -} - -TraceStepManager.prototype.findStepOverForward = function (currentStep) { - if (currentStep === this.traceAnalyser.trace.length - 1) return currentStep - return this.findStepOutForward(currentStep) -} - -TraceStepManager.prototype.findStepOutBack = function (currentStep) { - if (!this.traceAnalyser.trace) { - return currentStep - } - var i = currentStep - 1 - var depth = 0 - while (--i >= 0) { - if (this.isCallInstruction(i)) { - if (depth === 0) { - break - } else { - depth-- - } - } else if (this.isReturnInstruction(i)) { - depth++ - } - } - return i -} - -TraceStepManager.prototype.findStepOutForward = function (currentStep) { - if (!this.traceAnalyser.trace) { - return currentStep - } - var i = currentStep - var depth = 0 - while (++i < this.traceAnalyser.trace.length) { - if (this.isReturnInstruction(i)) { - if (depth === 0) { - break - } else { - depth-- - } - } else if (this.isCallInstruction(i)) { - depth++ - } - } - return i -} - -TraceStepManager.prototype.findNextCall = function (currentStep) { - if (!this.traceAnalyser.trace) { - return currentStep - } - var i = currentStep - while (++i < this.traceAnalyser.trace.length) { - if (this.isCallInstruction(i)) { - return i - } - } - return currentStep -} - -module.exports = TraceStepManager diff --git a/src/ui/ASMCode.js b/src/ui/ASMCode.js deleted file mode 100644 index 54c2f8b8ff..0000000000 --- a/src/ui/ASMCode.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict' -var style = require('./styles/basicStyles') -var yo = require('yo-yo') -var CodeManager = require('../code/codeManager') -var ui = require('../helpers/ui') - -function ASMCode (_parent, _traceManager) { - this.parent = _parent - this.codeManager = new CodeManager(_traceManager) - this.code - this.address - this.codeView - - this.init() -} - -ASMCode.prototype.render = function () { - var view = ( - yo`` - ) - if (!this.view) { - this.view = view - } - return view -} - -ASMCode.prototype.init = function () { - var self = this - this.codeManager.register('indexChanged', this, this.indexChanged) - this.codeManager.register('codeChanged', this, this.codeChanged) - this.codeManager.register('loadingCode', this, function (address) {}) - this.parent.register('indexChanged', this, function (index) { - self.codeManager.resolveStep(index, self.parent.tx) - }) -} - -ASMCode.prototype.indexChanged = function (index) { - document.getElementById('asmitems').value = index -} - -ASMCode.prototype.codeChanged = function (code, address, index) { - this.code = code - this.address = address - this.renderAssemblyItems() - yo.update(this.view, this.render()) - document.getElementById('asmitems').value = index -} - -ASMCode.prototype.renderAssemblyItems = function () { - if (this.code) { - this.codeView = this.code.map(function (item, i) { - return yo`` - }) - return this.codeView - } -} - -module.exports = ASMCode diff --git a/src/ui/ButtonNavigator.js b/src/ui/ButtonNavigator.js deleted file mode 100644 index 8a60f1a861..0000000000 --- a/src/ui/ButtonNavigator.js +++ /dev/null @@ -1,90 +0,0 @@ -'use strict' -var util = require('../helpers/global') -var EventManager = require('../lib/eventManager') -var yo = require('yo-yo') - -function ButtonNavigator (_traceManager) { - util.extend(this, new EventManager()) - this.intoBackDisabled = true - this.overBackDisabled = true - this.intoForwardDisabled = true - this.overForwardDisabled = true - this.nextCallDisabled = true - - this.traceManager = _traceManager - - this.view -} - -module.exports = ButtonNavigator - -ButtonNavigator.prototype.render = function () { - var self = this - var view = yo`
    - - - - - -
    ` - if (!this.view) { - this.view = view - } - return view -} - -ButtonNavigator.prototype.stepChanged = function (step) { - this.intoBackDisabled = step <= 0 - this.overBackDisabled = step <= 0 - if (!this.traceManager) { - this.intoForwardDisabled = true - this.overForwardDisabled = true - this.nextCallDisabled = true - } else { - var self = this - this.traceManager.getLength(function (error, length) { - if (error) { - self.intoBackDisabled = true - self.overBackDisabled = true - self.intoForwardDisabled = true - self.overForwardDisabled = true - self.nextCallDisabled = true - console.log(error) - } else { - self.intoForwardDisabled = step >= length - 1 - self.overForwardDisabled = step >= length - 1 - self.nextCallDisabled = step >= length - 1 - } - self.updateAll() - }) - } - this.updateAll() -} - -ButtonNavigator.prototype.updateAll = function () { - this.updateDisabled('intoback', this.intoBackDisabled) - this.updateDisabled('overback', this.overBackDisabled) - this.updateDisabled('overforward', this.overForwardDisabled) - this.updateDisabled('intoforward', this.intoForwardDisabled) - this.updateDisabled('nextcall', this.nextCallDisabled) -} - -ButtonNavigator.prototype.updateDisabled = function (id, disabled) { - if (disabled) { - document.getElementById(id).setAttribute('disabled', true) - } else { - document.getElementById(id).removeAttribute('disabled') - } -} - -module.exports = ButtonNavigator diff --git a/src/ui/Ethdebugger.js b/src/ui/Ethdebugger.js deleted file mode 100644 index 428151b5f0..0000000000 --- a/src/ui/Ethdebugger.js +++ /dev/null @@ -1,124 +0,0 @@ -'use strict' -var TxBrowser = require('./TxBrowser') -var StepManager = require('./StepManager') -var TraceManager = require('../trace/traceManager') -var VmDebugger = require('./VmDebugger') -var Sticker = require('./Sticker') -var style = require('./styles/basicStyles') -var util = require('../helpers/global') -var EventManager = require('../lib/eventManager') -var yo = require('yo-yo') -var ui = require('../helpers/ui') -var Web3Providers = require('../web3Provider/web3Providers') -var DummyProvider = require('../web3Provider/dummyProvider') - -function Ethdebugger () { - util.extend(this, new EventManager()) - - this.currentStepIndex = -1 - this.tx - this.statusMessage = '' - - this.view - this.web3Providers = new Web3Providers() - this.addProvider('DUMMYWEB3', new DummyProvider()) - this.switchProvider('DUMMYWEB3') - this.traceManager = new TraceManager() - - var self = this - this.txBrowser = new TxBrowser(this) - this.txBrowser.register('newTxLoading', this, function () { - self.unLoad() - }) - this.txBrowser.register('newTraceRequested', this, function (blockNumber, txIndex, tx) { - self.startDebugging(blockNumber, txIndex, tx) - }) - this.txBrowser.register('unloadRequested', this, function (blockNumber, txIndex, tx) { - self.unLoad() - }) - this.stepManager = new StepManager(this, this.traceManager) - this.stepManager.register('stepChanged', this, function (stepIndex) { - self.stepChanged(stepIndex) - }) - this.vmDebugger = new VmDebugger(this, this.traceManager) - this.sticker = new Sticker(this, this.traceManager) -} - -Ethdebugger.prototype.web3 = function () { - return util.web3 -} - -Ethdebugger.prototype.addProvider = function (type, obj) { - this.web3Providers.addProvider(type, obj) - this.trigger('providerAdded', [type]) -} - -Ethdebugger.prototype.switchProvider = function (type) { - var self = this - this.web3Providers.get(type, function (error, obj) { - if (error) { - console.log('provider ' + type + ' not defined') - } else { - util.web3 = obj - self.trigger('providerChanged', [type]) - } - }) -} - -Ethdebugger.prototype.debug = function (tx) { - this.txBrowser.load(tx.hash) -} - -Ethdebugger.prototype.render = function () { - var view = yo`
    -

    VM Debugger

    -
    - ${this.txBrowser.render()} - ${this.stepManager.render()} -
    -
    - ${this.sticker.render()} -
    -
    ${this.statusMessage}
    - ${this.vmDebugger.render()} -
    ` - if (!this.view) { - this.view = view - } - return view -} - -Ethdebugger.prototype.unLoad = function () { - this.traceManager.init() - this.stepManager.reset() - this.trigger('traceUnloaded') -} - -Ethdebugger.prototype.stepChanged = function (stepIndex) { - this.currentStepIndex = stepIndex - this.trigger('indexChanged', [stepIndex]) -} - -Ethdebugger.prototype.startDebugging = function (blockNumber, txIndex, tx) { - if (this.traceManager.isLoading) { - return - } - this.statusMessage = 'Loading trace...' - yo.update(this.view, this.render()) - console.log('loading trace...') - this.tx = tx - var self = this - this.traceManager.resolveTrace(tx, function (error, result) { - console.log('trace loaded ' + result + ' ' + error) - if (result) { - self.statusMessage = '' - yo.update(self.view, self.render()) - self.trigger('newTraceLoaded') - } else { - self.statusMessage = error - yo.update(self.view, self.render()) - } - }) -} - -module.exports = Ethdebugger diff --git a/src/ui/Slider.js b/src/ui/Slider.js deleted file mode 100644 index f0d9d08377..0000000000 --- a/src/ui/Slider.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict' -var style = require('./styles/sliderStyles') -var util = require('../helpers/global') -var EventManager = require('../lib/eventManager') -var yo = require('yo-yo') -var ui = require('../helpers/ui') - -function Slider (_traceManager) { - util.extend(this, new EventManager()) - this.traceManager = _traceManager - this.max - this.disabled = true - this.view -} - -Slider.prototype.render = function () { - var self = this - var view = yo`
    - -
    ` - if (!this.view) { - this.view = view - } - return view -} - -Slider.prototype.init = function (length) { - var slider = document.getElementById('slider') - slider.setAttribute('max', length - 1) - this.max = length - 1 - this.updateDisabled(length === 0) - this.disabled = length === 0 - this.setValue(0) -} - -Slider.prototype.onChange = function (event) { - var value = document.getElementById('slider').value - this.trigger('moved', [parseInt(value)]) -} - -Slider.prototype.setValue = function (value) { - var slider = document.getElementById('slider') - var diff = value - slider.value - if (diff > 0) { - slider.stepUp(diff) - } else { - slider.stepDown(Math.abs(diff)) - } -} - -Slider.prototype.updateDisabled = function (disabled) { - if (disabled) { - document.getElementById('slider').setAttribute('disabled', true) - } else { - document.getElementById('slider').removeAttribute('disabled') - } -} - -module.exports = Slider diff --git a/src/ui/StepManager.js b/src/ui/StepManager.js deleted file mode 100644 index bb7763e824..0000000000 --- a/src/ui/StepManager.js +++ /dev/null @@ -1,138 +0,0 @@ -'use strict' -var ButtonNavigator = require('./ButtonNavigator') -var Slider = require('./Slider') -var style = require('./styles/basicStyles') -var util = require('../helpers/global') -var EventManager = require('../lib/eventManager') -var yo = require('yo-yo') -var ui = require('../helpers/ui') - -function StepManager (_parent, _traceManager) { - util.extend(this, new EventManager()) - this.parent = _parent - this.traceManager = _traceManager - - var self = this - this.parent.register('newTraceLoaded', this, function () { - self.traceManager.getLength(function (error, length) { - if (error) { - console.log(error) - } else { - self.slider.init(length) - self.init() - } - }) - }) - - this.slider = new Slider(this.traceManager) - this.slider.register('moved', this, function (step) { - self.sliderMoved(step) - }) - - this.buttonNavigator = new ButtonNavigator(this.traceManager) - this.buttonNavigator.register('stepIntoBack', this, function () { - self.stepIntoBack() - }) - this.buttonNavigator.register('stepIntoForward', this, function () { - self.stepIntoForward() - }) - this.buttonNavigator.register('stepOverBack', this, function () { - self.stepOverBack() - }) - this.buttonNavigator.register('stepOverForward', this, function () { - self.stepOverForward() - }) - this.buttonNavigator.register('jumpNextCall', this, function () { - self.jumpNextCall() - }) -} - -StepManager.prototype.render = function () { - return ( - yo`
    - ${this.slider.render()} - ${this.buttonNavigator.render()} -
    ` - ) -} - -StepManager.prototype.reset = function () { - this.slider.setValue(0) - this.currentStepIndex = 0 - this.buttonNavigator.stepChanged(0) -} - -StepManager.prototype.init = function () { - this.slider.setValue(0) - this.changeState(0) -} - -StepManager.prototype.newTraceAvailable = function () { - this.init() -} - -StepManager.prototype.sliderMoved = function (step) { - if (!this.traceManager.inRange(step)) { - return - } - this.changeState(step) -} - -StepManager.prototype.stepIntoForward = function () { - if (!this.traceManager.isLoaded()) { - return - } - var step = this.currentStepIndex + 1 - if (!this.traceManager.inRange(step)) { - return - } - this.slider.setValue(step) - this.changeState(step) -} - -StepManager.prototype.stepIntoBack = function () { - if (!this.traceManager.isLoaded()) { - return - } - var step = this.currentStepIndex - 1 - if (!this.traceManager.inRange(step)) { - return - } - this.slider.setValue(step) - this.changeState(step) -} - -StepManager.prototype.stepOverForward = function () { - if (!this.traceManager.isLoaded()) { - return - } - var step = this.traceManager.findStepOverForward(this.currentStepIndex) - this.slider.setValue(step) - this.changeState(step) -} - -StepManager.prototype.stepOverBack = function () { - if (!this.traceManager.isLoaded()) { - return - } - var step = this.traceManager.findStepOverBack(this.currentStepIndex) - this.slider.setValue(step) - this.changeState(step) -} - -StepManager.prototype.jumpNextCall = function () { - if (!this.traceManager.isLoaded()) { - return - } - var step = this.traceManager.findNextCall(this.currentStepIndex) - this.slider.setValue(step) - this.changeState(step) -} - -StepManager.prototype.changeState = function (step) { - this.currentStepIndex = step - this.buttonNavigator.stepChanged(step) - this.trigger('stepChanged', [step]) -} - -module.exports = StepManager diff --git a/src/ui/Sticker.js b/src/ui/Sticker.js deleted file mode 100644 index e32f76b11d..0000000000 --- a/src/ui/Sticker.js +++ /dev/null @@ -1,150 +0,0 @@ -'use strict' -var yo = require('yo-yo') - -function Sticker (_parent, _traceManager) { - this.parent = _parent - this.traceManager = _traceManager - - this.vmTraceStep = '-' - this.step = '-' - this.addmemory = '-' - this.gas = '-' - this.remainingGas = '-' - this.loadedAddress = '-' - this.hide = true - - this.view - this.init() -} - -Sticker.prototype.render = function () { - var view = yo`
    - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - VMtracestep: - - ${this.vmTraceStep} -
    - Step: - - ${this.step} -
    - Add memory: - - ${this.addmemory} -
    - Gas: - - ${this.gas} -
    - Remaining gas: - - ${this.remainingGas} -
    - Loaded address: - - ${this.loadedAddress} -
    -
    ` - if (!this.view) { - this.view = view - } - return view -} - -Sticker.prototype.init = function () { - var self = this - this.parent.register('traceUnloaded', this, function () { - self.hide = true - yo.update(self.view, self.render()) - }) - - this.parent.register('newTraceLoaded', this, function () { - self.hide = false - yo.update(self.view, self.render()) - }) - - this.parent.register('indexChanged', this, function (index) { - if (index < 0) return - - self.vmTraceStep = index - - self.traceManager.getCurrentStep(index, function (error, step) { - if (error) { - console.log(error) - self.step = '-' - } else { - self.step = step - } - yo.update(self.view, self.render()) - }) - - self.traceManager.getMemExpand(index, function (error, addmem) { - if (error) { - console.log(error) - self.addmemory = '-' - } else { - self.addmemory = addmem - } - yo.update(self.view, self.render()) - }) - - self.traceManager.getStepCost(index, function (error, gas) { - if (error) { - console.log(error) - self.gas = '-' - } else { - self.gas = gas - } - yo.update(self.view, self.render()) - }) - - self.traceManager.getCurrentCalledAddressAt(index, function (error, address) { - if (error) { - console.log(error) - self.loadedAddress = '-' - } else { - self.loadedAddress = address - } - yo.update(self.view, self.render()) - }) - - self.traceManager.getRemainingGas(index, function (error, remaingas) { - if (error) { - console.log(error) - self.remainingGas = '-' - } else { - self.remainingGas = remaingas - } - yo.update(self.view, self.render()) - }) - }) -} - -module.exports = Sticker diff --git a/src/ui/StoragePanel.js b/src/ui/StoragePanel.js deleted file mode 100644 index abe41e5b73..0000000000 --- a/src/ui/StoragePanel.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict' -var BasicPanel = require('./BasicPanel') -var yo = require('yo-yo') - -function StoragePanel (_parent, _traceManager, _address) { - this.parent = _parent - this.traceManager = _traceManager - this.basicPanel = new BasicPanel('Storage Changes') - this.address = _address - this.init() - this.disabled = false -} - -StoragePanel.prototype.render = function () { - return yo`
    ${this.basicPanel.render()}
    ` -} - -StoragePanel.prototype.init = function () { - var self = this - this.parent.register('indexChanged', this, function (index) { - if (self.disabled) return - if (index < 0) return - if (self.parent.currentStepIndex !== index) return - - self.traceManager.getStorageAt(index, self.parent.tx, function (error, storage) { - if (error) { - console.log(error) - self.basicPanel.data = self.formatStorage(storage) - } else if (self.parent.currentStepIndex === index) { - self.basicPanel.data = self.formatStorage(storage) - } - self.basicPanel.update() - }, self.address) - }) -} - -StoragePanel.prototype.formatStorage = function (storage) { - var ret = '' - for (var key in storage) { - ret += key + ' ' + storage[key] + '\n' - } - return ret -} - -module.exports = StoragePanel diff --git a/src/ui/TxBrowser.js b/src/ui/TxBrowser.js deleted file mode 100644 index a4d18413f9..0000000000 --- a/src/ui/TxBrowser.js +++ /dev/null @@ -1,179 +0,0 @@ -var style = require('./styles/basicStyles') -var util = require('../helpers/global') -var EventManager = require('../lib/eventManager') -var traceHelper = require('../helpers/traceHelper') -var yo = require('yo-yo') -var ui = require('../helpers/ui') -var init = require('../helpers/init') - -function TxBrowser (_parent) { - util.extend(this, new EventManager()) - - this.blockNumber - this.txNumber - this.hash - this.from - this.to - this.view - this.displayConnectionSetting = true - var self = this - _parent.register('providerChanged', this, function (provider) { - self.displayConnectionSetting = provider === 'INTERNAL' - self.setDefaultValues() - if (self.view) { - yo.update(self.view, self.render()) - } - }) -} - -// creation 0xa9619e1d0a35b2c1d686f5b661b3abd87f998d2844e8e9cc905edb57fc9ce349 -// invokation 0x71a6d583d16d142c5c3e8903060e8a4ee5a5016348a9448df6c3e63b68076ec4 0xcda2b2835add61af54cf83bd076664d98d7908c6cd98d86423b3b48d8b8e51ff -// test: -// creation: 0x72908de76f99fca476f9e3a3b5d352f350a98cd77d09cebfc59ffe32a6ecaa0b -// invokation: 0x20ef65b8b186ca942fcccd634f37074dde49b541c27994fc7596740ef44cfd51 - -TxBrowser.prototype.setDefaultValues = function () { - this.from = ' - ' - this.to = ' - ' - this.hash = ' - ' - this.blockNumber = null - this.txNumber = '' - this.connectInfo = '' - this.updateWeb3Url(util.web3.currentProvider.host) -} - -TxBrowser.prototype.submit = function () { - if (!this.txNumber) { - return - } - this.trigger('newTxLoading', [this.blockNumber, this.txNumber, tx]) - var tx - try { - if (this.txNumber.indexOf('0x') !== -1) { - tx = util.web3.eth.getTransaction(this.txNumber) - } else { - tx = util.web3.eth.getTransactionFromBlock(this.blockNumber, this.txNumber) - } - } catch (e) { - console.log(e) - } - console.log(JSON.stringify(tx)) - if (tx) { - if (!tx.to) { - tx.to = traceHelper.contractCreationToken('0') - } - this.from = tx.from - this.to = tx.to - this.hash = tx.hash - this.trigger('newTraceRequested', [this.blockNumber, this.txNumber, tx]) - } else { - var mes = '' - this.from = mes - this.to = mes - this.hash = mes - console.log('cannot find ' + this.blockNumber + ' ' + this.txNumber) - } - yo.update(this.view, this.render()) -} - -TxBrowser.prototype.updateWeb3Url = function (newhost) { - init.setProvider(util.web3, newhost) - var self = this - this.checkWeb3(function (error, block) { - if (!error) { - self.connectInfo = 'Connected to ' + util.web3.currentProvider.host + '. Current block number: ' + block - } else { - self.connectInfo = 'Unable to connect to ' + util.web3.currentProvider.host + '. ' + error.message - } - yo.update(self.view, self.render()) - }) -} - -TxBrowser.prototype.checkWeb3 = function (callback) { - try { - util.web3.eth.getBlockNumber(function (error, block) { - callback(error, block) - }) - } catch (e) { - console.log(e) - callback(e.message, null) - } -} - -TxBrowser.prototype.updateBlockN = function (ev) { - this.blockNumber = ev.target.value -} - -TxBrowser.prototype.updateTxN = function (ev) { - this.txNumber = ev.target.value -} - -TxBrowser.prototype.load = function (txHash) { - this.txNumber = txHash - yo.update(this.view, this.render()) - this.submit() -} - -TxBrowser.prototype.init = function (ev) { - this.setDefaultValues() - yo.update(this.view, this.render()) -} - -TxBrowser.prototype.connectionSetting = function () { - if (this.displayConnectionSetting) { - var self = this - return yo`
    Node URL: - ${this.connectInfo}
    ` - } else { - return '' - } -} - -TxBrowser.prototype.render = function () { - var self = this - var view = yo`
    - ${this.connectionSetting()} - - - - -
    - - - - - - - - - - - - - - - -
    - Hash: - - ${this.hash} -
    - From: - - ${this.from} -
    - To: - - ${this.to} -
    -
    -
    ` - if (!this.view) { - this.view = view - } - return view -} - -module.exports = TxBrowser diff --git a/src/ui/VmDebugger.js b/src/ui/VmDebugger.js deleted file mode 100644 index d319d02d2c..0000000000 --- a/src/ui/VmDebugger.js +++ /dev/null @@ -1,102 +0,0 @@ -'use strict' -var style = require('./styles/basicStyles') -var ASMCode = require('./ASMCode') -var CalldataPanel = require('./CalldataPanel') -var MemoryPanel = require('./MemoryPanel') -var CallstackPanel = require('./CallstackPanel') -var StackPanel = require('./StackPanel') -var StoragePanel = require('./StoragePanel') -var BasicPanel = require('./BasicPanel') -var FullStoragesChangesPanel = require('./FullStoragesChanges') -var yo = require('yo-yo') -var ui = require('../helpers/ui') - -function VmDebugger (_parent, _traceManager) { - this.asmCode = new ASMCode(_parent, _traceManager) - this.stackPanel = new StackPanel(_parent, _traceManager) - this.storagePanel = new StoragePanel(_parent, _traceManager) - this.memoryPanel = new MemoryPanel(_parent, _traceManager) - this.calldataPanel = new CalldataPanel(_parent, _traceManager) - this.callstackPanel = new CallstackPanel(_parent, _traceManager) - - /* Return values - */ - this.returnValuesPanel = new BasicPanel('Return Value', '1205px', '100px') - _parent.register('indexChanged', this.returnValuesPanel, function (index) { - var self = this - _traceManager.getReturnValue(index, function (error, returnValue) { - if (error) { - console.log(error) - self.data = '' - } else if (_parent.currentStepIndex === index) { - self.data = returnValue - } - self.update() - if (!returnValue) { - self.hide() - } - }) - }) - /* Return values - */ - - this.fullStoragesChangesPanel = new FullStoragesChangesPanel(_parent, _traceManager) - - this.view - var self = this - _parent.register('newTraceLoaded', this, function () { - self.view.style.display = 'block' - }) - _parent.register('traceUnloaded', this, function () { - self.view.style.display = 'none' - }) -} - -VmDebugger.prototype.render = function () { - var view = yo`` - if (!this.view) { - this.view = view - } - return view -} - -module.exports = VmDebugger diff --git a/src/ui/styles/basicStyles.js b/src/ui/styles/basicStyles.js deleted file mode 100644 index e91abb1e4c..0000000000 --- a/src/ui/styles/basicStyles.js +++ /dev/null @@ -1,57 +0,0 @@ -'use strict' -module.exports = { - font: { - 'font-family': 'arial,sans-serif' - }, - container: { - 'margin': '10px', - 'padding': '5px' - }, - statusMessage: { - 'margin-left': '15px' - }, - address: { - 'font-style': 'italic' - }, - instructionsList: { - 'width': '600px', - 'height': '330px' - }, - transactionInfo: { - 'margin-top': '5px' - }, - panel: { - container: { - 'border': '1px solid', - 'width': '600px' - }, - tableContainer: { - 'height': '300px', - 'overflow-y': 'auto' - }, - table: { - 'padding': '5px' - }, - title: { - 'padding': '5px', - 'font-style': 'italic' - } - }, - hidden: { - 'display': 'none' - }, - display: { - 'display': 'block' - }, - sticker: { - 'vertical-align': 'top', - 'margin': '5px' - }, - inline: { - 'display': 'inline-block' - }, - vmargin: { - 'margin-top': '10px', - 'margin-bottom': '10px' - } -} diff --git a/src/ui/styles/sliderStyles.js b/src/ui/styles/sliderStyles.js deleted file mode 100644 index b559d69149..0000000000 --- a/src/ui/styles/sliderStyles.js +++ /dev/null @@ -1,16 +0,0 @@ -'use strict' -module.exports = { - rule: { - 'width': '600px' - }, - runner: { - 'position': 'absolute', - 'width': '16px', - 'height': '16px', - 'margin': '0', - 'padding': '0', - 'overflow': 'hidden', - 'border': '1px solid #a4bed4', - 'background-color': '#f1f7ff' - } -} diff --git a/src/util/web3Admin.js b/src/util/web3Admin.js deleted file mode 100644 index e0d98da6f4..0000000000 --- a/src/util/web3Admin.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict' -module.exports = { - extend: function (web3) { - // DEBUG - var methods = [] - if (!(web3.debug && web3.debug.traceTransaction)) { - methods.push(new web3._extend.Method({ - name: 'traceTransaction', - call: 'debug_traceTransaction', - inputFormatter: [null, null], - params: 2 - })) - } - - if (!(web3.debug && web3.debug.storageAt)) { - methods.push(new web3._extend.Method({ - name: 'storageAt', - call: 'debug_storageAt', - inputFormatter: [null, null, null], - params: 3 - })) - } - if (methods.length > 0) { - web3._extend({ - property: 'debug', - methods: methods, - properties: [] - }) - } - } -} diff --git a/src/web3Provider/web3VmProvider.js b/src/web3Provider/web3VmProvider.js deleted file mode 100644 index e47d510017..0000000000 --- a/src/web3Provider/web3VmProvider.js +++ /dev/null @@ -1,138 +0,0 @@ -var ethJSUtil = require('ethereumjs-util') -var util = require('../helpers/util') - -function web3VmProvider () { - var self = this - this.vm - this.vmTraces = {} - this.txs = {} - this.processingHash - this.incr = 0 - this.eth = {} - this.debug = {} - this.eth.getCode = function (address, cb) { return self.getCode(address, cb) } - this.eth.getTransaction = function (hash, cb) { return self.getTransaction(hash, cb) } - this.eth.getTransactionFromBlock = function (blockNumber, txIndex, cb) { return self.getTransactionFromBlock(blockNumber, txIndex, cb) } - this.eth.getBlockNumber = function (cb) { return self.getBlockNumber(cb) } - this.debug.traceTransaction = function (hash, options, cb) { return self.traceTransaction(hash, options, cb) } - this.debug.storageAt = function (blockNumber, txIndex, address, cb) { return self.storageAt(blockNumber, txIndex, address, cb) } - this.providers = { 'HttpProvider': function (url) {} } - this.currentProvider = {'host': 'vm provider'} -} - -web3VmProvider.prototype.setVM = function (vm) { - var self = this - this.vm = vm - this.vm.on('step', function (data) { - self.pushTrace(self, data) - }) - this.vm.on('afterTx', function (data) { - self.txProcessed(self, data) - }) - this.vm.on('beforeTx', function (data) { - self.txWillProcess(self, data) - }) -} - -web3VmProvider.prototype.releaseCurrentHash = function () { - var ret = this.processingHash - this.processingHash = undefined - return ret -} - -web3VmProvider.prototype.txWillProcess = function (self, data) { - self.incr++ - self.processingHash = '0x' + ethJSUtil.sha3([data.r, data.s, data.v, self.incr]).join('') - self.vmTraces[self.processingHash] = { - gas: '0x0', - return: '0x0', - structLogs: [] - } - var tx = {} - tx.hash = self.processingHash - tx.from = util.hexConvert(data.getSenderAddress()) - if (data.to && data.to.length) { - tx.to = util.hexConvert(data.to) - } - tx.data = util.hexConvert(data.data) - tx.input = util.hexConvert(data.input) - tx.gas = util.hexConvert(data.gas) - if (data.value) { - tx.value = util.hexConvert(data.value) - } - self.txs[self.processingHash] = tx -} - -web3VmProvider.prototype.txProcessed = function (self, data) { - self.vmTraces[self.processingHash].gas = '0x' + data.gasUsed.toString(16) - if (data.createdAddress) { - self.vmTraces[self.processingHash].return = util.hexConvert(data.createdAddress) - } else { - self.vmTraces[self.processingHash].return = util.hexConvert(data.vm.return) - } -} - -web3VmProvider.prototype.pushTrace = function (self, data) { - if (!self.processingHash) { - console.log('no tx processing') - return - } - var step = { - stack: util.hexListConvert(data.stack), - memory: util.formatMemory(data.memory), - storage: data.storage, - op: data.opcode.name, - pc: data.pc, - gasCost: data.opcode.fee.toString(), - gas: data.gasLeft.toString() - } - self.vmTraces[self.processingHash].structLogs.push(step) -} - -web3VmProvider.prototype.getCode = function (address, cb) { - this.vm.stateManager.getContractCode(address, function (error, result) { - cb(error, util.hexConvert(result)) - }) -} - -web3VmProvider.prototype.setProvider = function (provider) {} - -web3VmProvider.prototype.traceTransaction = function (txHash, options, cb) { - if (this.vmTraces[txHash]) { - if (cb) { - cb(null, this.vmTraces[txHash]) - } - return this.vmTraces[txHash] - } else { - if (cb) { - cb('unable to retrieve traces ' + txHash, null) - } - } -} - -web3VmProvider.prototype.storageAt = function (blockNumber, txIndex, address, cb) { cb(null, {}) } - -web3VmProvider.prototype.getBlockNumber = function (cb) { cb(null, 'vm provider') } - -web3VmProvider.prototype.getTransaction = function (txHash, cb) { - if (this.txs[txHash]) { - if (cb) { - cb(null, this.txs[txHash]) - } - return this.txs[txHash] - } else { - if (cb) { - cb('unable to retrieve tx ' + txHash, null) - } - } -} - -web3VmProvider.prototype.getTransactionFromBlock = function (blockNumber, txIndex, cb) { - var mes = 'not supposed to be needed by remix in vmmode' - console.log(mes) - if (cb) { - cb(mes, null) - } -} - -module.exports = web3VmProvider diff --git a/test-browser/init.js b/test-browser/init.js deleted file mode 100644 index 11a014f8a2..0000000000 --- a/test-browser/init.js +++ /dev/null @@ -1,205 +0,0 @@ -var init = require('../test/init') -module.exports = function (browser, callback) { - extendBrowser(browser) - browser - .url('http://127.0.0.1:8080') - .waitForElementPresent('#app div', 1000) - injectScript('./test/resources/testWeb3.json', browser, function () { - callback() - }) -} - -function injectScript (file, browser, callback) { - init.readFile(file, function (error, result) { - if (!error) { - browser.execute(function (data) { - var vmdebugger = document.getElementById('app').vmdebugger - data = JSON.parse(data) - var uiTestweb3 = {} - uiTestweb3.eth = {} - uiTestweb3.debug = {} - uiTestweb3.eth.getCode = function (address, callback) { - if (callback) { - callback(null, data.testCodes[address]) - } else { - return data.testCodes[address] - } - } - - uiTestweb3.debug.traceTransaction = function (txHash, options, callback) { - callback(null, data.testTraces[txHash]) - } - - uiTestweb3.debug.storageAt = function (blockNumber, txIndex, address, callback) { - callback(null, {}) - } - - uiTestweb3.eth.getTransaction = function (txHash, callback) { - if (callback) { - callback(null, data.testTxs[txHash]) - } else { - return data.testTxs[txHash] - } - } - - uiTestweb3.eth.getTransactionFromBlock = function (blockNumber, txIndex, callback) { - if (callback) { - callback(null, data.testTxsByBlock[blockNumber + '-' + txIndex]) - } else { - return data.testTxsByBlock[blockNumber + '-' + txIndex] - } - } - - uiTestweb3.eth.getBlockNumber = function (callback) { callback(null, 'web3 modified for testing purposes :)') } - - uiTestweb3.eth.providers = { 'HttpProvider': function (url) {} } - - uiTestweb3.eth.setProvider = function (provider) {} - - uiTestweb3.currentProvider = {host: 'web3 modified for testing purposes :)'} - - vmdebugger.addProvider('TEST', uiTestweb3) - vmdebugger.switchProvider('TEST') - }, [result], function () { - callback() - }) - } - }) -} - -function extendBrowser (browser) { - browser.assertCurrentSelectedItem = function (expected) { - browser.getValue('#asmitems', function (result) { - browser.expect.element('#asmitems option[value="' + result.value + '"]').text.to.equal(expected) - }) - return browser - } - - browser.assertSticker = function (vmtracestepinfo, stepinfo, addmemoryinfo, gasinfo, remaininggasinfo, loadedaddressinfo) { - browser.expect.element('#vmtracestepinfo').text.to.equal(vmtracestepinfo) - browser.expect.element('#stepinfo').text.to.equal(stepinfo) - browser.expect.element('#addmemoryinfo').text.to.equal(addmemoryinfo) - browser.expect.element('#gasinfo').text.to.equal(gasinfo) - browser.expect.element('#remaininggasinfo').text.to.equal(remaininggasinfo) - browser.expect.element('#loadedaddressinfo').text.to.equal(loadedaddressinfo) - return browser - } - - browser.assertStack = function (value) { - return assertPanel('#stackpanel', browser, value) - } - - browser.assertStorageChanges = function (value) { - return assertPanel('#storagepanel', browser, value) - } - - browser.assertMemory = function (value) { - return assertPanel('#memorypanel', browser, value) - } - - browser.assertCallData = function (value) { - return assertPanel('#calldatapanel', browser, value) - } - - browser.assertCallStack = function (value) { - return assertPanel('#callstackpanel', browser, value) - } - - browser.assertStackValue = function (index, value) { - return assertPanelValue('#stackpanel', browser, index, value) - } - - browser.assertStorageChangesValue = function (index, value) { - return assertPanelValue('#storagepanel', browser, index, value) - } - - browser.assertMemoryValue = function (index, value) { - return assertPanelValue('#memorypanel', browser, index, value) - } - - browser.assertCallStackValue = function (index, value) { - return assertPanelValue('#callstackpanel', browser, index, value) - } - - browser.debugerKeyCode = { - 'Enter': 13, - 'Up': 38, - 'Down': 40, - 'Right': '39', - 'Left': 37, - 'Esc': 27, - 'SpaceBar': 32, - 'Ctrl': 17, - 'Alt': 18, - 'Shift': 16 - } - -/* browser.sendKeys is not working for safari */ -/* still not working properly -browser.fireEvent = function (el, key, times, callback) { - var data = { - 'id': el.substring(1), - 'key': key, - 'times': times - } - browser.execute(function (data) { - data = JSON.parse(data) - var el = document.getElementById(data.id) - var eventObj - console.log(el) - console.log(data) - var k = 0 - if (document.createEventObject) { - eventObj = document.createEventObject() - eventObj.keyCode = data.key - while (k < data.times) { - console.log('firing brfore createEventObject') - el.fireEvent('onkeypress', eventObj) - console.log('firing') - k++ - } - } else if (typeof (KeyboardEvent) === 'function') { - eventObj = new KeyboardEvent('keyup') - eventObj.key = data.key - eventObj.which = data.key - while (k < data.times) { - console.log('firing brfore createEvent') - el.dispatchEvent(eventObj) - console.log('firing') - k++ - } - } - }, [JSON.stringify(data)], function () { - callback() - }) -} -*/ -} - -function assertPanel (id, browser, value) { - browser.expect.element(id + ' #basicpanel').text.to.equal(value) - return browser -} - -function assertPanelValue (id, browser, index, value) { - getInnerText(id + ' #basicpanel', browser, function (result) { - var values - if (result.value.indexOf('\r\n') !== -1) { - values = result.value.split('\r\n') - } else if (result.value.indexOf('\n') !== -1) { - values = result.value.split('\n') - } else if (result.value.indexOf('\r') !== -1) { - values = result.value.split('\r') - } - browser.assert.equal(values[index], value) - }) - return browser -} - -function getInnerText (id, browser, callback) { - browser.execute(function (data) { - return document.querySelector(data).innerText - }, [id], function (result) { - callback(result) - }) -} diff --git a/test/index.js b/test/index.js deleted file mode 100644 index 3dc4dc0e23..0000000000 --- a/test/index.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict' -var tape = require('tape') -var init = require('../src/helpers/init') -tape('index', function (t) { - t.test('loadContext - web3', function (st) { - var web3 = init.loadWeb3() - st.notEqual(web3, undefined) - st.notEqual(web3, null) - st.end() - }) -}) diff --git a/test/tests.js b/test/tests.js deleted file mode 100644 index 96ea8c66d4..0000000000 --- a/test/tests.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict' -require('./index.js') -require('./traceManager.js') -require('./codeManager.js')