diff --git a/.gitignore b/.gitignore index 33d2b94..5c20c22 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ coverage.json # node node_modules/ npm-debug.log +package-lock.json # other .vs/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc827f3 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# status.im contracts + +Usage: + ``` + npm install -g embark + git clone https://github.com/status-im/contracts.git + cd contracts + npm install + embark simulator + embark test + embark run + ``` + +| Contract | Deploy | Test | UI | +| -------------------------------------- | ------ | ---- | --- | +| token/TestToken | Yes | Yes | Yes | +| token/ERC20Token | No | Yes | Yes | \ No newline at end of file diff --git a/app/components/accountlist.css b/app/components/accountlist.css new file mode 100644 index 0000000..35b9ddd --- /dev/null +++ b/app/components/accountlist.css @@ -0,0 +1,37 @@ +.identicon { + border-radius: 50%; +} + +.selectedIdenticon { + border-radius: 50%; + overflow: hidden; + float: left; + margin: 7px 0; +} + +.accountHexString { + margin-left: 7px; + width: 267px; + overflow: hidden; + text-overflow: ellipsis; + display:inline-block; +} + +.accountBalance { + margin-left: 10px; + overflow: hidden; + display: inline-block; + width:77px; + text-align: center; + text-overflow: ellipsis; +} + +.accountList { + float: left; + margin-left: 10px; +} + +.account { + display: flex; + align-items: center; +} \ No newline at end of file diff --git a/app/components/accountlist.js b/app/components/accountlist.js new file mode 100644 index 0000000..fbd3cc7 --- /dev/null +++ b/app/components/accountlist.js @@ -0,0 +1,112 @@ +import web3 from "Embark/web3" +import EmbarkJS from 'Embark/EmbarkJS'; +import React from 'react'; +import { Nav, MenuItem , NavDropdown} from 'react-bootstrap'; +import Blockies from 'react-blockies'; + +import './accountlist.css'; + +class AccList extends React.Component { + + constructor(props) { + super(props); + this.state = { + classNameNavDropdown: props.classNameNavDropdown, + defaultAccount: "0x0000000000000000000000000000000000000000", + addresses: [], + balances: [] + } + __embarkContext.execWhenReady(() => { + this.load() + }); + } + + + load() { + web3.eth.getAccounts((err, addresses) => { + if (addresses) { + var defaultAccount = web3.eth.defaultAccount; + if(!defaultAccount){ + web3.eth.defaultAccount = addresses[0]; + } + + var balances = []; + balances.length == addresses.length; + addresses.forEach((address, index) => { + web3.eth.getBalance(address, 'latest', (err, balance) => { + balances[index] = balance; + if(index+1 == balances.length){ + this.setState({ + balances: balances + }); + } + }) + }) + this.setState({ + defaultAccount: defaultAccount, + addresses: addresses + }); + + } else { + console.log("No addresses available."); + } + + }) + } + setDefaultAccount(index) { + var defaultAcc = this.state.addresses[index]; + if(defaultAcc){ + web3.eth.defaultAccount = defaultAcc; + this.setState({defaultAccount: defaultAcc }); + } else { + console.log("invalid account") + } + } + + render() { + + var accsTitle; + var accsList = []; + if (this.state.addresses) { + accsTitle = this.state.defaultAccount; + this.state.addresses.forEach( + (name, index) => { + accsList.push( + this.setDefaultAccount(index) }> +
+
+ +
+
+ {name} +
+
+ Ξ {this.state.balances[index] / (10**18)} +
+
+
); + } + ) + } + + return ( + +
+
+ +
+
+ +
+
+
+ ) + } + + } + + export default AccList; \ No newline at end of file diff --git a/app/components/erc20token.js b/app/components/erc20token.js new file mode 100644 index 0000000..6695adf --- /dev/null +++ b/app/components/erc20token.js @@ -0,0 +1,131 @@ +import EmbarkJS from 'Embark/EmbarkJS'; +import ERC20Token from 'Embark/contracts/ERC20Token'; +import React from 'react'; +import { Form, FormGroup, FormControl, HelpBlock, Button } from 'react-bootstrap'; + +class ERC20TokenUI extends React.Component { + + constructor(props) { + super(props); + this.state = { + + balanceOf: 0, + transferTo: "", + transferAmount: 0, + logs: [] + } + } + + contractAddress(e){ + e.preventDefault(); + var tokenAddress = e.target.value; + ERC20Token.options.address = tokenAddress; + } + + update_transferTo(e){ + this.setState({transferTo: e.target.value}); + } + + update_transferAmount(e){ + this.setState({transferAmount: e.target.value}); + } + + transfer(e){ + var to = this.state.transferTo; + var amount = this.state.transferAmount; + var tx = ERC20Token.methods.transfer(to, amount).send({from: web3.eth.defaultAccount}); + this._addToLog(ERC20Token.options.address+".transfer(" + to + ", "+amount+")"); + } + + approve(e){ + var to = this.state.transferTo; + var amount = this.state.transferAmount; + var tx = ERC20Token.methods.approve(to, amount).send({from: web3.eth.defaultAccount}); + this._addToLog(ERC20Token.options.address+".approve(" + to + ", "+amount+")"); + } + + balanceOf(e){ + e.preventDefault(); + var who = e.target.value; + if (EmbarkJS.isNewWeb3()) { + ERC20Token.methods.balanceOf(who).call() + .then(_value => this.setState({balanceOf: _value})) + + } else { + ERC20Token.balanceOf(who) + .then(_value => this.x({balanceOf: _value})); + } + this._addToLog(ERC20Token.options.address+".balanceOf(" + who + ")"); + } + + + _addToLog(txt){ + this.state.logs.push(txt); + this.setState({logs: this.state.logs}); + } + + render(){ + return ( + +

Set token contract address

+
+ + this.contractAddress(e)} /> + +
+ + +

Read account token balance

+
+ + + + + +
+ +

Transfer/Approve token balance

+
+ + + + + + +
+ +

Contract Calls

+

Javascript calls being made:

+
+ { + this.state.logs.map((item, i) =>

{item}

) + } +
+
+ ); + } + } + + export default ERC20TokenUI; \ No newline at end of file diff --git a/app/components/testtoken.js b/app/components/testtoken.js new file mode 100644 index 0000000..3618caa --- /dev/null +++ b/app/components/testtoken.js @@ -0,0 +1,88 @@ +import EmbarkJS from 'Embark/EmbarkJS'; +import TestToken from 'Embark/contracts/TestToken'; +import React from 'react'; +import { Form, FormGroup, FormControl, HelpBlock, Button } from 'react-bootstrap'; + +class TestTokenUI extends React.Component { + + constructor(props) { + super(props); + this.state = { + amountToMint: 100, + accountBalance: 0, + accountB: web3.eth.defaultAccount, + balanceOf: 0, + logs: [] + } + } + + handleMintAmountChange(e){ + this.setState({amountToMint: e.target.value}); + } + + mint(e){ + e.preventDefault(); + + var value = parseInt(this.state.amountToMint, 10); + + if (EmbarkJS.isNewWeb3()) { + TestToken.methods.mint(value).send({from: web3.eth.defaultAccount}); + } else { + TestToken.mint(value); + this._addToLog("#blockchain", "TestToken.mint(" + value + ")"); + } + this._addToLog(TestToken.options.address +".mint("+value+").send({from: " + web3.eth.defaultAccount + "})"); + } + + getBalance(e){ + e.preventDefault(); + + if (EmbarkJS.isNewWeb3()) { + TestToken.methods.balanceOf(web3.eth.defaultAccount).call() + .then(_value => this.setState({accountBalance: _value})) + } else { + TestToken.balanceOf(web3.eth.defaultAccount) + .then(_value => this.x({valueGet: _value})) + } + this._addToLog(TestToken.options.address + ".balanceOf(" + web3.eth.defaultAccount + ")"); + } + + _addToLog(txt){ + this.state.logs.push(txt); + this.setState({logs: this.state.logs}); + } + + render(){ + return ( +

1. Mint Test Token

+
+ + this.handleMintAmountChange(e)} /> + + +
+ +

2. Read your account token balance

+
+ + Your test token balance is {this.state.accountBalance} + + +
+ +

3. Contract Calls

+

Javascript calls being made:

+
+ { + this.state.logs.map((item, i) =>

{item}

) + } +
+
+ ); + } + } + + export default TestTokenUI; \ No newline at end of file diff --git a/app/components/topnavbar.js b/app/components/topnavbar.js new file mode 100644 index 0000000..d8bc689 --- /dev/null +++ b/app/components/topnavbar.js @@ -0,0 +1,33 @@ +import EmbarkJS from 'Embark/EmbarkJS'; +import React from 'react'; +import { Navbar, NavItem, Nav, MenuItem , NavDropdown} from 'react-bootstrap'; +import AccountList from './accountList'; + +class TopNavbar extends React.Component { + + constructor(props) { + super(props); + this.state = { + + } + + } + + render(){ + + return ( + + + + + Status.im Demo + + + + + + ); + } + } + + export default TopNavbar; \ No newline at end of file diff --git a/app/dapp.css b/app/dapp.css new file mode 100644 index 0000000..0fd02eb --- /dev/null +++ b/app/dapp.css @@ -0,0 +1,63 @@ +.navbar { + +} + +.accounts { + float: right; + margin-right: 17px; + font-family: monospace; +} + +.identicon { + border-radius: 50%; +} + + +.logs { + background-color: black; + font-size: 14px; + color: white; + font-weight: bold; + padding: 10px; + border-radius: 8px; +} + +.tab-content { + border-left: 1px solid #ddd; + border-right: 1px solid #ddd; + border-bottom: 1px solid #ddd; + padding: 10px; + margin: 0px; +} + +.nav-tabs { + margin-bottom: 0; +} + +.status-offline { + vertical-align: middle; + margin-left: 5px; + margin-top: 4px; + width: 12px; + height: 12px; + background: red; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + border-radius: 10px; +} + +.status-online { + vertical-align: middle; + margin-left: 5px; + margin-top: 4px; + width: 12px; + height: 12px; + background: mediumseagreen; + -moz-border-radius: 10px; + -webkit-border-radius: 10px; + border-radius: 10px; +} + +input.form-control { + margin: 5px; +} \ No newline at end of file diff --git a/app/dapp.js b/app/dapp.js new file mode 100644 index 0000000..a0744b3 --- /dev/null +++ b/app/dapp.js @@ -0,0 +1,51 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import { Tabs, Tab } from 'react-bootstrap'; + +import EmbarkJS from 'Embark/EmbarkJS'; +import TopNavbar from './components/topnavbar'; +import TestTokenUI from './components/testtoken'; +import ERC20TokenUI from './components/erc20token'; + +import './dapp.css'; + +class App extends React.Component { + + constructor(props) { + super(props); + + + } + + componentDidMount(){ + __embarkContext.execWhenReady(() => { + + }); + } + + + _renderStatus(title, available) { + let className = available ? 'pull-right status-online' : 'pull-right status-offline'; + return + {title} + + ; + } + + render(){ + return ( +
+ + + + + + + + + +
); + } +} + +ReactDOM.render(, document.getElementById('app')); diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..1fb8607 --- /dev/null +++ b/app/index.html @@ -0,0 +1,12 @@ + + + Status.im - Contracts + + + + +
+
+ + + diff --git a/config/blockchain.json b/config/blockchain.json index 9dccb92..638c816 100644 --- a/config/blockchain.json +++ b/config/blockchain.json @@ -9,12 +9,12 @@ "maxpeers": 0, "rpcHost": "localhost", "rpcPort": 8545, - "rpcCorsDomain": "http://localhost:8000", + "rpcCorsDomain": "auto", "account": { "password": "config/development/password" }, "targetGasLimit": 8000000, - "wsOrigins": "http://localhost:8000", + "wsOrigins": "auto", "wsRPC": true, "wsHost": "localhost", "wsPort": 8546, diff --git a/config/contracts.json b/config/contracts.json index 3ebb190..923d2c9 100644 --- a/config/contracts.json +++ b/config/contracts.json @@ -15,6 +15,9 @@ ], "gas": "auto", "contracts": { + "ERC20Receiver": { + "deploy": false + } } } } diff --git a/config/development/genesis.json b/config/development/genesis.json index 4b6ce0d..1a9501b 100644 --- a/config/development/genesis.json +++ b/config/development/genesis.json @@ -1,6 +1,8 @@ { "config": { - "homesteadBlock": 1 + "homesteadBlock": 1, + "byzantiumBlock": 1, + "daoForkSupport": true }, "nonce": "0x0000000000000042", "difficulty": "0x0", diff --git a/contracts/token/ERC20Receiver.sol b/contracts/token/ERC20Receiver.sol new file mode 100644 index 0000000..9a6af70 --- /dev/null +++ b/contracts/token/ERC20Receiver.sol @@ -0,0 +1,90 @@ +pragma solidity ^0.4.23; + +import "./ERC20Token.sol"; + +contract ERC20Receiver { + + event TokenDeposited(address indexed token, address indexed sender, uint256 amount); + event TokenWithdrawn(address indexed token, address indexed sender, uint256 amount); + + mapping (address => mapping(address => uint256)) tokenBalances; + + constructor() public { + + } + + function depositToken( + ERC20Token _token + ) + external + { + _depositToken( + msg.sender, + _token, + _token.allowance( + msg.sender, + address(this) + ) + ); + } + + function withdrawToken( + ERC20Token _token, + uint256 _amount + ) + external + { + _withdrawToken(msg.sender, _token, _amount); + } + + function depositToken( + ERC20Token _token, + uint256 _amount + ) + external + { + require(_token.allowance(msg.sender, address(this)) >= _amount); + _depositToken(msg.sender, _token, _amount); + } + + function tokenBalanceOf( + ERC20Token _token, + address _from + ) + external + view + returns(uint256 fromTokenBalance) + { + return tokenBalances[address(_token)][_from]; + } + + function _depositToken( + address _from, + ERC20Token _token, + uint256 _amount + ) + private + { + require(_amount > 0); + if (_token.transferFrom(_from, address(this), _amount)) { + tokenBalances[address(_token)][_from] += _amount; + emit TokenDeposited(address(_token), _from, _amount); + } + } + + function _withdrawToken( + address _from, + ERC20Token _token, + uint256 _amount + ) + private + { + require(_amount > 0); + require(tokenBalances[address(_token)][_from] >= _amount); + tokenBalances[address(_token)][_from] -= _amount; + require(_token.transfer(_from, _amount)); + emit TokenWithdrawn(address(_token), _from, _amount); + } + + +} \ No newline at end of file diff --git a/contracts/token/StandardToken.sol b/contracts/token/StandardToken.sol index d42852c..b80c513 100644 --- a/contracts/token/StandardToken.sol +++ b/contracts/token/StandardToken.sol @@ -66,7 +66,7 @@ contract StandardToken is ERC20Token { function totalSupply() external view - returns(uint256 supply) + returns(uint256 currentTotalSupply) { return supply; } diff --git a/embark.json b/embark.json index ba742a4..7bd3d48 100644 --- a/embark.json +++ b/embark.json @@ -1,10 +1,17 @@ { "contracts": ["contracts/**"], + "app": { + "js/dapp.js": ["app/dapp.js"], + "index.html": "app/index.html", + "images/": ["app/images/**"] + }, "buildDir": "dist/", "config": "config/", - "plugins": { - }, "versions": { - "solc": "0.4.23" + "web3": "1.0.0-beta", + "solc": "0.4.23", + "ipfs-api": "17.2.4" + }, + "plugins": { } } diff --git a/package.json b/package.json index 7cf6084..fb83081 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "status-contracts", - "version": "1.0.0", + "version": "0.0.1", + "description": "", "scripts": { "solidity-coverage": "./node_modules/.bin/solidity-coverage", "test": "embark test" @@ -15,13 +16,10 @@ "url": "https://github.com/status-im/contracts/issues" }, "homepage": "https://github.com/status-im/contracts#readme", - "devDependencies": { - "solidity-coverage": "^0.5.0", - "elliptic": "^6.4.0" - }, "dependencies": { - "embark": "^2.7.0", - "elliptic-curve": "^0.1.0", - "ethereumjs-util": "^5.1.5" + "react": "^16.3.2", + "react-blockies": "^1.3.0", + "react-bootstrap": "^0.32.1", + "react-dom": "^16.3.2" } } diff --git a/test/erc20token.js b/test/erc20token.js new file mode 100644 index 0000000..3e71542 --- /dev/null +++ b/test/erc20token.js @@ -0,0 +1,80 @@ +describe("ERC20Token", async function() { + this.timeout(0); + var ERC20Token; + var accountsArr; + before(function(done) { + var contractsConfig = { + "TestToken": { }, + "ERC20Receiver": { } + }; + EmbarkSpec.deployAll(contractsConfig, async function(accounts) { + ERC20Token = TestToken; + accountsArr = accounts; + for(i=0;i (...args) => { return new Promise((resolve, reject) => { @@ -95,3 +93,147 @@ exports.promisify = (func) => }); } + +// This has been tested with the real Ethereum network and Testrpc. +// Copied and edited from: https://gist.github.com/xavierlepretre/d5583222fde52ddfbc58b7cfa0d2d0a9 +exports.assertReverts = (contractMethodCall, maxGasAvailable) => { + return new Promise((resolve, reject) => { + try { + resolve(contractMethodCall()) + } catch (error) { + reject(error) + } + }) + .then(tx => { + assert.equal(tx.receipt.gasUsed, maxGasAvailable, "tx successful, the max gas available was not consumed") + }) + .catch(error => { + if ((error + "").indexOf("invalid opcode") < 0 && (error + "").indexOf("out of gas") < 0) { + // Checks if the error is from TestRpc. If it is then ignore it. + // Otherwise relay/throw the error produced by the above assertion. + // Note that no error is thrown when using a real Ethereum network AND the assertion above is true. + throw error + } + }) +} + +exports.listenForEvent = event => new Promise((resolve, reject) => { + event({}, (error, response) => { + if (!error) { + resolve(response.args) + } else { + reject(error) + } + event.stopWatching() + }) +}); + +exports.eventValues = (receipt, eventName) => { + if(receipt.events[eventName]) + return receipt.events[eventName].returnValues; +} + +exports.addressToBytes32 = (address) => { + const stringed = "0000000000000000000000000000000000000000000000000000000000000000" + address.slice(2); + return "0x" + stringed.substring(stringed.length - 64, stringed.length); +} + + +// OpenZeppelin's expectThrow helper - +// Source: https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/test/helpers/expectThrow.js +exports.expectThrow = async promise => { + try { + await promise; + } catch (error) { + // TODO: Check jump destination to destinguish between a throw + // and an actual invalid jump. + const invalidOpcode = error.message.search('invalid opcode') >= 0; + // TODO: When we contract A calls contract B, and B throws, instead + // of an 'invalid jump', we get an 'out of gas' error. How do + // we distinguish this from an actual out of gas event? (The + // testrpc log actually show an 'invalid jump' event.) + const outOfGas = error.message.search('out of gas') >= 0; + const revert = error.message.search('revert') >= 0; + assert( + invalidOpcode || outOfGas || revert, + 'Expected throw, got \'' + error + '\' instead', + ); + return; + } + assert.fail('Expected throw not received'); + }; + +exports.assertJump = (error) => { + assert(error.message.search('revert') > -1, 'Revert should happen'); +} + +var callbackToResolve = function (resolve, reject) { + return function (error, value) { + if (error) { + reject(error); + } else { + resolve(value); + } + }; +}; + +exports.promisify = (func) => + (...args) => { + return new Promise((resolve, reject) => { + const callback = (err, data) => err ? reject(err) : resolve(data); + func.apply(this, [...args, callback]); + }); + } + +exports.zeroAddress = '0x0000000000000000000000000000000000000000'; +exports.zeroBytes32 = "0x0000000000000000000000000000000000000000000000000000000000000000"; +exports.timeUnits = { + seconds: 1, + minutes: 60, + hours: 60 * 60, + days: 24 * 60 * 60, + weeks: 7 * 24 * 60 * 60, + years: 365 * 24 * 60 * 60 +} + +exports.ensureException = function(error) { + assert(isException(error), error.toString()); +}; + +function isException(error) { + let strError = error.toString(); + return strError.includes('invalid opcode') || strError.includes('invalid JUMP') || strError.includes('revert'); +} + +exports.increaseTime = async (amount) => { + return new Promise(function(resolve, reject) { + web3.currentProvider.sendAsync( + { + jsonrpc: '2.0', + method: 'evm_increaseTime', + params: [+amount], + id: new Date().getSeconds() + }, + (error) => { + if (error) { + console.log(error); + return reject(err); + } + web3.currentProvider.sendAsync( + { + jsonrpc: '2.0', + method: 'evm_mine', + params: [], + id: new Date().getSeconds() + }, (error) => { + if (error) { + console.log(error); + return reject(err); + } + resolve(); + } + ) + } + ) + }); +}