diff --git a/Cargo.lock b/Cargo.lock index 7b3a3d3cc..dafac3d7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1250,7 +1250,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/ethcore/js-precompiled.git#957c5a66c33f3b06a7ae804ac5edc59c20e4535b" +source = "git+https://github.com/ethcore/js-precompiled.git#427319583ccde288ba26728c14384392ddbba93d" dependencies = [ "parity-dapps-glue 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/ethcore/res/ethereum/frontier.json b/ethcore/res/ethereum/frontier.json index 9bb9c50ff..2c520c46a 100644 --- a/ethcore/res/ethereum/frontier.json +++ b/ethcore/res/ethereum/frontier.json @@ -182,8 +182,8 @@ "enode://89d5dc2a81e574c19d0465f497c1af96732d1b61a41de89c2a37f35707689ac416529fae1038809852b235c2d30fd325abdc57c122feeefbeaaf802cc7e9580d@45.55.33.62:30303", "enode://605e04a43b1156966b3a3b66b980c87b7f18522f7f712035f84576016be909a2798a438b2b17b1a8c58db314d88539a77419ca4be36148c086900fba487c9d39@188.166.255.12:30303", "enode://016b20125f447a3b203a3cae953b2ede8ffe51290c071e7599294be84317635730c397b8ff74404d6be412d539ee5bb5c3c700618723d3b53958c92bd33eaa82@159.203.210.80:30303", - "enode://01f76fa0561eca2b9a7e224378dd854278735f1449793c46ad0c4e79e8775d080c21dcc455be391e90a98153c3b05dcc8935c8440de7b56fe6d67251e33f4e3c@10.6.6.117:30303", - "enode://fe11ef89fc5ac9da358fc160857855f25bbf9e332c79b9ca7089330c02b728b2349988c6062f10982041702110745e203d26975a6b34bcc97144f9fe439034e8@10.1.72.117:30303" + "enode://01f76fa0561eca2b9a7e224378dd854278735f1449793c46ad0c4e79e8775d080c21dcc455be391e90a98153c3b05dcc8935c8440de7b56fe6d67251e33f4e3c@51.15.42.252:30303", + "enode://8d91c8137890d29110b9463882f17ae4e279cd2c90cf56573187ed1c8546fca5f590a9f05e9f108eb1bd91767ed01ede4daad9e001b61727885eaa246ddb39c2@163.172.171.38:30303" ], "accounts": { "0000000000000000000000000000000000000001": { "builtin": { "name": "ecrecover", "pricing": { "linear": { "base": 3000, "word": 0 } } } }, diff --git a/ethcore/src/miner/miner.rs b/ethcore/src/miner/miner.rs index 5530ac462..7ad18ebfc 100644 --- a/ethcore/src/miner/miner.rs +++ b/ethcore/src/miner/miner.rs @@ -21,6 +21,7 @@ use util::*; use util::using_queue::{UsingQueue, GetAction}; use account_provider::AccountProvider; use views::{BlockView, HeaderView}; +use header::Header; use state::{State, CleanupMode}; use client::{MiningBlockChainClient, Executive, Executed, EnvInfo, TransactOptions, BlockID, CallAnalytics}; use client::TransactionImportResult; @@ -577,7 +578,16 @@ impl Miner { let schedule = chain.latest_schedule(); let gas_required = |tx: &SignedTransaction| tx.gas_required(&schedule).into(); + let best_block_header: Header = ::rlp::decode(&chain.best_block_header()); transactions.into_iter() + .filter(|tx| match self.engine.verify_transaction_basic(tx, &best_block_header) { + Ok(()) => true, + Err(e) => { + debug!(target: "miner", "Rejected tx {:?} with invalid signature: {:?}", tx.hash(), e); + false + } + } + ) .map(|tx| { let origin = accounts.as_ref().and_then(|accounts| { tx.sender().ok().and_then(|sender| match accounts.contains(&sender) { diff --git a/ethcrypto/src/lib.rs b/ethcrypto/src/lib.rs index 103e750e6..c98d14027 100644 --- a/ethcrypto/src/lib.rs +++ b/ethcrypto/src/lib.rs @@ -34,16 +34,43 @@ pub const KEY_LENGTH: usize = 32; pub const KEY_ITERATIONS: usize = 10240; pub const KEY_LENGTH_AES: usize = KEY_LENGTH / 2; +#[derive(PartialEq, Debug)] +pub enum ScryptError { + // log(N) < r / 16 + InvalidN, + // p <= (2^31-1 * 32)/(128 * r) + InvalidP, +} + +impl fmt::Display for ScryptError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + let s = match *self { + ScryptError::InvalidN => "Invalid N argument of the scrypt encryption" , + ScryptError::InvalidP => "Invalid p argument of the scrypt encryption", + }; + + write!(f, "{}", s) + } +} + #[derive(PartialEq, Debug)] pub enum Error { Secp(SecpError), + Scrypt(ScryptError), InvalidMessage, } +impl From for Error { + fn from(err: ScryptError) -> Self { + Error::Scrypt(err) + } +} + impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { let s = match *self { Error::Secp(ref err) => err.to_string(), + Error::Scrypt(ref err) => err.to_string(), Error::InvalidMessage => "Invalid message".into(), }; @@ -80,13 +107,23 @@ pub fn derive_key_iterations(password: &str, salt: &[u8; 32], c: u32) -> (Vec (Vec, Vec) { +pub fn derive_key_scrypt(password: &str, salt: &[u8; 32], n: u32, p: u32, r: u32) -> Result<(Vec, Vec), Error> { + // sanity checks + let log_n = (32 - n.leading_zeros() - 1) as u8; + if log_n as u32 >= r * 16 { + return Err(Error::Scrypt(ScryptError::InvalidN)); + } + + if p as u64 > ((u32::max_value() as u64 - 1) * 32)/(128 * (r as u64)) { + return Err(Error::Scrypt(ScryptError::InvalidP)); + } + let mut derived_key = vec![0u8; KEY_LENGTH]; - let scrypt_params = ScryptParams::new(n.trailing_zeros() as u8, r, p); + let scrypt_params = ScryptParams::new(log_n, r, p); scrypt(password.as_bytes(), salt, &scrypt_params, &mut derived_key); let derived_right_bits = &derived_key[0..KEY_LENGTH_AES]; let derived_left_bits = &derived_key[KEY_LENGTH_AES..KEY_LENGTH]; - (derived_right_bits.to_vec(), derived_left_bits.to_vec()) + Ok((derived_right_bits.to_vec(), derived_left_bits.to_vec())) } pub fn derive_mac(derived_left_bits: &[u8], cipher_text: &[u8]) -> Vec { diff --git a/ethstore/src/account/safe_account.rs b/ethstore/src/account/safe_account.rs index 5dab35251..336e72875 100644 --- a/ethstore/src/account/safe_account.rs +++ b/ethstore/src/account/safe_account.rs @@ -113,7 +113,7 @@ impl Crypto { let (derived_left_bits, derived_right_bits) = match self.kdf { Kdf::Pbkdf2(ref params) => crypto::derive_key_iterations(password, ¶ms.salt, params.c), - Kdf::Scrypt(ref params) => crypto::derive_key_scrypt(password, ¶ms.salt, params.n, params.p, params.r), + Kdf::Scrypt(ref params) => try!(crypto::derive_key_scrypt(password, ¶ms.salt, params.n, params.p, params.r)), }; let mac = crypto::derive_mac(&derived_right_bits, &self.ciphertext).keccak256(); diff --git a/ethstore/src/ethstore.rs b/ethstore/src/ethstore.rs index 4360a39f0..4991c4714 100644 --- a/ethstore/src/ethstore.rs +++ b/ethstore/src/ethstore.rs @@ -86,7 +86,7 @@ impl SecretStore for EthStore { fn insert_account(&self, secret: Secret, password: &str) -> Result { let keypair = try!(KeyPair::from_secret(secret).map_err(|_| Error::CreationFailed)); let id: [u8; 16] = Random::random(); - let account = SafeAccount::create(&keypair, id, password, self.iterations, UUID::from(id).into(), "{}".to_owned()); + let account = SafeAccount::create(&keypair, id, password, self.iterations, "".to_owned(), "{}".to_owned()); let address = account.address.clone(); try!(self.save(account)); Ok(address) diff --git a/js/package.json b/js/package.json index 2a2b6d590..9a45fbf18 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "0.2.50", + "version": "0.2.54", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", @@ -43,6 +43,7 @@ "test": "mocha 'src/**/*.spec.js'", "test:coverage": "istanbul cover _mocha -- 'src/**/*.spec.js'", "test:e2e": "mocha 'src/**/*.e2e.js'", + "test:npm": "(cd .npmjs && npm i) && node test/npmLibrary && (rm -rf .npmjs/node_modules)", "prepush": "npm run lint:cached" }, "devDependencies": { @@ -101,9 +102,10 @@ "postcss-nested": "^1.0.0", "postcss-simple-vars": "^3.0.0", "raw-loader": "^0.5.1", - "react-addons-test-utils": "^15.3.0", + "react-addons-test-utils": "~15.3.2", "react-copy-to-clipboard": "^4.2.3", - "react-hot-loader": "^1.3.0", + "react-dom": "~15.3.2", + "react-hot-loader": "~1.3.0", "rucksack-css": "^0.8.6", "sinon": "^1.17.4", "sinon-as-promised": "^4.0.2", @@ -113,7 +115,7 @@ "webpack": "^1.13.2", "webpack-dev-server": "^1.15.2", "webpack-error-notification": "0.1.6", - "webpack-hot-middleware": "^2.7.1", + "webpack-hot-middleware": "~2.13.2", "websocket": "^1.0.23" }, "dependencies": { @@ -133,23 +135,24 @@ "js-sha3": "^0.5.2", "lodash": "^4.11.1", "marked": "^0.3.6", - "material-ui": "^0.16.1", + "material-ui": "0.16.1", "material-ui-chip-input": "^0.8.0", "mobx": "^2.6.1", "mobx-react": "^3.5.8", "mobx-react-devtools": "^4.2.9", "moment": "^2.14.1", + "phoneformat.js": "^1.0.3", "qs": "^6.3.0", - "react": "^15.2.1", + "react": "~15.3.2", "react-ace": "^4.0.0", - "react-addons-css-transition-group": "^15.2.1", + "react-addons-css-transition-group": "~15.3.2", "react-chartjs-2": "^1.5.0", - "react-dom": "^15.2.1", + "react-dom": "~15.3.2", "react-dropzone": "^3.7.3", "react-redux": "^4.4.5", "react-router": "^2.6.1", "react-router-redux": "^4.0.5", - "react-tap-event-plugin": "^1.0.0", + "react-tap-event-plugin": "~1.0.0", "react-tooltip": "^2.0.3", "recharts": "^0.15.2", "redux": "^3.5.2", diff --git a/js/parity.package.json b/js/parity.package.json index 7d18cc5ed..0974e072f 100644 --- a/js/parity.package.json +++ b/js/parity.package.json @@ -27,6 +27,7 @@ }, "dependencies": { "bignumber.js": "^2.3.0", - "js-sha3": "^0.5.2" + "js-sha3": "^0.5.2", + "node-fetch": "^1.6.3" } } diff --git a/js/src/api/format/input.js b/js/src/api/format/input.js index 830ca0e21..4cd1c8a56 100644 --- a/js/src/api/format/input.js +++ b/js/src/api/format/input.js @@ -166,3 +166,11 @@ export function inTraceFilter (filterObject) { return filterObject; } + +export function inTraceType (whatTrace) { + if (isString(whatTrace)) { + return [whatTrace]; + } + + return whatTrace; +} diff --git a/js/src/api/format/input.spec.js b/js/src/api/format/input.spec.js index 219886d05..a22c8d131 100644 --- a/js/src/api/format/input.spec.js +++ b/js/src/api/format/input.spec.js @@ -16,7 +16,7 @@ import BigNumber from 'bignumber.js'; -import { inAddress, inBlockNumber, inData, inFilter, inHex, inNumber10, inNumber16, inOptions } from './input'; +import { inAddress, inBlockNumber, inData, inFilter, inHex, inNumber10, inNumber16, inOptions, inTraceType } from './input'; import { isAddress } from '../../../test/types'; describe('api/format/input', () => { @@ -242,4 +242,16 @@ describe('api/format/input', () => { }); }); }); + + describe('inTraceType', () => { + it('returns array of types as is', () => { + const types = ['vmTrace', 'trace', 'stateDiff']; + expect(inTraceType(types)).to.deep.equal(types); + }); + + it('formats single string type into array', () => { + const type = 'vmTrace'; + expect(inTraceType(type)).to.deep.equal([type]); + }); + }); }); diff --git a/js/src/api/format/output.js b/js/src/api/format/output.js index 8461df20f..262a275a0 100644 --- a/js/src/api/format/output.js +++ b/js/src/api/format/output.js @@ -254,3 +254,25 @@ export function outTrace (trace) { return trace; } + +export function outTraces (traces) { + if (traces) { + return traces.map(outTrace); + } + + return traces; +} + +export function outTraceReplay (trace) { + if (trace) { + Object.keys(trace).forEach((key) => { + switch (key) { + case 'trace': + trace[key] = outTraces(trace[key]); + break; + } + }); + } + + return trace; +} diff --git a/js/src/api/rpc/trace/trace.e2e.js b/js/src/api/rpc/trace/trace.e2e.js index 1a0720927..88c0988f6 100644 --- a/js/src/api/rpc/trace/trace.e2e.js +++ b/js/src/api/rpc/trace/trace.e2e.js @@ -20,15 +20,25 @@ describe('ethapi.trace', () => { const ethapi = createHttpApi(); describe('block', () => { - it('returns the latest block', () => { - return ethapi.trace.block().then((block) => { - expect(block).to.be.ok; + it('returns the latest block traces', () => { + return ethapi.trace.block().then((traces) => { + expect(traces).to.be.ok; }); }); - it('returns a specified block', () => { - return ethapi.trace.block('0x65432').then((block) => { - expect(block).to.be.ok; + it('returns traces for a specified block', () => { + return ethapi.trace.block('0x65432').then((traces) => { + expect(traces).to.be.ok; + }); + }); + }); + + describe('replayTransaction', () => { + it('returns traces for a specific transaction', () => { + return ethapi.eth.getBlockByNumber().then((latestBlock) => { + return ethapi.trace.replayTransaction(latestBlock.transactions[0]).then((traces) => { + expect(traces).to.be.ok; + }); }); }); }); diff --git a/js/src/api/rpc/trace/trace.js b/js/src/api/rpc/trace/trace.js index 95fed4230..5c693c0b5 100644 --- a/js/src/api/rpc/trace/trace.js +++ b/js/src/api/rpc/trace/trace.js @@ -14,35 +14,53 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { inBlockNumber, inHex, inNumber16, inTraceFilter } from '../../format/input'; -import { outTrace } from '../../format/output'; +import { inBlockNumber, inData, inHex, inNumber16, inOptions, inTraceFilter, inTraceType } from '../../format/input'; +import { outTraces, outTraceReplay } from '../../format/output'; export default class Trace { constructor (transport) { this._transport = transport; } + block (blockNumber = 'latest') { + return this._transport + .execute('trace_block', inBlockNumber(blockNumber)) + .then(outTraces); + } + + call (options, blockNumber = 'latest', whatTrace = ['trace']) { + return this._transport + .execute('trace_call', inOptions(options), inBlockNumber(blockNumber), inTraceType(whatTrace)) + .then(outTraceReplay); + } + filter (filterObj) { return this._transport .execute('trace_filter', inTraceFilter(filterObj)) - .then(traces => traces.map(trace => outTrace(trace))); + .then(outTraces); } get (txHash, position) { return this._transport .execute('trace_get', inHex(txHash), inNumber16(position)) - .then(trace => outTrace(trace)); + .then(outTraces); + } + + rawTransaction (data, whatTrace = ['trace']) { + return this._transport + .execute('trace_rawTransaction', inData(data), inTraceType(whatTrace)) + .then(outTraceReplay); + } + + replayTransaction (txHash, whatTrace = ['trace']) { + return this._transport + .execute('trace_replayTransaction', txHash, inTraceType(whatTrace)) + .then(outTraceReplay); } transaction (txHash) { return this._transport .execute('trace_transaction', inHex(txHash)) - .then(traces => traces.map(trace => outTrace(trace))); - } - - block (blockNumber = 'latest') { - return this._transport - .execute('trace_block', inBlockNumber(blockNumber)) - .then(traces => traces.map(trace => outTrace(trace))); + .then(outTraces); } } diff --git a/js/src/api/transport/ws/ws.js b/js/src/api/transport/ws/ws.js index 1cb1fb1c4..7b214fded 100644 --- a/js/src/api/transport/ws/ws.js +++ b/js/src/api/transport/ws/ws.js @@ -84,7 +84,7 @@ export default class Ws extends JsonRpcBase { this._connecting = false; if (this._autoConnect) { - this._connect(); + setTimeout(() => this._connect(), 500); } } diff --git a/js/src/contracts/abi/index.js b/js/src/contracts/abi/index.js index 80f49dc5b..a6a7f0783 100644 --- a/js/src/contracts/abi/index.js +++ b/js/src/contracts/abi/index.js @@ -23,6 +23,7 @@ import githubhint from './githubhint.json'; import owned from './owned.json'; import registry from './registry.json'; import signaturereg from './signaturereg.json'; +import smsverification from './sms-verification.json'; import tokenreg from './tokenreg.json'; import wallet from './wallet.json'; @@ -36,6 +37,7 @@ export { owned, registry, signaturereg, + smsverification, tokenreg, wallet }; diff --git a/js/src/contracts/abi/sms-verification.json b/js/src/contracts/abi/sms-verification.json new file mode 100644 index 000000000..400d22b44 --- /dev/null +++ b/js/src/contracts/abi/sms-verification.json @@ -0,0 +1 @@ +[{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"}],"name":"certify","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"request","outputs":[],"payable":true,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"},{"name":"_puzzle","type":"bytes32"}],"name":"puzzle","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getAddress","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"uint256"}],"name":"setFee","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_who","type":"address"}],"name":"revoke","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_code","type":"bytes32"}],"name":"confirm","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"drain","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"delegate","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getUint","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setDelegate","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"}],"name":"certified","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"get","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Requested","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"},{"indexed":false,"name":"puzzle","type":"bytes32"}],"name":"Puzzled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Confirmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Revoked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"old","type":"address"},{"indexed":true,"name":"current","type":"address"}],"name":"NewOwner","type":"event"}] diff --git a/js/src/contracts/contracts.js b/js/src/contracts/contracts.js index a04321c7b..9d745762c 100644 --- a/js/src/contracts/contracts.js +++ b/js/src/contracts/contracts.js @@ -19,6 +19,7 @@ import Registry from './registry'; import SignatureReg from './signaturereg'; import TokenReg from './tokenreg'; import GithubHint from './githubhint'; +import smsVerification from './sms-verification'; let instance = null; @@ -54,6 +55,10 @@ export default class Contracts { return this._githubhint; } + get smsVerification () { + return smsVerification; + } + static create (api) { return new Contracts(api); } diff --git a/js/src/contracts/sms-verification.js b/js/src/contracts/sms-verification.js new file mode 100644 index 000000000..e93d57ffc --- /dev/null +++ b/js/src/contracts/sms-verification.js @@ -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 . + +import { stringify } from 'querystring'; + +export const checkIfVerified = (contract, account) => { + return contract.instance.certified.call({}, [account]); +}; + +export const checkIfRequested = (contract, account) => { + return new Promise((resolve, reject) => { + contract.subscribe('Requested', { + fromBlock: 0, toBlock: 'pending' + }, (err, logs) => { + if (err) { + return reject(err); + } + const e = logs.find((l) => { + return l.type === 'mined' && l.params.who && l.params.who.value === account; + }); + resolve(e ? e.transactionHash : false); + }); + }); +}; + +export const postToServer = (query) => { + query = stringify(query); + return fetch('https://sms-verification.parity.io/?' + query, { + method: 'POST', mode: 'cors', cache: 'no-store' + }) + .then((res) => { + return res.json().then((data) => { + if (res.ok) { + return data.message; + } + throw new Error(data.message || 'unknown error'); + }); + }); +}; diff --git a/js/src/dapps/githubhint/Application/application.css b/js/src/dapps/githubhint/Application/application.css index be04ecf34..61929f552 100644 --- a/js/src/dapps/githubhint/Application/application.css +++ b/js/src/dapps/githubhint/Application/application.css @@ -15,12 +15,17 @@ /* along with Parity. If not, see . */ -.container { +.body { + text-align: center; background: #333; + color: #fff; +} + +.container { font-family: 'Roboto'; vertical-align: middle; padding: 4em 0; - text-align: center; + margin: 0 0 2em 0; } .form { @@ -98,7 +103,7 @@ color: #333; background: #eee; border: none; - border-radius: 5px; + border-radius: 0.5em; width: 100%; font-size: 1em; text-align: center; @@ -113,20 +118,29 @@ } .hashError, .hashWarning, .hashOk { - padding-top: 0.5em; + margin: 0.5em 0; text-align: center; + padding: 1em 0; + border: 0.25em solid #333; + border-radius: 0.5em; } .hashError { + border-color: #f66; color: #f66; + background: rgba(255, 102, 102, 0.25); } .hashWarning { + border-color: #f80; color: #f80; + background: rgba(255, 236, 0, 0.25); } .hashOk { - opacity: 0.5; + border-color: #6f6; + color: #6f6; + background: rgba(102, 255, 102, 0.25); } .typeButtons { diff --git a/js/src/dapps/githubhint/Application/application.js b/js/src/dapps/githubhint/Application/application.js index 5a7494928..1690bf1c4 100644 --- a/js/src/dapps/githubhint/Application/application.js +++ b/js/src/dapps/githubhint/Application/application.js @@ -19,6 +19,7 @@ import React, { Component } from 'react'; import { api } from '../parity'; import { attachInterface } from '../services'; import Button from '../Button'; +import Events from '../Events'; import IdentityIcon from '../IdentityIcon'; import Loading from '../Loading'; @@ -27,6 +28,8 @@ import styles from './application.css'; const INVALID_URL_HASH = '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'; const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; +let nextEventId = 0; + export default class Application extends Component { state = { fromAddress: null, @@ -43,7 +46,9 @@ export default class Application extends Component { registerState: '', registerType: 'file', repo: '', - repoError: null + repoError: null, + events: {}, + eventIds: [] } componentDidMount () { @@ -75,7 +80,7 @@ export default class Application extends Component { let hashClass = null; if (contentHashError) { hashClass = contentHashOwner !== fromAddress ? styles.hashError : styles.hashWarning; - } else { + } else if (contentHash) { hashClass = styles.hashOk; } @@ -116,29 +121,34 @@ export default class Application extends Component { } return ( -
-
-
- - -
-
-
- Provide a valid URL to register. The content information can be used in other contracts that allows for reverse lookups, e.g. image registries, dapp registries, etc. +
+
+
+
+ +
- { valueInputs } -
- { contentHashError || contentHash } +
+
+ Provide a valid URL to register. The content information can be used in other contracts that allows for reverse lookups, e.g. image registries, dapp registries, etc. +
+ { valueInputs } +
+ { contentHashError || contentHash } +
+ { registerBusy ? this.renderProgress() : this.renderButtons() }
- { registerBusy ? this.renderProgress() : this.renderButtons() }
+
); } @@ -285,15 +295,29 @@ export default class Application extends Component { } } - trackRequest (promise) { + trackRequest (eventId, promise) { return promise .then((signerRequestId) => { - this.setState({ signerRequestId, registerState: 'Transaction posted, Waiting for transaction authorization' }); + this.setState({ + events: Object.assign({}, this.state.events, { + [eventId]: Object.assign({}, this.state.events[eventId], { + signerRequestId, + registerState: 'Transaction posted, Waiting for transaction authorization' + }) + }) + }); return api.pollMethod('parity_checkRequest', signerRequestId); }) .then((txHash) => { - this.setState({ txHash, registerState: 'Transaction authorized, Waiting for network confirmations' }); + this.setState({ + events: Object.assign({}, this.state.events, { + [eventId]: Object.assign({}, this.state.events[eventId], { + txHash, + registerState: 'Transaction authorized, Waiting for network confirmations' + }) + }) + }); return api.pollMethod('eth_getTransactionReceipt', txHash, (receipt) => { if (!receipt || !receipt.blockNumber || receipt.blockNumber.eq(0)) { @@ -304,27 +328,72 @@ export default class Application extends Component { }); }) .then((txReceipt) => { - this.setState({ txReceipt, registerBusy: false, registerState: 'Network confirmed, Received transaction receipt', url: '', commit: '', repo: '', commitError: null, contentHash: '', contentHashOwner: null, contentHashError: null }); + this.setState({ + events: Object.assign({}, this.state.events, { + [eventId]: Object.assign({}, this.state.events[eventId], { + txReceipt, + registerBusy: false, + registerState: 'Network confirmed, Received transaction receipt' + }) + }) + }); }) .catch((error) => { console.error('onSend', error); - this.setState({ registerError: error.message }); + + this.setState({ + events: Object.assign({}, this.state.events, { + [eventId]: Object.assign({}, this.state.events[eventId], { + registerState: error.message, + registerError: true, + registerBusy: false + }) + }) + }); }); } - registerContent (repo, commit) { + registerContent (contentRepo, contentCommit) { const { contentHash, fromAddress, instance } = this.state; + contentCommit = contentCommit.substr(0, 2) === '0x' ? contentCommit : `0x${contentCommit}`; - this.setState({ registerBusy: true, registerState: 'Estimating gas for the transaction' }); - - const values = [contentHash, repo, commit.substr(0, 2) === '0x' ? commit : `0x${commit}`]; + const eventId = nextEventId++; + const values = [contentHash, contentRepo, contentCommit]; const options = { from: fromAddress }; + this.setState({ + eventIds: [eventId].concat(this.state.eventIds), + events: Object.assign({}, this.state.events, { + [eventId]: { + contentHash, + contentRepo, + contentCommit, + fromAddress, + registerBusy: true, + registerState: 'Estimating gas for the transaction', + timestamp: new Date() + } + }), + url: '', + commit: '', + repo: '', + commitError: null, + contentHash: '', + contentHashOwner: null, + contentHashError: null + }); + this.trackRequest( - instance + eventId, instance .hint.estimateGas(options, values) .then((gas) => { - this.setState({ registerState: 'Gas estimated, Posting transaction to the network' }); + this.setState({ + events: Object.assign({}, this.state.events, { + [eventId]: Object.assign({}, this.state.events[eventId], { + registerState: 'Gas estimated, Posting transaction to the network' + }) + }) + }); const gasPassed = gas.mul(1.2); options.gas = gasPassed.toFixed(0); @@ -335,19 +404,45 @@ export default class Application extends Component { ); } - registerUrl (url) { + registerUrl (contentUrl) { const { contentHash, fromAddress, instance } = this.state; - this.setState({ registerBusy: true, registerState: 'Estimating gas for the transaction' }); - - const values = [contentHash, url]; + const eventId = nextEventId++; + const values = [contentHash, contentUrl]; const options = { from: fromAddress }; + this.setState({ + eventIds: [eventId].concat(this.state.eventIds), + events: Object.assign({}, this.state.events, { + [eventId]: { + contentHash, + contentUrl, + fromAddress, + registerBusy: true, + registerState: 'Estimating gas for the transaction', + timestamp: new Date() + } + }), + url: '', + commit: '', + repo: '', + commitError: null, + contentHash: '', + contentHashOwner: null, + contentHashError: null + }); + this.trackRequest( - instance + eventId, instance .hintURL.estimateGas(options, values) .then((gas) => { - this.setState({ registerState: 'Gas estimated, Posting transaction to the network' }); + this.setState({ + events: Object.assign({}, this.state.events, { + [eventId]: Object.assign({}, this.state.events[eventId], { + registerState: 'Gas estimated, Posting transaction to the network' + }) + }) + }); const gasPassed = gas.mul(1.2); options.gas = gasPassed.toFixed(0); diff --git a/js/src/dapps/githubhint/Events/events.css b/js/src/dapps/githubhint/Events/events.css new file mode 100644 index 000000000..a33c09236 --- /dev/null +++ b/js/src/dapps/githubhint/Events/events.css @@ -0,0 +1,37 @@ +/* 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 . +*/ + +.list { + border: none; + margin: 0 auto; + text-align: left; + vertical-align: top; + + tr { + &[data-busy="true"] { + opacity: 0.5; + } + + &[data-error="true"] { + color: #f66; + } + } + + td { + padding: 0.5em; + } +} diff --git a/js/src/dapps/githubhint/Events/events.js b/js/src/dapps/githubhint/Events/events.js new file mode 100644 index 000000000..ba74ceaea --- /dev/null +++ b/js/src/dapps/githubhint/Events/events.js @@ -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 . + +import React, { Component, PropTypes } from 'react'; +import moment from 'moment'; + +import styles from './events.css'; + +export default class Events extends Component { + static propTypes = { + eventIds: PropTypes.array.isRequired, + events: PropTypes.array.isRequired + } + + render () { + return ( + + + { this.props.eventIds.map((id) => this.renderEvent(id, this.props.events[id])) } + +
+ ); + } + + renderEvent = (eventId, event) => { + return ( + + +
{ moment(event.timestamp).fromNow() }
+
{ event.registerState }
+ + +
{ event.contentUrl || `${event.contentRepo}/${event.contentCommit}` }
+
{ event.contentHash }
+ + + ); + } +} diff --git a/js/src/dapps/githubhint/Events/index.js b/js/src/dapps/githubhint/Events/index.js new file mode 100644 index 000000000..88ad6d407 --- /dev/null +++ b/js/src/dapps/githubhint/Events/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 './events'; diff --git a/js/src/jsonrpc/interfaces/trace.js b/js/src/jsonrpc/interfaces/trace.js index 3dc4451f0..efe45f34e 100644 --- a/js/src/jsonrpc/interfaces/trace.js +++ b/js/src/jsonrpc/interfaces/trace.js @@ -14,9 +14,45 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . -import { BlockNumber, Hash, Integer } from '../types'; +import { BlockNumber, Data, Hash, Integer } from '../types'; export default { + block: { + desc: 'Returns traces created at given block', + params: [ + { + type: BlockNumber, + desc: 'Integer block number, or \'latest\' for the last mined block or \'pending\', \'earliest\' for not yet mined transactions' + } + ], + returns: { + type: Array, + desc: 'Block traces' + } + }, + + call: { + desc: 'Returns traces for a specific call', + params: [ + { + type: Object, + desc: 'Call options' + }, + { + type: BlockNumber, + desc: 'The blockNumber' + }, + { + type: Array, + desc: 'Type of trace, one or more of \'vmTrace\', \'trace\' and/or \'stateDiff\'' + } + ], + returns: { + type: Array, + desc: 'Block traces' + } + }, + filter: { desc: 'Returns traces matching given filter', params: [ @@ -49,6 +85,42 @@ export default { } }, + rawTransaction: { + desc: 'Traces a call to eth_sendRawTransaction without making the call, returning the traces', + params: [ + { + type: Data, + desc: 'Transaction data' + }, + { + type: Array, + desc: 'Type of trace, one or more of \'vmTrace\', \'trace\' and/or \'stateDiff\'' + } + ], + returns: { + type: Array, + desc: 'Block traces' + } + }, + + replayTransaction: { + desc: 'Replays a transaction, returning the traces', + params: [ + { + type: Hash, + desc: 'Transaction hash' + }, + { + type: Array, + desc: 'Type of trace, one or more of \'vmTrace\', \'trace\' and/or \'stateDiff\'' + } + ], + returns: { + type: Array, + desc: 'Block traces' + } + }, + transaction: { desc: 'Returns all traces of given transaction', params: [ @@ -61,19 +133,5 @@ export default { type: Array, desc: 'Traces of given transaction' } - }, - - block: { - desc: 'Returns traces created at given block', - params: [ - { - type: BlockNumber, - desc: 'Integer block number, or \'latest\' for the last mined block or \'pending\', \'earliest\' for not yet mined transactions' - } - ], - returns: { - type: Array, - desc: 'Block traces' - } } }; diff --git a/js/src/library.js b/js/src/library.js index fbbab2286..2b526c28e 100644 --- a/js/src/library.js +++ b/js/src/library.js @@ -14,10 +14,22 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import 'babel-polyfill/dist/polyfill.js'; +import es6Promise from 'es6-promise'; +es6Promise.polyfill(); + +const isNode = typeof global !== 'undefined' && typeof global !== 'undefined'; +const isBrowser = typeof self !== 'undefined' && typeof self.window !== 'undefined'; + +if (isBrowser) { + require('whatwg-fetch'); +} + +if (isNode) { + global.fetch = require('node-fetch'); +} + import Abi from './abi'; import Api from './api'; -export { - Abi, - Api -}; +module.exports = { Api, Abi }; diff --git a/js/src/modals/AddContract/addContract.css b/js/src/modals/AddContract/addContract.css index ed92a86d5..0821a180e 100644 --- a/js/src/modals/AddContract/addContract.css +++ b/js/src/modals/AddContract/addContract.css @@ -14,19 +14,3 @@ /* You should have received a copy of the GNU General Public License /* along with Parity. If not, see . */ - -.spaced { - margin: 0.25em 0; -} - -.typeContainer { - display: flex; - flex-direction: column; - - .desc { - font-size: 0.8em; - margin-bottom: 0.5em; - color: #ccc; - z-index: 2; - } -} diff --git a/js/src/modals/AddContract/addContract.js b/js/src/modals/AddContract/addContract.js index 4c73d3da0..110a91837 100644 --- a/js/src/modals/AddContract/addContract.js +++ b/js/src/modals/AddContract/addContract.js @@ -20,13 +20,10 @@ import ContentClear from 'material-ui/svg-icons/content/clear'; import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward'; import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back'; -import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; - -import { Button, Modal, Form, Input, InputAddress } from '../../ui'; +import { Button, Modal, Form, Input, InputAddress, RadioButtons } from '../../ui'; import { ERRORS, validateAbi, validateAddress, validateName } from '../../util/validation'; import { eip20, wallet } from '../../contracts/abi'; -import styles from './addContract.css'; const ABI_TYPES = [ { @@ -105,13 +102,12 @@ export default class AddContract extends Component { const { abiTypeIndex } = this.state; return ( - - { this.renderAbiTypes() } - + /> ); } @@ -194,20 +190,13 @@ export default class AddContract extends Component { ); } - renderAbiTypes () { - return ABI_TYPES.map((type, index) => ( - - { type.label } - { type.description } -
- ) } - key={ index } - /> - )); + getAbiTypes () { + return ABI_TYPES.map((type, index) => ({ + label: type.label, + description: type.description, + key: index, + ...type + })); } onNext = () => { @@ -218,8 +207,8 @@ export default class AddContract extends Component { this.setState({ step: this.state.step - 1 }); } - onChangeABIType = (event, index) => { - const abiType = ABI_TYPES[index]; + onChangeABIType = (value, index) => { + const abiType = value || ABI_TYPES[index]; this.setState({ abiTypeIndex: index, abiType }); this.onEditAbi(abiType.value); } diff --git a/js/src/modals/DeployContract/DetailsStep/detailsStep.js b/js/src/modals/DeployContract/DetailsStep/detailsStep.js index 6e23f79c9..8d0a2457c 100644 --- a/js/src/modals/DeployContract/DetailsStep/detailsStep.js +++ b/js/src/modals/DeployContract/DetailsStep/detailsStep.js @@ -15,13 +15,12 @@ // along with Parity. If not, see . import React, { Component, PropTypes } from 'react'; +import { MenuItem } from 'material-ui'; -import { AddressSelect, Form, Input, TypedInput } from '../../../ui'; +import { AddressSelect, Form, Input, Select } from '../../../ui'; import { validateAbi } from '../../../util/validation'; import { parseAbiType } from '../../../util/abi'; -import styles from '../deployContract.css'; - export default class DetailsStep extends Component { static contextTypes = { api: PropTypes.object.isRequired @@ -29,24 +28,26 @@ export default class DetailsStep extends Component { static propTypes = { accounts: PropTypes.object.isRequired, - abi: PropTypes.string, - abiError: PropTypes.string, - code: PropTypes.string, - codeError: PropTypes.string, - description: PropTypes.string, - descriptionError: PropTypes.string, + + onFromAddressChange: PropTypes.func.isRequired, + onNameChange: PropTypes.func.isRequired, + onDescriptionChange: PropTypes.func.isRequired, + onAbiChange: PropTypes.func.isRequired, + onCodeChange: PropTypes.func.isRequired, + onParamsChange: PropTypes.func.isRequired, + onInputsChange: PropTypes.func.isRequired, + fromAddress: PropTypes.string, fromAddressError: PropTypes.string, name: PropTypes.string, nameError: PropTypes.string, - params: PropTypes.array, - paramsError: PropTypes.array, - onAbiChange: PropTypes.func.isRequired, - onCodeChange: PropTypes.func.isRequired, - onFromAddressChange: PropTypes.func.isRequired, - onDescriptionChange: PropTypes.func.isRequired, - onNameChange: PropTypes.func.isRequired, - onParamsChange: PropTypes.func.isRequired, + description: PropTypes.string, + descriptionError: PropTypes.string, + abi: PropTypes.string, + abiError: PropTypes.string, + code: PropTypes.string, + codeError: PropTypes.string, + readOnly: PropTypes.bool }; @@ -55,7 +56,9 @@ export default class DetailsStep extends Component { }; state = { - inputs: [] + solcOutput: '', + contracts: {}, + selectedContractIndex: 0 } componentDidMount () { @@ -63,6 +66,7 @@ export default class DetailsStep extends Component { if (abi) { this.onAbiChange(abi); + this.setState({ solcOutput: abi }); } if (code) { @@ -71,8 +75,19 @@ export default class DetailsStep extends Component { } render () { - const { accounts } = this.props; - const { abi, abiError, code, codeError, fromAddress, fromAddressError, name, nameError, readOnly } = this.props; + const { + accounts, + readOnly, + + fromAddress, fromAddressError, + name, nameError, + description, descriptionError, + abiError, + code, codeError + } = this.props; + + const { solcOutput, contracts } = this.state; + const solc = contracts && Object.keys(contracts).length > 0; return (
@@ -83,18 +98,30 @@ export default class DetailsStep extends Component { error={ fromAddressError } accounts={ accounts } onChange={ this.onFromAddressChange } /> + + value={ name || '' } + onChange={ this.onNameChange } /> + + + { this.renderContractSelect() } + + + readOnly={ readOnly || solc } /> - { this.renderConstructorInputs() }
); } - renderConstructorInputs () { - const { accounts, params, paramsError } = this.props; - const { inputs } = this.state; + renderContractSelect () { + const { contracts } = this.state; - if (!inputs || !inputs.length) { + if (!contracts || Object.keys(contracts).length === 0) { return null; } - return inputs.map((input, index) => { - const onChange = (value) => this.onParamChange(index, value); + const { selectedContractIndex } = this.state; + const contractsItems = Object.keys(contracts).map((name, index) => ( + + { name } + + )); - const label = `${input.name ? `${input.name}: ` : ''}${input.type}`; - const value = params[index]; - const error = paramsError[index]; - const param = parseAbiType(input.type); + return ( + + ); + } - return ( -
- -
- ); + onContractChange = (event, index) => { + const { contracts } = this.state; + const contractName = Object.keys(contracts)[index]; + const contract = contracts[contractName]; + + if (!this.props.name || this.props.name.trim() === '') { + this.onNameChange(null, contractName); + } + + const { abi, bin } = contract; + const code = /^0x/.test(bin) ? bin : `0x${bin}`; + + this.setState({ selectedContractIndex: index }, () => { + this.onAbiChange(abi); + this.onCodeChange(code); }); } + onSolcChange = (event, value) => { + // Change triggered only if valid + if (this.props.abiError) { + return null; + } + + this.onSolcSubmit(value); + } + + onSolcSubmit = (value) => { + try { + const solcParsed = JSON.parse(value); + + if (!solcParsed || !solcParsed.contracts) { + throw new Error('Wrong solc output'); + } + + this.setState({ contracts: solcParsed.contracts }, () => { + this.onContractChange(null, 0); + }); + } catch (e) { + this.setState({ contracts: null }); + this.onAbiChange(value); + } + + this.setState({ solcOutput: value }); + } + onFromAddressChange = (event, fromAddress) => { const { onFromAddressChange } = this.props; onFromAddressChange(fromAddress); } - onNameChange = (name) => { + onNameChange = (event, name) => { const { onNameChange } = this.props; onNameChange(name); } - onParamChange = (index, value) => { - const { params, onParamsChange } = this.props; + onDescriptionChange = (event, description) => { + const { onDescriptionChange } = this.props; - params[index] = value; - onParamsChange(params); + onDescriptionChange(description); } onAbiChange = (abi) => { const { api } = this.context; - const { onAbiChange, onParamsChange } = this.props; + const { onAbiChange, onParamsChange, onInputsChange } = this.props; const { abiError, abiParsed } = validateAbi(abi, api); if (!abiError) { @@ -176,10 +245,10 @@ export default class DetailsStep extends Component { }); onParamsChange(params); - this.setState({ inputs }); + onInputsChange(inputs); } else { onParamsChange([]); - this.setState({ inputs: [] }); + onInputsChange([]); } onAbiChange(abi); diff --git a/js/src/modals/DeployContract/ParametersStep/index.js b/js/src/modals/DeployContract/ParametersStep/index.js new file mode 100644 index 000000000..77545b406 --- /dev/null +++ b/js/src/modals/DeployContract/ParametersStep/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 './parametersStep'; diff --git a/js/src/modals/DeployContract/ParametersStep/parametersStep.js b/js/src/modals/DeployContract/ParametersStep/parametersStep.js new file mode 100644 index 000000000..7916c9f5a --- /dev/null +++ b/js/src/modals/DeployContract/ParametersStep/parametersStep.js @@ -0,0 +1,105 @@ +// 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 . +// 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 { Form, TypedInput } from '../../../ui'; +import { parseAbiType } from '../../../util/abi'; + +import styles from '../deployContract.css'; + +export default class ParametersStep extends Component { + static contextTypes = { + api: PropTypes.object.isRequired + }; + + static propTypes = { + accounts: PropTypes.object.isRequired, + onParamsChange: PropTypes.func.isRequired, + + inputs: PropTypes.array, + params: PropTypes.array, + paramsError: PropTypes.array + }; + + render () { + return ( +
+ { this.renderConstructorInputs() } +
+ ); + } + + renderConstructorInputs () { + const { accounts, params, paramsError } = this.props; + const { inputs } = this.props; + + if (!inputs || !inputs.length) { + return null; + } + + const inputsComponents = inputs.map((input, index) => { + const onChange = (value) => this.onParamChange(index, value); + + const label = `${input.name ? `${input.name}: ` : ''}${input.type}`; + const value = params[index]; + const error = paramsError[index]; + const param = parseAbiType(input.type); + + return ( +
+ +
+ ); + }); + + return ( +
+

Choose the contract parameters

+ { inputsComponents } +
+ ); + } + + onParamChange = (index, value) => { + const { params, onParamsChange } = this.props; + + params[index] = value; + onParamsChange(params); + } +} diff --git a/js/src/modals/DeployContract/deployContract.css b/js/src/modals/DeployContract/deployContract.css index 45fd7a852..90097df81 100644 --- a/js/src/modals/DeployContract/deployContract.css +++ b/js/src/modals/DeployContract/deployContract.css @@ -31,3 +31,7 @@ .funcparams { padding-left: 3em; } + +p { + color: rgba(255, 255, 255, 0.498039); +} diff --git a/js/src/modals/DeployContract/deployContract.js b/js/src/modals/DeployContract/deployContract.js index 996948092..55e3166e8 100644 --- a/js/src/modals/DeployContract/deployContract.js +++ b/js/src/modals/DeployContract/deployContract.js @@ -22,13 +22,19 @@ import { BusyStep, CompletedStep, CopyToClipboard, Button, IdentityIcon, Modal, import { ERRORS, validateAbi, validateCode, validateName } from '../../util/validation'; import DetailsStep from './DetailsStep'; +import ParametersStep from './ParametersStep'; import ErrorStep from './ErrorStep'; import styles from './deployContract.css'; import { ERROR_CODES } from '../../api/transport/error'; -const steps = ['contract details', 'deployment', 'completed']; +const STEPS = { + CONTRACT_DETAILS: { title: 'contract details' }, + CONTRACT_PARAMETERS: { title: 'contract parameters' }, + DEPLOYMENT: { title: 'deployment', waiting: true }, + COMPLETED: { title: 'completed' } +}; export default class DeployContract extends Component { static contextTypes = { @@ -55,7 +61,6 @@ export default class DeployContract extends Component { abiError: ERRORS.invalidAbi, code: '', codeError: ERRORS.invalidCode, - deployState: '', description: '', descriptionError: null, fromAddress: Object.keys(this.props.accounts)[0], @@ -64,9 +69,12 @@ export default class DeployContract extends Component { nameError: ERRORS.invalidName, params: [], paramsError: [], - step: 0, + inputs: [], + + deployState: '', deployError: null, - rejected: false + rejected: false, + step: 'CONTRACT_DETAILS' } componentWillMount () { @@ -95,20 +103,30 @@ export default class DeployContract extends Component { } render () { - const { step, deployError, rejected } = this.state; + const { step, deployError, rejected, inputs } = this.state; + + const realStep = Object.keys(STEPS).findIndex((k) => k === step); + const realSteps = deployError || rejected + ? null + : Object.keys(STEPS) + .filter((k) => k !== 'CONTRACT_PARAMETERS' || inputs.length > 0) + .map((k) => STEPS[k]); - const realSteps = deployError || rejected ? null : steps; const title = realSteps ? null : (deployError ? 'deployment failed' : 'rejected'); + const waiting = realSteps + ? realSteps.map((s, i) => s.waiting ? i : false).filter((v) => v !== false) + : null; + return ( s.title) : null } title={ title } - waiting={ realSteps ? [1] : null } + waiting={ waiting } visible scroll> { this.renderStep() } @@ -146,20 +164,29 @@ export default class DeployContract extends Component { } switch (step) { - case 0: + case 'CONTRACT_DETAILS': return [ cancelBtn,
+ ); + } + + let action = () => {}; + switch (phase) { + case 1: + action = store.sendRequest; + break; + case 2: + action = store.queryCode; + break; + case 3: + action = store.sendConfirmation; + break; + case 4: + action = store.done; + break; + } + + return ( +
+ { cancel } +
+ ); + } + + renderStep (phase, error) { + if (error) { + return (

{ error }

); + } + + const { + step, + fee, number, isNumberValid, isVerified, hasRequested, + requestTx, isCodeValid, confirmationTx, + setNumber, setConsentGiven, setCode + } = this.props.store; + + if (phase === 5) { + return (); + } + if (phase === 4) { + return (); + } + if (phase === 3) { + return ( + + ); + } + if (phase === 2) { + return (); + } + if (phase === 1) { + const { setNumber, setConsentGiven } = this.props.store; + return ( + + ); + } + if (phase === 0) { + return (

Preparing awesomeness!

); + } + + return null; + } +} diff --git a/js/src/modals/SMSVerification/SendConfirmation/index.js b/js/src/modals/SMSVerification/SendConfirmation/index.js new file mode 100644 index 000000000..498a8572e --- /dev/null +++ b/js/src/modals/SMSVerification/SendConfirmation/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 './sendConfirmation'; diff --git a/js/src/modals/SMSVerification/SendConfirmation/sendConfirmation.css b/js/src/modals/SMSVerification/SendConfirmation/sendConfirmation.css new file mode 100644 index 000000000..d2395f24d --- /dev/null +++ b/js/src/modals/SMSVerification/SendConfirmation/sendConfirmation.css @@ -0,0 +1,20 @@ +/* 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 . +*/ + +.centered { + text-align: center; +} diff --git a/js/src/modals/SMSVerification/SendConfirmation/sendConfirmation.js b/js/src/modals/SMSVerification/SendConfirmation/sendConfirmation.js new file mode 100644 index 000000000..a3c8e3e18 --- /dev/null +++ b/js/src/modals/SMSVerification/SendConfirmation/sendConfirmation.js @@ -0,0 +1,51 @@ +// 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 nullable from '../../../util/nullable-proptype'; + +import TxHash from '../../../ui/TxHash'; +import { + POSTING_CONFIRMATION, POSTED_CONFIRMATION +} from '../store'; + +import styles from './sendConfirmation.css'; + +export default class SendConfirmation extends Component { + static propTypes = { + step: PropTypes.any.isRequired, + tx: nullable(PropTypes.any.isRequired) + } + + render () { + const { step, tx } = this.props; + + if (step === POSTING_CONFIRMATION) { + return (

The verification code will be sent to the contract. Please authorize this using the Parity Signer.

); + } + + if (step === POSTED_CONFIRMATION) { + return ( +
+ +

Please keep this window open.

+
+ ); + } + + return null; + } +} diff --git a/js/src/modals/SMSVerification/SendRequest/index.js b/js/src/modals/SMSVerification/SendRequest/index.js new file mode 100644 index 000000000..1a8bfaf73 --- /dev/null +++ b/js/src/modals/SMSVerification/SendRequest/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 './sendRequest'; diff --git a/js/src/modals/SMSVerification/SendRequest/sendRequest.css b/js/src/modals/SMSVerification/SendRequest/sendRequest.css new file mode 100644 index 000000000..d2395f24d --- /dev/null +++ b/js/src/modals/SMSVerification/SendRequest/sendRequest.css @@ -0,0 +1,20 @@ +/* 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 . +*/ + +.centered { + text-align: center; +} diff --git a/js/src/modals/SMSVerification/SendRequest/sendRequest.js b/js/src/modals/SMSVerification/SendRequest/sendRequest.js new file mode 100644 index 000000000..6f9a6077f --- /dev/null +++ b/js/src/modals/SMSVerification/SendRequest/sendRequest.js @@ -0,0 +1,57 @@ +// 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 nullable from '../../../util/nullable-proptype'; + +import TxHash from '../../../ui/TxHash'; +import { + POSTING_REQUEST, POSTED_REQUEST, REQUESTING_SMS +} from '../store'; + +import styles from './sendRequest.css'; + +export default class SendRequest extends Component { + static propTypes = { + step: PropTypes.any.isRequired, + tx: nullable(PropTypes.any.isRequired) + } + + render () { + const { step, tx } = this.props; + + switch (step) { + case POSTING_REQUEST: + return (

A verification request will be sent to the contract. Please authorize this using the Parity Signer.

); + + case POSTED_REQUEST: + return ( +
+ +

Please keep this window open.

+
+ ); + + case REQUESTING_SMS: + return ( +

Requesting an SMS from the Parity server.

+ ); + + default: + return null; + } + } +} diff --git a/js/src/modals/SMSVerification/index.js b/js/src/modals/SMSVerification/index.js new file mode 100644 index 000000000..d9b0990db --- /dev/null +++ b/js/src/modals/SMSVerification/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 './SMSVerification'; diff --git a/js/src/modals/SMSVerification/store.js b/js/src/modals/SMSVerification/store.js new file mode 100644 index 000000000..7337f4eac --- /dev/null +++ b/js/src/modals/SMSVerification/store.js @@ -0,0 +1,246 @@ +// 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 { observable, computed, autorun, action } from 'mobx'; +import phone from 'phoneformat.js'; +import { sha3 } from '../../api/util/sha3'; + +import Contracts from '../../contracts'; + +import { checkIfVerified, checkIfRequested, postToServer } from '../../contracts/sms-verification'; +import checkIfTxFailed from '../../util/check-if-tx-failed'; +import waitForConfirmations from '../../util/wait-for-block-confirmations'; + +const validCode = /^[A-Z\s]+$/i; + +export const LOADING = 'fetching-contract'; +export const QUERY_DATA = 'query-data'; +export const POSTING_REQUEST = 'posting-request'; +export const POSTED_REQUEST = 'posted-request'; +export const REQUESTING_SMS = 'requesting-sms'; +export const REQUESTED_SMS = 'requested-sms'; +export const QUERY_CODE = 'query-code'; +export const POSTING_CONFIRMATION = 'posting-confirmation'; +export const POSTED_CONFIRMATION = 'posted-confirmation'; +export const DONE = 'done'; + +export default class VerificationStore { + @observable step = null; + @observable error = null; + + @observable contract = null; + @observable fee = null; + @observable isVerified = null; + @observable hasRequested = null; + @observable consentGiven = false; + @observable number = ''; + @observable requestTx = null; + @observable code = ''; + @observable confirmationTx = null; + + @computed get isCodeValid () { + return validCode.test(this.code); + } + @computed get isNumberValid () { + return phone.isValidNumber(this.number); + } + + @computed get isStepValid () { + if (this.step === DONE) { + return true; + } + if (this.error) { + return false; + } + + switch (this.step) { + case LOADING: + return this.contract && this.fee && this.isVerified !== null && this.hasRequested !== null; + case QUERY_DATA: + return this.isNumberValid && this.consentGiven; + case REQUESTED_SMS: + return this.requestTx; + case QUERY_CODE: + return this.isCodeValid; + case POSTED_CONFIRMATION: + return this.confirmationTx; + default: + return false; + } + } + + constructor (api, account) { + this.api = api; + this.account = account; + + this.step = LOADING; + Contracts.create(api).registry.getContract('smsVerification') + .then((contract) => { + this.contract = contract; + this.load(); + }) + .catch((err) => { + this.error = 'Failed to fetch the contract: ' + err.message; + }); + + autorun(() => { + if (this.error) { + console.error('sms verification: ' + this.error); + } + }); + } + + @action load = () => { + const { contract, account } = this; + this.step = LOADING; + + const fee = contract.instance.fee.call() + .then((fee) => { + this.fee = fee; + }) + .catch((err) => { + this.error = 'Failed to fetch the fee: ' + err.message; + }); + + const isVerified = checkIfVerified(contract, account) + .then((isVerified) => { + this.isVerified = isVerified; + }) + .catch((err) => { + this.error = 'Failed to check if verified: ' + err.message; + }); + + const hasRequested = checkIfRequested(contract, account) + .then((txHash) => { + this.hasRequested = !!txHash; + if (txHash) { + this.requestTx = txHash; + } + }) + .catch((err) => { + this.error = 'Failed to check if requested: ' + err.message; + }); + + Promise + .all([ fee, isVerified, hasRequested ]) + .then(() => { + this.step = QUERY_DATA; + }); + } + + @action setNumber = (number) => { + this.number = number; + } + + @action setConsentGiven = (consentGiven) => { + this.consentGiven = consentGiven; + } + + @action setCode = (code) => { + this.code = code; + } + + @action sendRequest = () => { + const { api, account, contract, fee, number, hasRequested } = this; + + const request = contract.functions.find((fn) => fn.name === 'request'); + const options = { from: account, value: fee.toString() }; + + let chain = Promise.resolve(); + if (!hasRequested) { + this.step = POSTING_REQUEST; + chain = request.estimateGas(options, []) + .then((gas) => { + options.gas = gas.mul(1.2).toFixed(0); + return request.postTransaction(options, []); + }) + .then((handle) => { + // TODO: The "request rejected" error doesn't have any property to + // distinguish it from other errors, so we can't give a meaningful error here. + return api.pollMethod('parity_checkRequest', handle); + }) + .then((txHash) => { + this.requestTx = txHash; + return checkIfTxFailed(api, txHash, options.gas) + .then((hasFailed) => { + if (hasFailed) { + throw new Error('Transaction failed, all gas used up.'); + } + this.step = POSTED_REQUEST; + return waitForConfirmations(api, txHash, 1); + }); + }); + } + + chain + .then(() => { + this.step = REQUESTING_SMS; + return postToServer({ number, address: account }); + }) + .then(() => { + this.step = REQUESTED_SMS; + }) + .catch((err) => { + this.error = 'Failed to request a confirmation SMS: ' + err.message; + }); + } + + @action queryCode = () => { + this.step = QUERY_CODE; + } + + @action sendConfirmation = () => { + const { api, account, contract, code } = this; + const token = sha3(code); + + const confirm = contract.functions.find((fn) => fn.name === 'confirm'); + const options = { from: account }; + const values = [ token ]; + + this.step = POSTING_CONFIRMATION; + confirm.estimateGas(options, values) + .then((gas) => { + options.gas = gas.mul(1.2).toFixed(0); + return confirm.postTransaction(options, values); + }) + .then((handle) => { + // TODO: The "request rejected" error doesn't have any property to + // distinguish it from other errors, so we can't give a meaningful error here. + return api.pollMethod('parity_checkRequest', handle); + }) + .then((txHash) => { + this.confirmationTx = txHash; + return checkIfTxFailed(api, txHash, options.gas) + .then((hasFailed) => { + if (hasFailed) { + throw new Error('Transaction failed, all gas used up.'); + } + this.step = POSTED_CONFIRMATION; + return waitForConfirmations(api, txHash, 1); + }); + }) + .then(() => { + this.step = DONE; + }) + .catch((err) => { + this.error = 'Failed to send the verification code: ' + err.message; + }); + } + + @action done = () => { + this.step = DONE; + } +} diff --git a/js/src/modals/SMSVerification/terms-of-service.js b/js/src/modals/SMSVerification/terms-of-service.js new file mode 100644 index 000000000..f61b3c97d --- /dev/null +++ b/js/src/modals/SMSVerification/terms-of-service.js @@ -0,0 +1,27 @@ +// 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 from 'react'; + +export default ( +
    +
  • This privacy notice relates to your use of the Parity SMS verification service. We take your privacy seriously and deal in an honest, direct and transparent way when it comes to your data.
  • +
  • We collect your phone number when you use this service. This is temporarily kept in memory, and then encrypted and stored in our EU servers. We only retain the cryptographic hash of the number to prevent duplicated accounts. You consent to this use.
  • +
  • You pay a fee for the cost of this service using the account you want to verify.
  • +
  • Your phone number is transmitted to a third party US SMS verification service Twilio for the sole purpose of the SMS verification. You consent to this use. Twilio’s privacy policy is here: https://www.twilio.com/legal/privacy/developer.
  • +
  • Parity Technology Limited is registered in England and Wales under company number 09760015 and complies with the Data Protection Act 1998 (UK). You may contact us via email at admin@parity.io. Our general privacy policy can be found here: https://ethcore.io/legal.html.
  • +
+); diff --git a/js/src/modals/index.js b/js/src/modals/index.js index ccecde734..d0f8f7afe 100644 --- a/js/src/modals/index.js +++ b/js/src/modals/index.js @@ -22,6 +22,7 @@ import EditMeta from './EditMeta'; import ExecuteContract from './ExecuteContract'; import FirstRun from './FirstRun'; import Shapeshift from './Shapeshift'; +import SMSVerification from './SMSVerification'; import Transfer from './Transfer'; import PasswordManager from './PasswordManager'; import SaveContract from './SaveContract'; @@ -36,6 +37,7 @@ export { ExecuteContract, FirstRun, Shapeshift, + SMSVerification, Transfer, PasswordManager, LoadContract, diff --git a/js/src/redux/actions.js b/js/src/redux/actions.js index 58e9c2a36..bb5f42a33 100644 --- a/js/src/redux/actions.js +++ b/js/src/redux/actions.js @@ -16,7 +16,7 @@ import { newError } from '../ui/Errors/actions'; import { setAddressImage } from './providers/imagesActions'; -import { clearStatusLogs, toggleStatusLogs } from './providers/statusActions'; +import { clearStatusLogs, toggleStatusLogs, toggleStatusRefresh } from './providers/statusActions'; import { toggleView } from '../views/Settings'; export { @@ -24,5 +24,6 @@ export { clearStatusLogs, setAddressImage, toggleStatusLogs, + toggleStatusRefresh, toggleView }; diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index 9f47517f5..ed7a2cbe0 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.js @@ -15,32 +15,29 @@ // along with Parity. If not, see . import { statusBlockNumber, statusCollection, statusLogs } from './statusActions'; +import { isEqual } from 'lodash'; export default class Status { constructor (store, api) { this._api = api; this._store = store; + + this._pingable = false; + this._apiStatus = {}; + this._status = {}; + this._longStatus = {}; + this._minerSettings = {}; + + this._pollPingTimeoutId = null; + this._longStatusTimeoutId = null; } start () { this._subscribeBlockNumber(); this._pollPing(); this._pollStatus(); + this._pollLongStatus(); this._pollLogs(); - this._fetchEnode(); - } - - _fetchEnode () { - this._api.parity - .enode() - .then((enode) => { - this._store.dispatch(statusCollection({ enode })); - }) - .catch(() => { - window.setTimeout(() => { - this._fetchEnode(); - }, 1000); - }); } _subscribeBlockNumber () { @@ -51,16 +48,58 @@ export default class Status { } this._store.dispatch(statusBlockNumber(blockNumber)); + + this._api.eth + .getBlockByNumber(blockNumber) + .then((block) => { + this._store.dispatch(statusCollection({ gasLimit: block.gasLimit })); + }) + .catch((error) => { + console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error); + }); }) .then((subscriptionId) => { console.log('status._subscribeBlockNumber', 'subscriptionId', subscriptionId); }); } + /** + * Pinging should be smart. It should only + * be used when the UI is connecting or the + * Node is deconnected. + * + * @see src/views/Connection/connection.js + */ + _shouldPing = () => { + const { isConnected, isConnecting } = this._apiStatus; + return isConnecting || !isConnected; + } + + _stopPollPing = () => { + if (!this._pollPingTimeoutId) { + return; + } + + clearTimeout(this._pollPingTimeoutId); + this._pollPingTimeoutId = null; + } + _pollPing = () => { - const dispatch = (status, timeout = 500) => { - this._store.dispatch(statusCollection({ isPingable: status })); - setTimeout(this._pollPing, timeout); + // Already pinging, don't try again + if (this._pollPingTimeoutId) { + return; + } + + const dispatch = (pingable, timeout = 1000) => { + if (pingable !== this._pingable) { + this._pingable = pingable; + this._store.dispatch(statusCollection({ isPingable: pingable })); + } + + this._pollPingTimeoutId = setTimeout(() => { + this._stopPollPing(); + this._pollPing(); + }, timeout); }; fetch('/', { method: 'HEAD' }) @@ -79,61 +118,162 @@ export default class Status { } _pollStatus = () => { - const { secureToken, isConnected, isConnecting, needsToken } = this._api; - const nextTimeout = (timeout = 1000) => { setTimeout(this._pollStatus, timeout); }; - this._store.dispatch(statusCollection({ isConnected, isConnecting, needsToken, secureToken })); + const { isConnected, isConnecting, needsToken, secureToken } = this._api; + + const apiStatus = { + isConnected, + isConnecting, + needsToken, + secureToken + }; + + const gotReconnected = !this._apiStatus.isConnected && apiStatus.isConnected; + + if (gotReconnected) { + this._pollLongStatus(); + } + + if (!isEqual(apiStatus, this._apiStatus)) { + this._store.dispatch(statusCollection(apiStatus)); + this._apiStatus = apiStatus; + } + + // Ping if necessary, otherwise stop pinging + if (this._shouldPing()) { + this._pollPing(); + } else { + this._stopPollPing(); + } if (!isConnected) { - nextTimeout(250); - return; + return nextTimeout(250); } + const { refreshStatus } = this._store.getState().nodeStatus; + + const statusPromises = [ this._api.eth.syncing() ]; + + if (refreshStatus) { + statusPromises.push(this._api.eth.hashrate()); + statusPromises.push(this._api.parity.netPeers()); + } + + Promise + .all(statusPromises) + .then((statusResults) => { + const status = statusResults.length === 1 + ? { + syncing: statusResults[0] + } + : { + syncing: statusResults[0], + hashrate: statusResults[1], + netPeers: statusResults[2] + }; + + if (!isEqual(status, this._status)) { + this._store.dispatch(statusCollection(status)); + this._status = status; + } + + nextTimeout(); + }) + .catch((error) => { + console.error('_pollStatus', error); + nextTimeout(250); + }); + } + + /** + * Miner settings should never changes unless + * Parity is restarted, or if the values are changed + * from the UI + */ + _pollMinerSettings = () => { + Promise + .all([ + this._api.eth.coinbase(), + this._api.parity.extraData(), + this._api.parity.minGasPrice(), + this._api.parity.gasFloorTarget() + ]) + .then(([ + coinbase, extraData, minGasPrice, gasFloorTarget + ]) => { + const minerSettings = { + coinbase, + extraData, + minGasPrice, + gasFloorTarget + }; + + if (!isEqual(minerSettings, this._minerSettings)) { + this._store.dispatch(statusCollection(minerSettings)); + this._minerSettings = minerSettings; + } + }) + .catch((error) => { + console.error('_pollMinerSettings', error); + }); + } + + /** + * The data fetched here should not change + * unless Parity is restarted. They are thus + * fetched every 30s just in case, and whenever + * the client got reconnected. + */ + _pollLongStatus = () => { + const nextTimeout = (timeout = 30000) => { + if (this._longStatusTimeoutId) { + clearTimeout(this._longStatusTimeoutId); + } + + this._longStatusTimeoutId = setTimeout(this._pollLongStatus, timeout); + }; + + // Poll Miner settings just in case + this._pollMinerSettings(); + Promise .all([ this._api.web3.clientVersion(), - this._api.eth.coinbase(), this._api.parity.defaultExtraData(), - this._api.parity.extraData(), - this._api.parity.gasFloorTarget(), - this._api.eth.hashrate(), - this._api.parity.minGasPrice(), this._api.parity.netChain(), - this._api.parity.netPeers(), this._api.parity.netPort(), - this._api.parity.nodeName(), this._api.parity.rpcSettings(), - this._api.eth.syncing() + this._api.parity.enode() ]) - .then(([clientVersion, coinbase, defaultExtraData, extraData, gasFloorTarget, hashrate, minGasPrice, netChain, netPeers, netPort, nodeName, rpcSettings, syncing, traceMode]) => { + .then(([ + clientVersion, defaultExtraData, netChain, netPort, rpcSettings, enode + ]) => { const isTest = netChain === 'morden' || netChain === 'testnet'; - this._store.dispatch(statusCollection({ + const longStatus = { clientVersion, - coinbase, defaultExtraData, - extraData, - gasFloorTarget, - hashrate, - minGasPrice, netChain, - netPeers, netPort, - nodeName, rpcSettings, - syncing, - isTest, - traceMode - })); + enode, + isTest + }; + + if (!isEqual(longStatus, this._longStatus)) { + this._store.dispatch(statusCollection(longStatus)); + this._longStatus = longStatus; + } + + nextTimeout(); }) .catch((error) => { - console.error('_pollStatus', error); + console.error('_pollLongStatus', error); + nextTimeout(250); }); - - nextTimeout(); } _pollLogs = () => { diff --git a/js/src/redux/providers/statusActions.js b/js/src/redux/providers/statusActions.js index 142cdabdc..1c175c29d 100644 --- a/js/src/redux/providers/statusActions.js +++ b/js/src/redux/providers/statusActions.js @@ -47,3 +47,10 @@ export function clearStatusLogs () { type: 'clearStatusLogs' }; } + +export function toggleStatusRefresh (refreshStatus) { + return { + type: 'toggleStatusRefresh', + refreshStatus + }; +} diff --git a/js/src/redux/providers/statusReducer.js b/js/src/redux/providers/statusReducer.js index 98bb536ae..f0b6947c0 100644 --- a/js/src/redux/providers/statusReducer.js +++ b/js/src/redux/providers/statusReducer.js @@ -28,6 +28,7 @@ const initialState = { enode: '', extraData: '', gasFloorTarget: new BigNumber(0), + gasLimit: new BigNumber(0), hashrate: new BigNumber(0), minGasPrice: new BigNumber(0), netChain: 'morden', @@ -37,12 +38,13 @@ const initialState = { max: new BigNumber(0) }, netPort: new BigNumber(0), - nodeName: '', rpcSettings: {}, syncing: false, - isApiConnected: true, - isPingConnected: true, + isConnected: false, + isConnecting: false, + isPingable: false, isTest: false, + refreshStatus: false, traceMode: undefined }; @@ -73,5 +75,10 @@ export default handleActions({ clearStatusLogs (state, action) { return Object.assign({}, state, { devLogs: [] }); + }, + + toggleStatusRefresh (state, action) { + const { refreshStatus } = action; + return Object.assign({}, state, { refreshStatus }); } }, initialState); diff --git a/js/src/ui/Form/RadioButtons/index.js b/js/src/ui/Form/RadioButtons/index.js new file mode 100644 index 000000000..c708eb728 --- /dev/null +++ b/js/src/ui/Form/RadioButtons/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 './radioButtons'; diff --git a/js/src/ui/Form/RadioButtons/radioButtons.css b/js/src/ui/Form/RadioButtons/radioButtons.css new file mode 100644 index 000000000..ed92a86d5 --- /dev/null +++ b/js/src/ui/Form/RadioButtons/radioButtons.css @@ -0,0 +1,32 @@ +/* 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 . +*/ + +.spaced { + margin: 0.25em 0; +} + +.typeContainer { + display: flex; + flex-direction: column; + + .desc { + font-size: 0.8em; + margin-bottom: 0.5em; + color: #ccc; + z-index: 2; + } +} diff --git a/js/src/ui/Form/RadioButtons/radioButtons.js b/js/src/ui/Form/RadioButtons/radioButtons.js new file mode 100644 index 000000000..8a4cf6824 --- /dev/null +++ b/js/src/ui/Form/RadioButtons/radioButtons.js @@ -0,0 +1,100 @@ +// 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 { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; + +import styles from './radioButtons.css'; + +export default class RadioButtons extends Component { + static propTypes = { + onChange: PropTypes.func.isRequired, + values: PropTypes.array.isRequired, + + value: PropTypes.any, + name: PropTypes.string + }; + + static defaultProps = { + value: 0, + name: '' + }; + + render () { + const { value, values } = this.props; + + const index = parseInt(value); + const selectedValue = typeof value !== 'object' ? values[index] : value; + const key = this.getKey(selectedValue, index); + + return ( + + { this.renderContent() } + + ); + } + + renderContent () { + const { values } = this.props; + + return values.map((value, index) => { + const label = typeof value === 'string' ? value : value.label || ''; + const description = (typeof value !== 'string' && value.description) || null; + const key = this.getKey(value, index); + + return ( + + { label } + { + description + ? ( + { description } + ) + : null + } +
+ ) } + /> + ); + }); + } + + getKey (value, index) { + if (typeof value !== 'string') { + return typeof value.key === 'undefined' ? index : value.key; + } + + return index; + } + + onChange = (event, index) => { + const { onChange, values } = this.props; + + const value = values[index] || values.find((v) => v.key === index); + onChange(value, index); + } +} diff --git a/js/src/ui/Form/index.js b/js/src/ui/Form/index.js index 46bb106f4..113f3424a 100644 --- a/js/src/ui/Form/index.js +++ b/js/src/ui/Form/index.js @@ -23,6 +23,7 @@ import InputAddressSelect from './InputAddressSelect'; import InputChip from './InputChip'; import InputInline from './InputInline'; import Select from './Select'; +import RadioButtons from './RadioButtons'; export default from './form'; export { @@ -34,5 +35,6 @@ export { InputAddressSelect, InputChip, InputInline, - Select + Select, + RadioButtons }; diff --git a/js/src/ui/Modal/modal.js b/js/src/ui/Modal/modal.js index 7137c02a4..7c2a54b4e 100644 --- a/js/src/ui/Modal/modal.js +++ b/js/src/ui/Modal/modal.js @@ -43,7 +43,7 @@ class Modal extends Component { waiting: PropTypes.array, scroll: PropTypes.bool, steps: PropTypes.array, - title: React.PropTypes.oneOfType([ + title: PropTypes.oneOfType([ PropTypes.node, PropTypes.string ]), visible: PropTypes.bool.isRequired, diff --git a/js/src/ui/Page/page.css b/js/src/ui/Page/page.css index 81f4e0e68..cdb9fc868 100644 --- a/js/src/ui/Page/page.css +++ b/js/src/ui/Page/page.css @@ -19,5 +19,5 @@ } .layout>div { - padding-bottom: 0.25em; + padding-bottom: 0.75em; } diff --git a/js/src/ui/TxHash/txHash.js b/js/src/ui/TxHash/txHash.js index 5af000499..83e5c79f5 100644 --- a/js/src/ui/TxHash/txHash.js +++ b/js/src/ui/TxHash/txHash.js @@ -31,9 +31,14 @@ class TxHash extends Component { static propTypes = { hash: PropTypes.string.isRequired, isTest: PropTypes.bool, - summary: PropTypes.bool + summary: PropTypes.bool, + maxConfirmations: PropTypes.number } + static defaultProps = { + maxConfirmations: 10 + }; + state = { blockNumber: new BigNumber(0), transaction: null, @@ -79,6 +84,7 @@ class TxHash extends Component { } renderConfirmations () { + const { maxConfirmations } = this.props; const { blockNumber, transaction } = this.state; let txBlock = 'Pending'; @@ -89,14 +95,16 @@ class TxHash extends Component { const num = blockNumber.minus(transaction.blockNumber).plus(1); txBlock = `#${transaction.blockNumber.toFormat(0)}`; confirmations = num.toFormat(0); - value = num.gt(10) ? 10 : num.toNumber(); + value = num.gt(maxConfirmations) ? maxConfirmations : num.toNumber(); } return (
diff --git a/js/src/ui/index.js b/js/src/ui/index.js index 69a7d26c3..d443d0dbc 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -29,7 +29,7 @@ import ContextProvider from './ContextProvider'; import CopyToClipboard from './CopyToClipboard'; import Editor from './Editor'; import Errors from './Errors'; -import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select } from './Form'; +import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form'; import IdentityIcon from './IdentityIcon'; import IdentityName from './IdentityName'; import MethodDecoding from './MethodDecoding'; @@ -78,6 +78,7 @@ export { muiTheme, Page, ParityBackground, + RadioButtons, SignerIcon, Tags, Tooltip, diff --git a/js/src/util/check-if-tx-failed.js b/js/src/util/check-if-tx-failed.js new file mode 100644 index 000000000..39689bedd --- /dev/null +++ b/js/src/util/check-if-tx-failed.js @@ -0,0 +1,28 @@ +// 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 . + +const checkIfTxFailed = (api, tx, gasSent) => { + return api.pollMethod('eth_getTransactionReceipt', tx) + .then((receipt) => { + // TODO: Right now, there's no way to tell wether the EVM code crashed. + // Because you usually send a bit more gas than estimated (to make sure + // it gets mined quickly), we transaction probably failed if all the gas + // has been used up. + return receipt.gasUsed.eq(gasSent); + }); +}; + +export default checkIfTxFailed; diff --git a/js/src/util/nullable-proptype.js b/js/src/util/nullable-proptype.js new file mode 100644 index 000000000..331be6c18 --- /dev/null +++ b/js/src/util/nullable-proptype.js @@ -0,0 +1,21 @@ +// 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 { PropTypes } from 'react'; + +export default function (type) { + return PropTypes.oneOfType([ PropTypes.oneOf([ null ]), type ]); +} diff --git a/js/src/util/validation.js b/js/src/util/validation.js index c41cae29e..c0a58655d 100644 --- a/js/src/util/validation.js +++ b/js/src/util/validation.js @@ -37,7 +37,7 @@ export function validateAbi (abi, api) { try { abiParsed = JSON.parse(abi); - if (!api.util.isArray(abiParsed) || !abiParsed.length) { + if (!api.util.isArray(abiParsed)) { abiError = ERRORS.invalidAbi; return { abi, abiError, abiParsed }; } diff --git a/js/src/util/wait-for-block-confirmations.js b/js/src/util/wait-for-block-confirmations.js new file mode 100644 index 000000000..79ba2be25 --- /dev/null +++ b/js/src/util/wait-for-block-confirmations.js @@ -0,0 +1,44 @@ +// 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 . + +const isValidReceipt = (receipt) => { + return receipt && receipt.blockNumber && receipt.blockNumber.gt(0); +}; + +const waitForConfirmations = (api, tx, confirmations) => { + return new Promise((resolve, reject) => { + api.pollMethod('eth_getTransactionReceipt', tx, isValidReceipt) + .then((receipt) => { + let subscription; + api.subscribe('eth_blockNumber', (err, block) => { + if (err) { + reject(err); + } else if (block.minus(confirmations - 1).gte(receipt.blockNumber)) { + if (subscription) { + api.unsubscribe(subscription); + } + resolve(); + } + }) + .then((_subscription) => { + subscription = _subscription; + }) + .catch(reject); + }); + }); +}; + +export default waitForConfirmations; diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js index 902e3e7c1..86a76073e 100644 --- a/js/src/views/Account/account.js +++ b/js/src/views/Account/account.js @@ -20,8 +20,9 @@ import { bindActionCreators } from 'redux'; import ContentCreate from 'material-ui/svg-icons/content/create'; import ContentSend from 'material-ui/svg-icons/content/send'; import LockIcon from 'material-ui/svg-icons/action/lock'; +import VerifyIcon from 'material-ui/svg-icons/action/verified-user'; -import { EditMeta, Shapeshift, Transfer, PasswordManager } from '../../modals'; +import { EditMeta, Shapeshift, SMSVerification, Transfer, PasswordManager } from '../../modals'; import { Actionbar, Button, Page } from '../../ui'; import shapeshiftBtn from '../../../assets/images/shapeshift-btn.png'; @@ -29,9 +30,15 @@ import shapeshiftBtn from '../../../assets/images/shapeshift-btn.png'; import Header from './Header'; import Transactions from './Transactions'; +import VerificationStore from '../../modals/SMSVerification/store'; + import styles from './account.css'; class Account extends Component { + static contextTypes = { + api: PropTypes.object.isRequired + } + static propTypes = { params: PropTypes.object, accounts: PropTypes.object, @@ -45,10 +52,20 @@ class Account extends Component { state = { showEditDialog: false, showFundDialog: false, + showVerificationDialog: false, + verificationStore: null, showTransferDialog: false, showPasswordDialog: false } + componentDidMount () { + const { api } = this.context; + const { address } = this.props.params; + + const store = new VerificationStore(api, address); + this.setState({ verificationStore: store }); + } + render () { const { accounts, balances, isTest } = this.props; const { address } = this.props.params; @@ -64,6 +81,7 @@ class Account extends Component {
{ this.renderEditDialog(account) } { this.renderFundDialog() } + { this.renderVerificationDialog() } { this.renderTransferDialog() } { this.renderPasswordDialog() } { this.renderActionbar() } @@ -99,6 +117,11 @@ class Account extends Component { icon={ } label='shapeshift' onClick={ this.onShapeshiftAccountClick } />, +