From 0e4ef539fc441dfc663a36a517bc63cff3196bc2 Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Fri, 11 Nov 2016 15:00:04 +0100 Subject: [PATCH 01/20] Solidity Compiler in UI (#3279) * Added new Deploy Contract page // Use Brace in React #2276 * Adding Web Wrokers WIP * Compiling Solidity code // Getting mandatory params #2276 * Working editor and deployment #2276 * WIP : displaying source code * Added Solidity hightling, editor component in UI * Re-adding the standard Deploy Modal #2276 * Using MobX in Contract Edition // Save to Localstorage #2276 * User select Solidity version #2276 * Loading Solidity versions and closing worker properly #2276 * Adds export to solidity editor #2276 * Adding Import to Contract Editor #2276 * Persistent Worker => Don't load twice Solidity Code #2276 * UI Fixes * Editor tweaks * Added Details with ABI in Contract view * Adds Save capabilities to contract editor // WIP on Load #3279 * Working Load and Save contracts... #3231 * Adding loader of Snippets // Export with name #3279 * Added snippets / Importing from files and from URL * Fix wrong ID in saved Contract * Fix lint * Fixed Formal errors as warning #3279 * Fixing lint issues * Use NPM Module for valid URL (fixes linting issue too) * Don't clobber tests. --- js/package.json | 9 +- .../snippets/human-standard-token.sol | 60 ++ js/src/contracts/snippets/standard-token.sol | 55 + js/src/contracts/snippets/token.sol | 47 + js/src/index.js | 3 +- .../DeployContract/DetailsStep/detailsStep.js | 31 +- .../modals/DeployContract/deployContract.js | 43 +- js/src/modals/LoadContract/index.js | 17 + js/src/modals/LoadContract/loadContract.css | 52 + js/src/modals/LoadContract/loadContract.js | 284 +++++ js/src/modals/SaveContract/index.js | 17 + js/src/modals/SaveContract/saveContract.css | 20 + js/src/modals/SaveContract/saveContract.js | 109 ++ js/src/modals/index.js | 6 +- js/src/redux/providers/compilerActions.js | 37 + js/src/redux/providers/compilerReducer.js | 29 + js/src/redux/providers/compilerWorker.js | 177 ++++ js/src/redux/providers/index.js | 1 + js/src/redux/reducers.js | 3 +- js/src/ui/Actionbar/Import/import.css | 45 + js/src/ui/Actionbar/Import/import.js | 198 ++++ js/src/ui/Actionbar/Import/index.js | 17 + js/src/ui/Button/button.js | 13 +- js/src/ui/Editor/editor.js | 103 ++ js/src/ui/Editor/index.js | 17 + js/src/ui/Editor/mode-solidity.js | 994 ++++++++++++++++++ js/src/ui/index.js | 4 + js/src/views/Accounts/List/list.js | 14 +- js/src/views/Application/TabBar/tabBar.js | 3 +- js/src/views/Application/application.css | 2 + js/src/views/Contract/contract.css | 9 + js/src/views/Contract/contract.js | 78 +- js/src/views/Contracts/contracts.js | 11 + js/src/views/WriteContract/index.js | 17 + js/src/views/WriteContract/writeContract.css | 174 +++ js/src/views/WriteContract/writeContract.js | 502 +++++++++ .../views/WriteContract/writeContractStore.js | 373 +++++++ js/src/views/index.js | 2 + 38 files changed, 3555 insertions(+), 21 deletions(-) create mode 100644 js/src/contracts/snippets/human-standard-token.sol create mode 100644 js/src/contracts/snippets/standard-token.sol create mode 100644 js/src/contracts/snippets/token.sol create mode 100644 js/src/modals/LoadContract/index.js create mode 100644 js/src/modals/LoadContract/loadContract.css create mode 100644 js/src/modals/LoadContract/loadContract.js create mode 100644 js/src/modals/SaveContract/index.js create mode 100644 js/src/modals/SaveContract/saveContract.css create mode 100644 js/src/modals/SaveContract/saveContract.js create mode 100644 js/src/redux/providers/compilerActions.js create mode 100644 js/src/redux/providers/compilerReducer.js create mode 100644 js/src/redux/providers/compilerWorker.js create mode 100644 js/src/ui/Actionbar/Import/import.css create mode 100644 js/src/ui/Actionbar/Import/import.js create mode 100644 js/src/ui/Actionbar/Import/index.js create mode 100644 js/src/ui/Editor/editor.js create mode 100644 js/src/ui/Editor/index.js create mode 100644 js/src/ui/Editor/mode-solidity.js create mode 100644 js/src/views/WriteContract/index.js create mode 100644 js/src/views/WriteContract/writeContract.css create mode 100644 js/src/views/WriteContract/writeContract.js create mode 100644 js/src/views/WriteContract/writeContractStore.js diff --git a/js/package.json b/js/package.json index 549d55dc8..573e623d0 100644 --- a/js/package.json +++ b/js/package.json @@ -100,6 +100,7 @@ "postcss-loader": "^0.8.1", "postcss-nested": "^1.0.0", "postcss-simple-vars": "^3.0.0", + "raw-loader": "^0.5.1", "react-addons-test-utils": "^15.3.0", "react-copy-to-clipboard": "^4.2.3", "react-hot-loader": "^1.3.0", @@ -118,6 +119,7 @@ "dependencies": { "bignumber.js": "^2.3.0", "blockies": "0.0.2", + "brace": "^0.9.0", "bytes": "^2.4.0", "chart.js": "^2.3.0", "es6-promise": "^3.2.1", @@ -138,9 +140,11 @@ "moment": "^2.14.1", "qs": "^6.3.0", "react": "^15.2.1", + "react-ace": "^4.0.0", "react-addons-css-transition-group": "^15.2.1", "react-chartjs-2": "^1.5.0", "react-dom": "^15.2.1", + "react-dropzone": "^3.7.3", "react-redux": "^4.4.5", "react-router": "^2.6.1", "react-router-redux": "^4.0.5", @@ -152,10 +156,13 @@ "redux-thunk": "^2.1.0", "rlp": "^2.0.0", "scryptsy": "^2.0.0", + "solc": "ngotchac/solc-js", "store": "^1.3.20", "utf8": "^2.1.1", + "valid-url": "^1.0.9", "validator": "^5.7.0", "web3": "^0.17.0-beta", - "whatwg-fetch": "^1.0.0" + "whatwg-fetch": "^1.0.0", + "worker-loader": "^0.7.1" } } diff --git a/js/src/contracts/snippets/human-standard-token.sol b/js/src/contracts/snippets/human-standard-token.sol new file mode 100644 index 000000000..db05bbc7d --- /dev/null +++ b/js/src/contracts/snippets/human-standard-token.sol @@ -0,0 +1,60 @@ +/* +This Token Contract implements the standard token functionality (https://github.com/ethereum/EIPs/issues/20) as well as the following OPTIONAL extras intended for use by humans. + +In other words. This is intended for deployment in something like a Token Factory or Mist wallet, and then used by humans. +Imagine coins, currencies, shares, voting weight, etc. +Machine-based, rapid creation of many tokens would not necessarily need these extra features or will be minted in other manners. + +1) Initial Finite Supply (upon creation one specifies how much is minted). +2) In the absence of a token registry: Optional Decimal, Symbol & Name. +3) Optional approveAndCall() functionality to notify a contract if an approval() has occurred. + +.*/ + +import "StandardToken.sol"; + +contract HumanStandardToken is StandardToken { + + function () { + //if ether is sent to this address, send it back. + throw; + } + + /* Public variables of the token */ + + /* + NOTE: + The following variables are OPTIONAL vanities. One does not have to include them. + They allow one to customise the token contract & in no way influences the core functionality. + Some wallets/interfaces might not even bother to look at this information. + */ + string public name; //fancy name: eg Simon Bucks + uint8 public decimals; //How many decimals to show. ie. There could 1000 base units with 3 decimals. Meaning 0.980 SBX = 980 base units. It's like comparing 1 wei to 1 ether. + string public symbol; //An identifier: eg SBX + string public version = 'H0.1'; //human 0.1 standard. Just an arbitrary versioning scheme. + + function HumanStandardToken( + uint256 _initialAmount, + string _tokenName, + uint8 _decimalUnits, + string _tokenSymbol + ) { + balances[msg.sender] = _initialAmount; // Give the creator all initial tokens + totalSupply = _initialAmount; // Update total supply + name = _tokenName; // Set the name for display purposes + decimals = _decimalUnits; // Amount of decimals for display purposes + symbol = _tokenSymbol; // Set the symbol for display purposes + } + + /* Approves and then calls the receiving contract */ + function approveAndCall(address _spender, uint256 _value, bytes _extraData) returns (bool success) { + allowed[msg.sender][_spender] = _value; + Approval(msg.sender, _spender, _value); + + //call the receiveApproval function on the contract you want to be notified. This crafts the function signature manually so one doesn't have to include a contract in here just for this. + //receiveApproval(address _from, uint256 _value, address _tokenContract, bytes _extraData) + //it is assumed that when does this that the call *should* succeed, otherwise one would use vanilla approve instead. + if(!_spender.call(bytes4(bytes32(sha3("receiveApproval(address,uint256,address,bytes)"))), msg.sender, _value, this, _extraData)) { throw; } + return true; + } +} diff --git a/js/src/contracts/snippets/standard-token.sol b/js/src/contracts/snippets/standard-token.sol new file mode 100644 index 000000000..3d91e5510 --- /dev/null +++ b/js/src/contracts/snippets/standard-token.sol @@ -0,0 +1,55 @@ +/* +You should inherit from StandardToken or, for a token like you would want to +deploy in something like Mist, see HumanStandardToken.sol. +(This implements ONLY the standard functions and NOTHING else. +If you deploy this, you won't have anything useful.) + +Implements ERC 20 Token standard: https://github.com/ethereum/EIPs/issues/20 +.*/ + +import "Token.sol"; + +contract StandardToken is Token { + + function transfer(address _to, uint256 _value) returns (bool success) { + //Default assumes totalSupply can't be over max (2^256 - 1). + //If your token leaves out totalSupply and can issue more tokens as time goes on, you need to check if it doesn't wrap. + //Replace the if with this one instead. + //if (balances[msg.sender] >= _value && balances[_to] + _value > balances[_to]) { + if (balances[msg.sender] >= _value && _value > 0) { + balances[msg.sender] -= _value; + balances[_to] += _value; + Transfer(msg.sender, _to, _value); + return true; + } else { return false; } + } + + function transferFrom(address _from, address _to, uint256 _value) returns (bool success) { + //same as above. Replace this line with the following if you want to protect against wrapping uints. + //if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && balances[_to] + _value > balances[_to]) { + if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value && _value > 0) { + balances[_to] += _value; + balances[_from] -= _value; + allowed[_from][msg.sender] -= _value; + Transfer(_from, _to, _value); + return true; + } else { return false; } + } + + function balanceOf(address _owner) constant returns (uint256 balance) { + return balances[_owner]; + } + + function approve(address _spender, uint256 _value) returns (bool success) { + allowed[msg.sender][_spender] = _value; + Approval(msg.sender, _spender, _value); + return true; + } + + function allowance(address _owner, address _spender) constant returns (uint256 remaining) { + return allowed[_owner][_spender]; + } + + mapping (address => uint256) balances; + mapping (address => mapping (address => uint256)) allowed; +} diff --git a/js/src/contracts/snippets/token.sol b/js/src/contracts/snippets/token.sol new file mode 100644 index 000000000..d54c5c424 --- /dev/null +++ b/js/src/contracts/snippets/token.sol @@ -0,0 +1,47 @@ +// Abstract contract for the full ERC 20 Token standard +// https://github.com/ethereum/EIPs/issues/20 + +contract Token { + /* This is a slight change to the ERC20 base standard. + function totalSupply() constant returns (uint256 supply); + is replaced with: + uint256 public totalSupply; + This automatically creates a getter function for the totalSupply. + This is moved to the base contract since public getter functions are not + currently recognised as an implementation of the matching abstract + function by the compiler. + */ + /// total amount of tokens + uint256 public totalSupply; + + /// @param _owner The address from which the balance will be retrieved + /// @return The balance + function balanceOf(address _owner) constant returns (uint256 balance); + + /// @notice send `_value` token to `_to` from `msg.sender` + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + /// @return Whether the transfer was successful or not + function transfer(address _to, uint256 _value) returns (bool success); + + /// @notice send `_value` token to `_to` from `_from` on the condition it is approved by `_from` + /// @param _from The address of the sender + /// @param _to The address of the recipient + /// @param _value The amount of token to be transferred + /// @return Whether the transfer was successful or not + function transferFrom(address _from, address _to, uint256 _value) returns (bool success); + + /// @notice `msg.sender` approves `_addr` to spend `_value` tokens + /// @param _spender The address of the account able to transfer the tokens + /// @param _value The amount of wei to be approved for transfer + /// @return Whether the approval was successful or not + function approve(address _spender, uint256 _value) returns (bool success); + + /// @param _owner The address of the account owning tokens + /// @param _spender The address of the account able to transfer the tokens + /// @return Amount of remaining tokens allowed to spent + function allowance(address _owner, address _spender) constant returns (uint256 remaining); + + event Transfer(address indexed _from, address indexed _to, uint256 _value); + event Approval(address indexed _owner, address indexed _spender, uint256 _value); +} diff --git a/js/src/index.js b/js/src/index.js index 966e2708e..c0f4f94ad 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -31,7 +31,7 @@ import ContractInstances from './contracts'; import { initStore } from './redux'; import { ContextProvider, muiTheme } from './ui'; -import { Accounts, Account, Addresses, Address, Application, Contract, Contracts, Dapp, Dapps, Settings, SettingsBackground, SettingsParity, SettingsProxy, SettingsViews, Signer, Status } from './views'; +import { Accounts, Account, Addresses, Address, Application, Contract, Contracts, WriteContract, Dapp, Dapps, Settings, SettingsBackground, SettingsParity, SettingsProxy, SettingsViews, Signer, Status } from './views'; import { setApi } from './redux/providers/apiActions'; @@ -76,6 +76,7 @@ ReactDOM.render( + diff --git a/js/src/modals/DeployContract/DetailsStep/detailsStep.js b/js/src/modals/DeployContract/DetailsStep/detailsStep.js index e0f02bc70..854715396 100644 --- a/js/src/modals/DeployContract/DetailsStep/detailsStep.js +++ b/js/src/modals/DeployContract/DetailsStep/detailsStep.js @@ -25,7 +25,7 @@ import styles from '../deployContract.css'; export default class DetailsStep extends Component { static contextTypes = { api: PropTypes.object.isRequired - } + }; static propTypes = { accounts: PropTypes.object.isRequired, @@ -46,16 +46,33 @@ export default class DetailsStep extends Component { onFromAddressChange: PropTypes.func.isRequired, onDescriptionChange: PropTypes.func.isRequired, onNameChange: PropTypes.func.isRequired, - onParamsChange: PropTypes.func.isRequired - } + onParamsChange: PropTypes.func.isRequired, + readOnly: PropTypes.bool + }; + + static defaultProps = { + readOnly: false + }; state = { inputs: [] } + componentDidMount () { + const { abi, code } = this.props; + + if (abi) { + this.onAbiChange(abi); + } + + if (code) { + this.onCodeChange(code); + } + } + render () { const { accounts } = this.props; - const { abi, abiError, code, codeError, fromAddress, fromAddressError, name, nameError } = this.props; + const { abi, abiError, code, codeError, fromAddress, fromAddressError, name, nameError, readOnly } = this.props; return (
@@ -77,13 +94,15 @@ export default class DetailsStep extends Component { hint='the abi of the contract to deploy' error={ abiError } value={ abi } - onSubmit={ this.onAbiChange } /> + onSubmit={ this.onAbiChange } + readOnly={ readOnly } /> + onSubmit={ this.onCodeChange } + readOnly={ readOnly } /> { this.renderConstructorInputs() }
); diff --git a/js/src/modals/DeployContract/deployContract.js b/js/src/modals/DeployContract/deployContract.js index 588d16f6a..768723d1f 100644 --- a/js/src/modals/DeployContract/deployContract.js +++ b/js/src/modals/DeployContract/deployContract.js @@ -36,8 +36,17 @@ export default class DeployContract extends Component { static propTypes = { accounts: PropTypes.object.isRequired, - onClose: PropTypes.func.isRequired - } + onClose: PropTypes.func.isRequired, + abi: PropTypes.string, + code: PropTypes.string, + readOnly: PropTypes.bool, + source: PropTypes.string + }; + + static defaultProps = { + readOnly: false, + source: '' + }; state = { abi: '', @@ -57,6 +66,31 @@ export default class DeployContract extends Component { deployError: null } + componentWillMount () { + const { abi, code } = this.props; + + if (abi && code) { + this.setState({ abi, code }); + } + } + + componentWillReceiveProps (nextProps) { + const { abi, code } = nextProps; + const newState = {}; + + if (abi !== this.props.abi) { + newState.abi = abi; + } + + if (code !== this.props.code) { + newState.code = code; + } + + if (Object.keys(newState).length) { + this.setState(newState); + } + } + render () { const { step, deployError } = this.state; @@ -115,7 +149,7 @@ export default class DeployContract extends Component { } renderStep () { - const { accounts } = this.props; + const { accounts, readOnly } = this.props; const { address, deployError, step, deployState, txhash } = this.state; if (deployError) { @@ -129,6 +163,7 @@ export default class DeployContract extends Component { return ( { const { api, store } = this.context; + const { source } = this.props; const { abiParsed, code, description, name, params, fromAddress } = this.state; const options = { data: code, @@ -219,6 +255,7 @@ export default class DeployContract extends Component { contract: true, timestamp: Date.now(), deleted: false, + source, description }) ]) diff --git a/js/src/modals/LoadContract/index.js b/js/src/modals/LoadContract/index.js new file mode 100644 index 000000000..be5e62af5 --- /dev/null +++ b/js/src/modals/LoadContract/index.js @@ -0,0 +1,17 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './loadContract'; diff --git a/js/src/modals/LoadContract/loadContract.css b/js/src/modals/LoadContract/loadContract.css new file mode 100644 index 000000000..f3144eeb8 --- /dev/null +++ b/js/src/modals/LoadContract/loadContract.css @@ -0,0 +1,52 @@ +/* Copyright 2015, 2016 Ethcore (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +.loadContainer { + display: flex; + flex-direction: row; + + > * { + flex: 50%; + width: 0; + } +} + +.editor { + display: flex; + flex-direction: column; + padding-left: 1em; + + p { + line-height: 48px; + height: 48px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + margin: 0; + font-size: 1.2em; + } +} + +.confirmRemoval { + text-align: center; + + .editor { + text-align: left; + margin-top: 0.5em; + } +} diff --git a/js/src/modals/LoadContract/loadContract.js b/js/src/modals/LoadContract/loadContract.js new file mode 100644 index 000000000..3de55561a --- /dev/null +++ b/js/src/modals/LoadContract/loadContract.js @@ -0,0 +1,284 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React, { Component, PropTypes } from 'react'; + +import ContentClear from 'material-ui/svg-icons/content/clear'; +import CheckIcon from 'material-ui/svg-icons/navigation/check'; +import DeleteIcon from 'material-ui/svg-icons/action/delete'; + +import { List, ListItem, makeSelectable } from 'material-ui/List'; +import { Subheader, IconButton, Tabs, Tab } from 'material-ui'; +import moment from 'moment'; + +import { Button, Modal, Editor } from '../../ui'; + +import styles from './loadContract.css'; + +const SelectableList = makeSelectable(List); + +const SELECTED_STYLE = { + backgroundColor: 'rgba(255, 255, 255, 0.1)' +}; + +export default class LoadContract extends Component { + + static propTypes = { + onClose: PropTypes.func.isRequired, + onLoad: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired, + contracts: PropTypes.object.isRequired, + snippets: PropTypes.object.isRequired + }; + + state = { + selected: -1, + deleteRequest: false, + deleteId: -1 + }; + + render () { + const { deleteRequest } = this.state; + + const title = deleteRequest + ? 'confirm removal' + : 'view contracts'; + + return ( + + { this.renderBody() } + + ); + } + + renderBody () { + if (this.state.deleteRequest) { + return this.renderConfirmRemoval(); + } + + const { contracts, snippets } = this.props; + + const contractsTab = Object.keys(contracts).length === 0 + ? null + : ( + + { this.renderEditor() } + + + Saved Contracts + { this.renderContracts(contracts) } + + + ); + + return ( +
+ + { contractsTab } + + + { this.renderEditor() } + + + Contract Snippets + { this.renderContracts(snippets, false) } + + + +
+ ); + } + + renderConfirmRemoval () { + const { deleteId } = this.state; + const { name, timestamp, sourcecode } = this.props.contracts[deleteId]; + + return ( +
+

+ Are you sure you want to remove the following + contract from your saved contracts? +

+ + +
+ +
+
+ ); + } + + renderEditor () { + const { contracts, snippets } = this.props; + const { selected } = this.state; + + const mergedContracts = Object.assign({}, contracts, snippets); + + if (selected === -1 || !mergedContracts[selected]) { + return null; + } + + const { sourcecode, name } = mergedContracts[selected]; + + return ( +
+

{ name }

+ +
+ ); + } + + renderContracts (contracts, removable = true) { + const { selected } = this.state; + + return Object + .values(contracts) + .map((contract) => { + const { id, name, timestamp, description } = contract; + const onDelete = () => this.onDeleteRequest(id); + + const secondaryText = description || `Saved ${moment(timestamp).fromNow()}`; + const remove = removable + ? ( + + + + ) + : null; + + return ( + + ); + }); + } + + renderDialogActions () { + const { deleteRequest } = this.state; + + if (deleteRequest) { + return [ +